Cypress doesn’t just run your tests; it can actively manipulate the passage of time and observe how your application reacts.
Let’s see how this looks in practice. Imagine a simple component that displays a countdown timer.
// countdown.js
function CountdownDisplay({ initialSeconds }) {
const [secondsLeft, setSecondsLeft] = React.useState(initialSeconds);
React.useEffect(() => {
if (secondsLeft <= 0) return;
const timerId = setTimeout(() => {
setSecondsLeft(secondsLeft - 1);
}, 1000); // Decrements every second
return () => clearTimeout(timerId);
}, [secondsLeft]);
return <div>Time left: {secondsLeft}</div>;
}
Now, a naive Cypress test might look like this:
// cypress/e2e/countdown.cy.js
describe('Countdown Timer', () => {
it('should display the correct time remaining', () => {
cy.visit('/countdown'); // Assuming this renders the CountdownDisplay component
cy.get('div').should('contain', 'Time left: 10'); // Initial state
// Wait for 5 seconds and check if it decremented
cy.wait(5000);
cy.get('div').should('contain', 'Time left: 5');
});
});
This test works, but it’s brittle. If your test runner is slow, or your machine is under load, cy.wait(5000) might actually take longer than 5 seconds, making your test fail even though the application logic is fine. More importantly, it’s slow. We’re making the browser wait.
This is where Cypress’s cy.clock() comes in. It gives you a powerful way to control time within your tests. When you invoke cy.clock(), Cypress replaces the browser’s native Date, setTimeout, setInterval, and requestAnimationFrame with its own sandboxed versions.
// cypress/e2e/countdown.cy.js
describe('Countdown Timer with Clock', () => {
beforeEach(() => {
cy.visit('/countdown');
cy.clock(); // Activate time control
});
it('should display the correct time remaining by fast-forwarding time', () => {
cy.get('div').should('contain', 'Time left: 10'); // Initial state
// Fast-forward time by 5 seconds
cy.tick(5000); // 5000 milliseconds
cy.get('div').should('contain', 'Time left: 5');
});
it('should also work with setInterval', () => {
// Let's imagine a component that polls every 2 seconds
// For now, we'll just test the clock itself
cy.clock(); // Ensure clock is active
let counter = 0;
const intervalId = setInterval(() => {
counter++;
console.log('Interval tick:', counter);
}, 2000); // Interval every 2 seconds
cy.tick(1000); // Advance 1 second, counter should be 0
expect(counter).to.equal(0);
cy.tick(2000); // Advance another 2 seconds (total 3 seconds elapsed)
// The interval should have fired once at 2 seconds
expect(counter).to.equal(1);
cy.tick(4000); // Advance another 4 seconds (total 7 seconds elapsed)
// The interval should have fired at 2s, 4s, 6s.
expect(counter).to.equal(3);
clearInterval(intervalId);
});
});
Notice how cy.tick(5000) instantly advances the timer by 5 seconds. The browser doesn’t actually pause; Cypress simulates the passage of time and the execution of any timers that would have fired during that period. This makes your tests significantly faster and more reliable because they are no longer dependent on actual wall-clock time.
Beyond just controlling time, Cypress allows you to spy on functions. Spies are essentially wrappers around existing functions that record how they are called. This is incredibly useful for verifying that certain actions have occurred or that functions were called with the expected arguments.
// cypress/e2e/spy_example.cy.js
describe('Spying on events', () => {
it('should spy on a button click handler', () => {
cy.visit('/my-page'); // Assume this page has a button with id="my-button"
cy.get('body').then(($body) => {
// Check if the button exists before trying to spy on its handler
if ($body.find('#my-button').length) {
// Spy on the 'click' event handler attached to the button
const clickSpy = cy.spy().as('clickSpy');
cy.get('#my-button').invoke('on', 'click', clickSpy);
// Trigger the click
cy.get('#my-button').click();
// Assert that the spy was called
cy.get('@clickSpy').should('have.been.calledOnce');
} else {
cy.log('Button #my-button not found on the page.');
}
});
});
});
Here, cy.spy().as('clickSpy') creates a spy and gives it an alias. We then attach this spy to the click event of the button using invoke('on', 'click', clickSpy). After clicking the button, we assert that our clickSpy was indeed called once. This confirms that the click event handler is functioning as expected.
Stubs are even more powerful than spies. A stub is a pre-programmed function that replaces an existing function. It can be used to return specific values, throw errors, or even call original implementations if needed. This is invaluable for isolating your tests and controlling dependencies.
Consider an API call your component makes. Instead of making a real network request, you can stub the fetch function.
// cypress/e2e/stub_example.cy.js
describe('Stubbing API responses', () => {
it('should display user data fetched from an API', () => {
// Stub the fetch call to return mock user data
cy.intercept('GET', '/api/user/123', {
statusCode: 200,
body: {
id: 123,
name: 'Cypress User',
email: 'cypress@example.com'
},
}).as('getUser'); // Alias the intercept for later assertion
cy.visit('/user-profile/123'); // Page that fetches user data
// Wait for the intercepted request to complete
cy.wait('@getUser');
// Assert that the UI displays the stubbed data
cy.contains('Cypress User');
cy.contains('cypress@example.com');
});
it('should handle API errors gracefully', () => {
// Stub the fetch call to return an error
cy.intercept('GET', '/api/user/123', {
statusCode: 500,
body: { message: 'Internal Server Error' },
}).as('getUserError');
cy.visit('/user-profile/123');
cy.wait('@getUserError');
// Assert that an error message is displayed
cy.contains('Error loading user data');
});
});
In this example, cy.intercept is Cypress’s primary tool for stubbing network requests. We intercept GET requests to /api/user/123 and provide a mock statusCode and body. This completely bypasses the actual network call, making the test faster and ensuring consistent results regardless of server availability. We then cy.wait('@getUser') to ensure our application has processed the stubbed response before making assertions.
The most surprising thing about cy.clock() is that it doesn’t just control setTimeout and setInterval. It also hijacks requestAnimationFrame, meaning you can cy.tick() through animations and visual updates that rely on the browser’s rendering loop, making tests for animations deterministic.
By mastering cy.clock(), cy.spy(), and cy.intercept() (which is how you stub network requests in modern Cypress), you gain fine-grained control over your test environment, enabling faster, more reliable, and more insightful end-to-end tests.
The next step is often to combine these techniques to test complex asynchronous workflows.