Cypress provides exceptional debugging capabilities that set it apart from other testing frameworks. Understanding these tools will help you quickly identify and fix failing tests.
Time Travel Debugging
flowchart LR
subgraph Timeline["Command Timeline"]
A[Step 1] --> B[Step 2]
B --> C[Step 3]
C --> D[Step 4]
end
E[Click Step] --> F[See DOM State]
F --> G[Inspect Elements]
style Timeline fill:#3b82f6,color:#fff
style G fill:#10b981,color:#fff
How Time Travel Works
When you run tests in the Cypress Test Runner, each command creates a snapshot of the DOM at that moment. You can hover over or click any command to see exactly what the page looked like at that step.
// Each of these commands creates a snapshot
cy.visit('/login'); // Snapshot 1
cy.get('[data-testid="email"]') // Snapshot 2
.type('user@example.com'); // Snapshot 3
cy.get('[data-testid="submit"]') // Snapshot 4
.click(); // Snapshot 5
cy.url() // Snapshot 6
.should('include', '/dashboard'); // Snapshot 7
Before/After States
For commands that modify the DOM, Cypress shows both "before" and "after" states:
- Hover over command β See "after" state
- Pin command β Toggle between "before" and "after"
The Command Log
Reading the Command Log
// The command log shows:
// - Command name (GET, CLICK, TYPE)
// - Selector or value used
// - Yielded subject
// - Assertions made
// - Time taken
cy.get('[data-testid="users"]') // GET [data-testid="users"]
.find('.user') // FIND .user
.should('have.length', 5) // ASSERT expected 5 got 5
.first() // FIRST
.click(); // CLICK
Command Log States
| State | Color | Meaning |
|---|---|---|
| Pending | Gray | Command is queued |
| Running | Blue | Command is executing |
| Passed | Green | Command succeeded |
| Failed | Red | Command or assertion failed |
Using cy.pause()
Pause test execution to inspect the current state:
it('debugs step by step', () => {
cy.visit('/dashboard');
cy.pause(); // Test pauses here - inspect the DOM
cy.get('[data-testid="sidebar"]').click();
cy.pause(); // Pause again to check sidebar state
cy.get('[data-testid="menu-item"]').should('be.visible');
});
When paused, you can:
- Inspect elements in the DOM
- Check network requests
- Use browser DevTools
- Step forward one command at a time
Using cy.debug()
Insert a debugger statement that triggers browser DevTools:
it('uses browser debugger', () => {
cy.visit('/users');
cy.get('[data-testid="user-list"]')
.debug() // Opens DevTools debugger
.find('.user')
.should('have.length.gt', 0);
});
The .debug() command:
- Logs the current subject to console
- Triggers the browser's debugger
- Allows inspection of the yielded value
Console Logging
Using cy.log()
Add custom messages to the command log:
it('logs progress', () => {
cy.log('Starting user creation test');
cy.visit('/users/new');
cy.log('Filling out form');
cy.get('[data-testid="name"]').type('John');
cy.get('[data-testid="email"]').type('john@example.com');
cy.log('Submitting form');
cy.get('[data-testid="submit"]').click();
cy.log('Verifying user was created');
cy.url().should('include', '/users/');
});
Using console.log with .then()
cy.get('[data-testid="user-id"]')
.invoke('text')
.then((text) => {
console.log('User ID:', text);
// Continue with test
});
// Or log the subject directly
cy.get('.item')
.then(($el) => {
console.log('Element:', $el);
console.log('Text:', $el.text());
console.log('Classes:', $el.attr('class'));
});
Inspecting Network Requests
Viewing in DevTools
it('inspects API calls', () => {
// Open DevTools Network tab before running
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers').then((interception) => {
console.log('Request:', interception.request);
console.log('Response:', interception.response);
});
});
Debugging Intercepts
cy.intercept('POST', '/api/users', (req) => {
console.log('Request body:', req.body);
console.log('Request headers:', req.headers);
req.continue((res) => {
console.log('Response status:', res.statusCode);
console.log('Response body:', res.body);
});
}).as('createUser');
Common Debugging Scenarios
Element Not Found
// Problem: cy.get() fails to find element
cy.get('[data-testid="submit"]'); // Error: Timed out
// Debug steps:
// 1. Check if element exists
cy.get('body').then(($body) => {
console.log('Submit button exists:', $body.find('[data-testid="submit"]').length > 0);
});
// 2. Check for typos in selector
cy.get('[data-testid]').then(($els) => {
$els.each((i, el) => {
console.log('Found testid:', el.getAttribute('data-testid'));
});
});
// 3. Wait for async content
cy.get('[data-testid="submit"]', { timeout: 10000 });
Assertion Failures
// Problem: should() assertion fails
cy.get('.count').should('have.text', '5'); // Error: expected "3" to equal "5"
// Debug: Log the actual value
cy.get('.count')
.invoke('text')
.then((text) => {
console.log('Actual text:', text);
console.log('Text length:', text.length);
console.log('Trimmed:', text.trim());
});
Timing Issues
// Problem: Element appears briefly then disappears
cy.get('[data-testid="toast"]').should('be.visible'); // Fails
// Debug: Add longer timeout and log state
cy.get('[data-testid="toast"]', { timeout: 10000 })
.should(($el) => {
console.log('Toast visible:', $el.is(':visible'));
console.log('Toast text:', $el.text());
});
// Or use cy.pause() to catch the moment
cy.pause();
cy.get('[data-testid="toast"]').should('be.visible');
Screenshots and Videos
Automatic Screenshots on Failure
// cypress.config.js
module.exports = defineConfig({
e2e: {
screenshotOnRunFailure: true,
screenshotsFolder: 'cypress/screenshots',
},
});
Manual Screenshots
it('takes screenshots for debugging', () => {
cy.visit('/dashboard');
cy.screenshot('dashboard-loaded');
cy.get('[data-testid="sidebar"]').click();
cy.screenshot('sidebar-opened');
// Screenshots saved to cypress/screenshots/
});
Video Recording
// cypress.config.js
module.exports = defineConfig({
e2e: {
video: true,
videoCompression: 32,
videosFolder: 'cypress/videos',
},
});
Debugging Test Isolation
Identifying State Leakage
describe('Tests with shared state issues', () => {
it('first test modifies state', () => {
cy.visit('/');
localStorage.setItem('user', 'admin');
// Test passes
});
it('second test expects clean state', () => {
cy.visit('/');
// Fails because localStorage has 'user' from previous test
cy.get('[data-testid="login-button"]').should('exist');
});
});
// Fix: Reset state in beforeEach
describe('Tests with proper isolation', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.clearCookies();
});
it('first test', () => {
// ...
});
it('second test has clean state', () => {
// ...
});
});
Debugging in CI
Adding Debug Information
// cypress/support/e2e.js
Cypress.on('fail', (error, runnable) => {
console.log('Test failed:', runnable.title);
console.log('Error:', error.message);
console.log('Stack:', error.stack);
// Log additional context
cy.url().then((url) => console.log('URL at failure:', url));
throw error; // Re-throw to fail the test
});
Environment-Specific Debugging
// Add more logging in CI
if (Cypress.env('CI')) {
Cypress.on('command:start', ({ name, args }) => {
console.log(`Command: ${name}`, args);
});
}
Best Practices
1. Use Descriptive Logs
it('creates a new order', () => {
cy.log('**Step 1: Navigate to products**');
cy.visit('/products');
cy.log('**Step 2: Add item to cart**');
cy.get('[data-testid="product-1"]').click();
cy.get('[data-testid="add-to-cart"]').click();
cy.log('**Step 3: Checkout**');
cy.get('[data-testid="checkout"]').click();
cy.log('**Step 4: Verify order created**');
cy.url().should('include', '/orders/');
});
2. Debug One Thing at a Time
// Instead of one long test
it('complex flow', () => {
// 50 lines of code
});
// Break into smaller, focused tests
it('adds item to cart', () => { /* ... */ });
it('proceeds to checkout', () => { /* ... */ });
it('completes payment', () => { /* ... */ });
3. Use Conditional Debugging
// Only debug in interactive mode
if (Cypress.config('isInteractive')) {
cy.pause();
}
// Only log in development
if (Cypress.env('DEBUG')) {
cy.log('Debug info:', someValue);
}
Summary
| Tool | Purpose |
|---|---|
| Time Travel | View DOM at each step |
| Command Log | See command execution flow |
cy.pause() |
Stop test for manual inspection |
cy.debug() |
Open browser debugger |
cy.log() |
Add custom messages to log |
.then() + console.log |
Log values to console |
| Screenshots | Capture visual state |
| Videos | Record full test run |
Key takeaways:
- Use Time Travel to see the DOM state at each command
- Click commands in the Command Log to pin and inspect
- Use
cy.pause()to stop and manually inspect the page - Use
cy.debug()to open browser DevTools at a specific point - Log values with
.then()andconsole.log()for debugging - Take screenshots at key points for visual debugging
- Enable video recording for CI debugging
- Isolate tests properly to avoid state leakage issues
Cypress's debugging tools make it much easier to understand why tests fail and fix issues quickly.
References
- Cypress Debugging Guide
- Cypress Test Runner
- Mwaura, Waweru. End-to-End Web Testing with Cypress. Packt, 2021.