How Cypress Implements the Cypress.on() and cy.on() Event System
Cypress builds its event system on top of EventEmitter2, using a $Cy class that extends EventEmitter2 to power the cy object, while an extend helper proxies EventEmitter2 methods onto the global Cypress object to create a unified event API.
The cypress-io/cypress repository implements a sophisticated event architecture that powers both the global Cypress.on() API and the per-test cy.on() method. This system allows developers to hook into test lifecycle events, command execution, and browser interactions through a consistent interface. Understanding how Cypress implements this event system reveals why the global and instance-level APIs behave identically while maintaining separate event scopes.
Core Architecture: EventEmitter2 Foundation
Cypress does not implement its own event system from scratch. Instead, it relies on the EventEmitter2 library to provide the underlying listener management and emission capabilities.
The architecture consists of two cooperating pieces:
$Cyclass – A subclass ofEventEmitter2defined inpackages/driver/src/cypress/cy.tsthat powers thecyobject. It holds test-run state, the command queue, and registers listeners for Cypress-wide events.extendhelper – A utility inpackages/driver/src/cypress/events.tsthat copies the EventEmitter2 API onto any object, specifically the globalCypressobject.
The $Cy Class Implementation
The $Cy class serves as the concrete implementation of the cy object that test code interacts with. Because it inherits from EventEmitter2, it natively possesses methods like .on(), .once(), .off(), and .emit().
When a test run starts, the driver creates a new $Cy instance (referenced as curCy in lines 72-74 of cy.ts):
// packages/driver/src/cypress/cy.ts
const cy = new $Cy()
This instance holds the current test's state and becomes the target for event proxying from the global Cypress object.
The extend Helper and Global Cypress Object
The global Cypress object that users access in support files and plugins receives its event capabilities through the extend function in events.ts. This function creates a fresh EventEmitter2 instance and proxies its methods onto the passed-in object:
// packages/driver/src/cypress/events.ts
import { extend as extendEvents } from './events'
const Cypress = {} as any
extendEvents(Cypress) // Adds .on, .once, .emit, emitThen, etc.
The extend function also wires a proxyTo method that allows a child emitter (the $Cy instance) to forward its events to the parent while managing logging behavior.
Event Proxying and Forwarding
The connection between Cypress.on() and cy.on() happens through explicit proxying. Inside cy.ts, the driver hooks the $Cy instance into the global emitter:
// packages/driver/src/cypress/cy.ts
const cy = new $Cy()
extendEvents(Cypress).proxyTo(cy)
From this point forward, any call to Cypress.on('test:before:run', fn) registers the listener on the underlying $Cy emitter. The proxyTo method (lines 30-68 in events.ts) forwards events to the parent while temporarily silencing duplicate logs, preventing double-printing when both Cypress and cy listen to the same event.
Lifecycle Events and Emission
Throughout the driver codebase, events are emitted at critical lifecycle points:
Cypress.emit('enqueue:command')– Emitted incy.tswhen a command is queued (line 353)Cypress.emit('test:before:run')– Emitted during test lifecycle transitions incommand_queue.tsandproxy-logging.tsCypress.emit('command:enqueued')– Used to notify listeners when commands enter the queue
EventEmitter2 provides rich emission patterns including emit, emitThen, and emitThenSeries, which Cypress wraps with logging controls (logEmit flag) to conditionally output debug information.
Practical Usage Examples
Global Event Listeners
Use Cypress.on() in support files or plugins to handle events across all tests:
// cypress/support/e2e.ts
Cypress.on('uncaught:exception', (err) => {
// Return false to prevent Cypress from failing the test
return !err.message.includes('ResizeObserver loop')
})
Per-Test Listeners
Use cy.on() within individual tests for scoped event handling:
it('tracks command queue', () => {
cy.on('command:enqueued', (cmd) => {
console.log('Queued command:', cmd)
})
cy.get('button').click()
})
Custom Events
Emit and listen to custom events for plugin communication:
// Listening
Cypress.on('my:plugin:event', (payload) => {
console.log('Plugin says:', payload)
})
// Emitting (rare in test code, but possible)
Cypress.emit('my:plugin:event', { foo: 'bar' })
Key Source Files
Understanding the implementation requires examining these specific files:
packages/driver/src/cypress/cy.ts– Defines the$Cyclass and wires it to the globalCypressemitterpackages/driver/src/cypress/events.ts– Implementsextend()which mixes EventEmitter2 methods onto any objectpackages/driver/src/cypress/command_queue.ts– Shows usage ofCypress.once('command:enqueued')for command lifecycle managementpackages/driver/src/cypress/proxy-logging.ts– Emits lifecycle events such asrequest:eventandtest:before:run
Summary
- EventEmitter2 Foundation – Cypress leverages EventEmitter2 rather than building a custom event system, inheriting methods like
on,once,emit, andemitThen. - Dual API Design – The
$Cyclass provides thecy.on()API, while theextendhelper inevents.tsproxies these capabilities to the globalCypress.on()API. - Proxy Pattern – The
proxyTomethod connects global and instance-level event emitters, ensuring events flow correctly while preventing duplicate logging. - Lifecycle Integration – The driver emits standard events like
test:before:run,command:enqueued, anduncaught:exceptionthroughout the test lifecycle.
Frequently Asked Questions
What is the difference between Cypress.on() and cy.on()?
Cypress.on() registers listeners on the global event emitter, affecting all tests in the run, while cy.on() registers listeners on the specific test's $Cy instance. However, because the global Cypress object proxies events to the current $Cy instance through the proxyTo method, both APIs ultimately operate on the same underlying event bus during test execution.
Why does Cypress use EventEmitter2 instead of Node.js EventEmitter?
EventEmitter2 provides advanced features like namespaced events, wildcards, and asynchronous emission patterns (emitThen, emitThenSeries) that the standard Node.js EventEmitter lacks. These capabilities are essential for Cypress's complex event handling across test lifecycles, command queues, and browser automation.
How do I prevent duplicate event handlers when using both Cypress.on() and cy.on()?
The proxyTo implementation in events.ts (lines 30-68) automatically suppresses duplicate logging, but you should still use Cypress.once() or manually call .off() if you need to ensure a handler fires only once. For per-test cleanup, cy.on() listeners automatically scope to the test and do not persist across tests, while Cypress.on() listeners persist until explicitly removed.
Can I emit custom events in Cypress?
Yes, you can emit custom events using Cypress.emit('event:name', payload), though this is rare in standard test code. This pattern is more commonly used in plugins and internal driver code to communicate between different parts of the Cypress architecture, such as between the driver and the reporter.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →