How to Use App-Emitted Events in Cypress End-to-End Tests
Hook the app’s event interface before startup, drive a real user flow, and assert stable event payload fields without confusing app events with Cypress events.
To test events emitted by your application in a Cypress end-to-end test, attach an observer to the interface the app actually uses, exercise the UI, and assert the meaningful parts of the emitted event. If the app emits during startup, install the observer in cy.visit()‘s onBeforeLoad callback so it is ready before application code runs. Use a spy when you need to observe a method without replacing it; use a stub only when you intend to change that method’s behavior.
There is no universal Cypress listener that records every arbitrary application event. First identify whether your app uses a method such as window.postMessage, a custom DOM event, or another event interface. Cypress’s own runner and lifecycle events are a separate stream.
1. Identify the event interface
Inspect the application code or its event contract and answer these questions before writing the assertion:
- What emits the event: a method call, a DOM
CustomEvent, or a Cypress event? - Can the event fire during app startup, before the page is interactive?
- Which payload fields represent a stable contract, and which are incidental metadata such as timestamps?
- Is the purpose to verify an internal integration, or a behavior a user depends on?
Cypress distinguishes application events from Cypress events. Its event catalog includes application window lifecycle events as well as events from Cypress. Choose the observer based on the source you want to test.
2. Spy on a method before the app starts
Here is a complete spec for an application that emits messages through window.postMessage. It assumes the app is served at the configured base URL and has an input with class .new-todo; adapt the selectors and expected message shape to your application.
describe('app-emitted messages', () => {
it('emits a message when a user adds a todo', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win, 'postMessage').as('postMessage')
},
})
cy.get('.new-todo').type('learn testing{enter}')
cy.get('.todo-list li').should('have.length', 1)
cy.get('@postMessage').should('have.been.called')
})
})
The onBeforeLoad callback runs before the application’s JavaScript. Cypress’s visit documentation and its tutorial on events emitted by an application demonstrate this timing. A spy records calls while allowing the original method to run.
Assert a useful call and payload
When an app emits multiple messages, assert the one associated with the user action and check stable fields. Avoid comparing a complete event object if it contains generated IDs, timestamps, or other values that change from run to run.
cy.get('@postMessage').should('have.been.called')
cy.get('@postMessage').should((spy) => {
const calls = spy.getCalls()
const actionCall = calls.find(([message]) => message?.type === 'TODO_ADDED')
expect(actionCall, 'TODO_ADDED message').to.exist
expect(actionCall.args[0]).to.include({
type: 'TODO_ADDED',
text: 'learn testing',
})
})
The example assumes the app’s message is an object with type and text properties. If the application sends a string, parse that string according to the app’s documented format before asserting. If it sends a MessageEvent to a listener, inspect the relevant event.data instead.
3. Observe a custom DOM event
For an app that dispatches a DOM CustomEvent, attach a native listener to the AUT window before startup and save the data for a later Cypress assertion. Cypress commands should not run inside the native callback.
describe('custom application events', () => {
it('emits cart:item-added with the selected product', () => {
cy.visit('/shop', {
onBeforeLoad(win) {
win.__appEvents = []
win.addEventListener('cart:item-added', (event) => {
win.__appEvents.push(event.detail)
})
},
})
cy.get('[data-testid="add-to-cart"]').click()
cy.get('[data-testid="cart-count"]').should('have.text', '1')
cy.window().its('__appEvents').should((events) => {
expect(events).to.have.length(1)
expect(events[0]).to.include({ productId: 'sku-123' })
})
})
})
This example assumes the application dispatches an event like new CustomEvent('cart:item-added', { detail: { productId: 'sku-123' } }). The property assignment is plain JavaScript for clarity; in a TypeScript project, define a window type augmentation for __appEvents or use a test-owned store with an appropriate type.
4. Choose the right Cypress mechanism
| Need | Use | Tradeoff |
|---|---|---|
| Observe a method while preserving its real behavior | cy.spy(object, method) |
Install before the first call if startup emissions matter. |
| Replace a method or control its result | cy.stub(object, method) |
Changes application behavior; it cannot prove the original method performed its work. |
| Inspect the current AUT window after navigation | cy.window() |
May miss events emitted before the command runs. |
| Invoke an event handler with chosen event data | .trigger(eventName, options) |
Dispatches an event but does not perform the browser’s default action. |
| Listen to Cypress or test lifecycle events | cy.on() or Cypress.on() |
These are Cypress events, not a recorder for arbitrary app events. |
See Cypress’s guides to spies and stubs, the AUT window, and triggering events. A spy observes; a stub intervenes. Use the latter when the test needs to simulate a failure or control a callback, not when confirming the real operation.
5. Keep callbacks and listeners within Cypress’s rules
cy.on() listeners are scoped to the current test. Cypress.on() listeners persist for the life of the spec window; registering one repeatedly can add duplicate listeners. Cypress event callbacks run outside the normal command queue, so do not call cy.get() or make Cypress assertions directly inside those callbacks. Capture the needed data in the callback and assert later in the test body.
If you use a Cypress event listener for diagnostics, keep its callback small. For an app-owned DOM event, a native listener can be appropriate, as in the example above; it follows the same principle of collecting data first and asserting in the Cypress chain.
6. Exercise the real user flow when behavior matters
Prefer Cypress actions such as .click() and .type() when the test needs to verify a user journey. Use .trigger() when the specific goal is to invoke a handler with controlled event data. Synthetic dispatch does not reproduce browser behavior such as the default action associated with some events.
Pair internal event assertions with visible outcomes when the user-facing result matters. For example, verify both that an item appears in the cart and that the app emitted its documented cart event. This keeps the test useful if internal implementation details change.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The spy has no calls, but the app visibly works | The app emits through a different interface, the wrong window was spied on, or the event fired before the spy was installed. | Find the actual emission site. For startup calls, install the spy in onBeforeLoad; confirm the app calls that exact method on the AUT window. |
| The assertion sees registration messages but not the expected action | The app emitted a different message shape or the action did not occur. | Inspect recorded calls, perform the UI action, and match a stable discriminator such as a message type before checking its fields. |
| The spy assertion passes but the operation did not happen | A stub replaced the method, or the assertion only checked that a call was attempted. | Use a spy to preserve the original method and pair the event assertion with an outcome assertion. |
| A custom DOM event is missed | The listener was attached after dispatch, the event name differs, or the app dispatches on another target. | Attach before startup and listen on the actual target, such as window or a specific element. Verify the event name and whether payload data is in detail. |
| Commands fail inside a listener callback | The callback is outside Cypress’s command queue. | Store the data in a variable or AUT-window property, then assert later using Cypress commands. |
| The test passes locally but fails intermittently | The assertion runs before an asynchronous event arrives, or it matches unstable payload metadata. | Use a retryable Cypress assertion on the captured data and assert stable fields rather than exact timestamps or generated identifiers. |
| A triggered handler runs but the browser interaction seems incomplete | .trigger() dispatched the event without its browser default action. |
Use normal Cypress interaction for a user flow; keep .trigger() for handler-specific tests. |
8. Performance, reliability, and maintenance
- Keep observation narrow. Capture only the event types and fields relevant to the test. Recording every event can make failures harder to diagnose and assertions more coupled to implementation.
- Prefer retryable assertions. Cypress retries assertions chained from commands such as
cy.window(). Avoid arbitrary sleeps when a condition can express readiness. - Clean up deliberately. Per-test window listeners disappear with that page context. If you attach listeners to a longer-lived object or register
Cypress.on(), avoid repeated registration or remove listeners where appropriate. - Balance contract coverage and E2E scope. A small number of E2E tests can verify that important user actions emit required events. Test detailed event serialization and edge cases closer to the emitting code when that does not require a full browser journey.
- Control external variability. If the event depends on network responses, use deterministic test data and Cypress network controls where suitable; do not make the event assertion depend on an unstable third-party service.
Or skip the browser setup
If the task is capturing a page image rather than testing your application’s emitted events, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF. See the ScreenshotNeo documentation for request options.
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}`);
- Cookie banners are accepted and removed before the shot; newsletter popups and chat widgets are removed too, and each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
- An MCP server lets AI agents use screenshot, page-info, and PDF-capture tools.
- 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does Cypress automatically capture every custom event?
No. Attach an observer to the interface and target used by your app.
Should an E2E test assert every emitted event?
Usually not. Assert events that represent a meaningful contract, and check visible behavior when the user outcome matters.
Can I use cy.window() for startup events?
It gives access to the active AUT window after visiting. Install the observer in onBeforeLoad if an emission can happen before then.
Is window.postMessage required?
No. It is one example interface. Use the method, DOM event, or other mechanism your application actually exposes.


