How to Test Browser Notifications With Cypress
Stub browser notifications in Cypress to test permission branches and app behavior without native prompts. Learn what these tests prove, how to debug them, and where browser screenshots fit.
Test browser notification behavior in Cypress by replacing the browser’s Notification API before your application loads, then control the permission result and assert the app’s response. In an end-to-end test, install the stub in cy.visit()’s onBeforeLoad callback. In a component test, stub the API before mounting the component. This keeps tests deterministic and avoids depending on a live permission prompt or operating-system notification UI.
Cypress lists browser notifications as a testing use case in its official recipes index. Its cy.stub() documentation explains how to replace functions and inspect calls. The examples below adapt that documented setup to the notification permission branches your app implements.
1. Decide what the test should prove
Separate application behavior from native browser and operating-system behavior:
| Test layer | What to assert | What it does not prove |
|---|---|---|
| Application logic with Cypress stubs | When the app asks for permission, how it handles granted, denied, and default, and whether it creates a notification with expected arguments. |
That a browser prompt appears or the operating system displays a notification. |
| Native integration check | Behavior in the actual target browser and operating system, including permission state and display behavior. | It is not a substitute for deterministic tests of all application branches. |
Cypress automation disables some browser behaviors and device permission prompts to reduce interruptions in unattended runs. Keep ordinary Cypress assertions focused on application calls and visible in-page UI. If the native prompt or operating-system display is a product requirement, validate it separately in the real target environment. See Cypress’s browser launching guidance.
2. Stub notifications in an end-to-end test
Put the stub in onBeforeLoad. Cypress calls this before the app code runs, so the application does not initialize against the real browser method first.
describe('browser notifications', () => {
it('asks for permission and handles a granted result', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.stub(win, 'Notification').as('notification')
},
})
// Replace this selector and action with the app's user flow.
cy.get('[data-cy="enable-notifications"]').click()
// Assert the app attempted to construct a notification.
cy.get('@notification').should('have.been.called')
})
})
This minimal example proves that the stub was called. Adapt the assertion to your app’s actual contract: for example, assert the notification title and options, the permission request timing, or an in-page success message. Do not assume a constructor call alone proves the browser or operating system displayed anything.
Stub the permission promise
Notification.requestPermission() returns a promise resolving to granted, denied, or default. MDN says applications treat default as denied. Control that promise to exercise each branch without opening a real prompt.
describe('notification permission states', () => {
const visitWithPermission = (permission) => {
cy.visit('/', {
onBeforeLoad(win) {
cy.stub(win, 'Notification').as('notification')
cy.stub(win.Notification, 'requestPermission')
.resolves(permission)
.as('requestPermission')
},
})
}
it('shows the enabled state when permission is granted', () => {
visitWithPermission('granted')
cy.get('[data-cy="enable-notifications"]').click()
cy.get('@requestPermission').should('have.been.calledOnce')
cy.get('[data-cy="notification-status"]')
.should('contain', 'enabled')
})
it('shows a fallback when permission is denied', () => {
visitWithPermission('denied')
cy.get('[data-cy="enable-notifications"]').click()
cy.get('[data-cy="notification-status"]')
.should('contain', 'unavailable')
})
it('handles default as a denied-like outcome', () => {
visitWithPermission('default')
cy.get('[data-cy="enable-notifications"]').click()
cy.get('[data-cy="notification-status"]')
.should('contain', 'unavailable')
})
})
These are adaptable patterns rather than a drop-in test for every app. The exact shape depends on how your code accesses the constructor and permission method. If your app wraps notifications in a service, consider asserting through that app-facing seam as well as checking the browser API call.
When the app constructs a notification
Some apps call new Notification(title, options) after permission is granted. A stubbed constructor records the call, but it does not behave exactly like the native constructor or generate a real OS notification. Assert the arguments and separately assert the app’s visible state.
cy.get('@notification').should('have.been.calledWith',
'Build complete',
{ body: 'Your report is ready' }
)
Use the title, options, and selectors your application actually uses. If construction is conditional, make the test trigger the same user action and application state that should produce the notification.
3. Stub before mounting in component tests
For component tests, install the stub before mounting so the component sees the controlled API from its first render. Cypress documents that stubs are reset and restored between tests.
it('renders the permission fallback', () => {
cy.stub(window, 'Notification').as('notification')
cy.stub(window.Notification, 'requestPermission')
.resolves('denied')
.as('requestPermission')
cy.mount(<NotificationSettings />)
cy.get('[data-cy="enable-notifications"]').click()
cy.get('[data-cy="notification-status"]')
.should('contain', 'unavailable')
})
Use the component-testing mount command configured by your project. If the component reads window.Notification during module evaluation rather than at mount time, ensure the stub exists before that code is imported or refactor the notification dependency behind an injectable function.
4. Cover the permission and interaction edge cases
- Granted: test the success path and notification construction or other expected app action.
- Denied: test the fallback, such as an explanation or an alternative in-page update.
- Default: test the same safe behavior as denied unless your product has a deliberate distinct experience. The API’s
defaultresult is treated as denial by applications. - Request timing: permission requests should follow a user interaction. Trigger the actual button or action in the test and assert the request occurs there, rather than during page initialization.
- Unsupported API: if your app supports environments without the Notification API, test that fallback separately by removing or replacing the property in the controlled test setup.
- Repeated interaction: if users can click more than once, assert whether the app avoids duplicate requests or notifications according to its intended behavior.
- Async UI: wait on Cypress assertions for the resulting UI state instead of adding arbitrary delays. Resolve the permission stub deterministically.
The Notifications API is available only in secure contexts in supporting browsers. A stub-based test exercises your application logic; it does not validate HTTPS deployment or a browser’s real permission policy. See MDN’s requestPermission() reference.
5. Run the relevant browser matrix
Cypress documents support for Chrome-family browsers and Firefox, with WebKit experimental. Select a browser with the Cypress --browser option and use the cross-browser testing guide for configuration details.
npx cypress run --browser chrome
npx cypress run --browser firefox
Choose the matrix based on the browsers your product supports. Stubbed application tests can catch browser-specific differences in app code and test setup, but they do not demonstrate native notification presentation. Validate that behavior in the actual browser and operating-system combinations that matter to your users.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The app still uses the real API or the stub has no calls. | The stub was installed after application code loaded, or the app takes a different code path. | For E2E, move setup into cy.visit()’s onBeforeLoad. For component tests, stub before mount. Confirm the test triggers the action that requests permission. |
Cannot stub requestPermission. |
The constructor stub does not expose the expected static method, or the application accesses the API differently. | Inspect how the app reads the API. Stub the method on the object the app uses, or inject a notification service and stub that boundary. Keep the setup before app initialization. |
| The test hangs waiting for an outcome. | The permission promise is unresolved, or the app never reaches the action that resolves its UI. | Configure the stub with .resolves('granted'), .resolves('denied'), or .resolves('default'), then verify the correct control was activated. |
| A native permission prompt does not appear. | Cypress automation disables some native device permission prompts. | Do not make the deterministic app test depend on the prompt. Test the permission branches with stubs; check native behavior separately in a real target environment. |
| Permission works locally but not on the deployed site. | The real Notifications API requires a secure context in supporting browsers, or the browser’s existing permission state differs. | Verify HTTPS and the target browser’s permission state in a real browser check. A stubbed test cannot verify either condition. |
| The component test sees an undefined API. | The component reads the API before the stub is installed, or the environment lacks that browser property. | Install the stub before mount and before any eager module code reads it; use an app-level adapter where dependency timing makes global replacement brittle. |
| Assertions pass but no desktop notification is visible. | A stub records calls but does not create native UI. | Assert the call and in-page behavior in Cypress. Use a separate native integration check for operating-system display. |
7. Keep the suite fast and reliable
- Stub at the browser boundary and resolve permission promises immediately; this avoids human prompt interaction and makes branch tests repeatable.
- Assert observable outcomes and meaningful call arguments. Avoid asserting implementation details unrelated to the user-visible behavior.
- Keep each permission state isolated in its own test. Cypress restores stubs between tests, and isolated cases make failures easier to diagnose.
- Use retryable Cypress assertions for asynchronous UI changes instead of fixed sleeps.
- Reserve native prompt and OS display checks for a small, purpose-built environment check. Their behavior depends on browser, OS, stored permission state, and automation settings.
There is no need for a paid browser-notification service to test these branches: Cypress stubs are the relevant mechanism. ScreenshotNeo is a separate website screenshot API and MCP server. It can help capture page appearance for visual review, but a screenshot does not verify permission prompts or native notification delivery.
Or skip the browser setup
If your task is to capture a webpage image rather than exercise notification permission logic, ScreenshotNeo returns a screenshot or PDF with one GET request. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a passing Cypress test prove a notification appeared on a user’s desktop?
No. A stub lets you verify application calls and state. Check native display separately in the target browser and operating system.
Should I test default separately from denied?
Yes, when the branch matters to your app. The API returns both values, and applications generally handle default as denied.
Can I test a real permission prompt in a normal Cypress run?
Do not rely on it: Cypress automation disables some device permission prompts. Use controlled stubs for app logic.
Which browsers should I include?
Use the browsers your product supports. Cypress documents Chrome-family browsers and Firefox, while WebKit remains experimental; native notification checks also depend on the target runtime.


