Learn Cypress in 10 DaysDay 9: Debugging and Test Strategy
books.chapter 9Learn Cypress in 10 Days

Day 9: Debugging and Test Strategy

What You Will Learn Today

  • Using cy.debug() and cy.pause()
  • Debugging with DevTools Console
  • Time Travel Debugging (Test Runner)
  • Screenshots and Video Recording
  • Test Retry Configuration
  • Test Isolation Principles
  • Proper Use of beforeEach / afterEach
  • The Page Object Pattern
  • Test Naming Conventions and File Organization

Debugging Fundamentals

When a test fails, quickly identifying the root cause is critical. Cypress comes with powerful built-in debugging tools to help you do just that.

flowchart TB
    subgraph Tools["Cypress Debugging Tools"]
        A["cy.debug()"]
        B["cy.pause()"]
        C["Time Travel"]
        D["Screenshots"]
        E["Video Recording"]
    end

    subgraph Flow["Debugging Flow"]
        F["Test Failure"] --> G["Check Error Message"]
        G --> H["Inspect State via Time Travel"]
        H --> I["Pause with cy.pause()"]
        I --> J["Investigate in DevTools"]
    end

    style Tools fill:#3b82f6,color:#fff
    style Flow fill:#8b5cf6,color:#fff

cy.debug() and cy.pause()

cy.debug()

cy.debug() launches the browser's DevTools debugger during test execution. You can inspect the result of the previous command as subject.

cy.get('.user-name')
  .debug()  // Pauses in the DevTools debugger
  .should('contain', 'John');

In the DevTools Console, type subject to inspect the element returned by cy.get().

cy.pause()

cy.pause() pauses test execution and allows you to step through commands manually.

cy.visit('/login');
cy.pause();  // Pauses here

cy.get('#username').type('testuser');
cy.pause();  // Check state after input

cy.get('#password').type('password123');
cy.get('#login-btn').click();

Use the "Resume" or "Next" button in the Test Runner to continue execution.

When to Use Each

Method Purpose Pauses In
cy.debug() Detailed inspection of elements and data DevTools Debugger
cy.pause() Step-by-step test execution Test Runner UI

Debugging with DevTools Console

Since the Cypress Test Runner runs in a Chromium-based browser, you have full access to DevTools.

Using Console Logs

cy.get('.item-list')
  .then(($el) => {
    // Log the jQuery element to the console
    console.log('Element:', $el);
    console.log('Text:', $el.text());
    console.log('Length:', $el.length);
  });

Test Logging with cy.log()

cy.log('--- Starting login test ---');
cy.get('#username').type('testuser');
cy.log('Entered username');

cy.get('#password').type('password123');
cy.log('Entered password');

cy.get('#login-btn').click();
cy.log('--- Clicked login button ---');

cy.log() output appears in the Test Runner's command log, making it easy to visually trace test execution.

Accessing Cypress Objects

You can access Cypress objects directly from the DevTools Console.

// Run these in the DevTools Console
Cypress.env()              // View environment variables
Cypress.config()           // View configuration
Cypress.spec               // Current spec file info

Time Travel Debugging

One of Cypress's most powerful debugging features is Time Travel.

flowchart LR
    subgraph Timeline["Command Log (Timeline)"]
        C1["visit('/')"] --> C2["get('.btn')"] --> C3["click()"] --> C4["url()"] --> C5["should('include')"]
    end

    C3 -->|"Click to view<br/>DOM snapshot"| S["DOM Snapshot"]

    style Timeline fill:#22c55e,color:#fff
    style S fill:#f59e0b,color:#000

How to Use It

  1. Run a test
  2. Click any command in the command log on the left side of the Test Runner
  3. The DOM state at the time that command executed is displayed in the preview
  4. Toggle Before/After to see changes before and after the command

Pinning

Clicking a command "pins" it, freezing the DOM at that point in time. You can then use the DevTools Elements panel to inspect the DOM structure in detail.

// You can inspect the DOM at each command's execution point
cy.visit('/dashboard');           // Step 1: Navigate to page
cy.get('.sidebar').click();       // Step 2: Click sidebar
cy.get('.menu-item').first().click(); // Step 3: Select menu item
cy.get('.content').should('be.visible'); // Step 4: Verify content is visible

Screenshots and Video Recording

Screenshots

// Take a screenshot at any point
cy.screenshot('login-page');

// Capture only a specific element
cy.get('.error-message').screenshot('error-state');

// Screenshots are automatically saved on test failure

Screenshots are saved to cypress/screenshots/ by default.

Configuration

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    screenshotsFolder: 'cypress/screenshots',
    screenshotOnRunFailure: true,  // Auto-capture on failure
  },
});

Video Recording

When running with cypress run (headless mode), test videos are recorded automatically.

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    video: true,                    // Enable video recording
    videosFolder: 'cypress/videos', // Output directory
    videoCompression: 32,           // Compression level (0-51)
  },
});
# Run in headless mode (video will be recorded)
npx cypress run

Screenshots vs. Video

Feature Screenshots Video
Use case Inspect state at a specific point Review the entire test flow
Auto-save Automatically on failure Automatically during run
File size Small Large
CI/CD usage Identifying failure causes Reviewing the full flow

Test Retries

Tests can become flaky due to network latency, animations, and other factors. The retry feature helps improve stability.

flowchart TB
    subgraph Retry["Retry Flow"]
        T["Run Test"] --> R{"Pass?"}
        R -->|"Yes"| P["Pass"]
        R -->|"No"| C{"Retry limit<br/>exceeded?"}
        C -->|"No"| T
        C -->|"Yes"| F["Fail"]
    end

    style P fill:#22c55e,color:#fff
    style F fill:#ef4444,color:#fff
    style Retry fill:#8b5cf6,color:#fff

Global Configuration

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  retries: {
    runMode: 2,    // Retries during cypress run
    openMode: 0,   // Retries during cypress open
  },
});

Per-Test Configuration

// Set retries for an entire describe block
describe('Flaky API integration tests', { retries: 3 }, () => {
  it('fetches and displays data', () => {
    cy.visit('/dashboard');
    cy.get('.data-table').should('be.visible');
  });
});

// Set retries for a specific test
it('shows a notification', { retries: { runMode: 3, openMode: 1 } }, () => {
  cy.get('.notification').should('be.visible');
});

Retry Pitfalls

// BAD: State is not reset on retry
it('counter test', () => {
  cy.get('#increment').click();  // Clicked again on retry
  cy.get('#count').should('have.text', '1');
});

// GOOD: Reset state with beforeEach
beforeEach(() => {
  cy.visit('/counter');  // Reload the page each time
});

it('counter test', () => {
  cy.get('#increment').click();
  cy.get('#count').should('have.text', '1');
});

Test Isolation

Each test should be independent and able to run on its own without relying on other tests.

flowchart TB
    subgraph Bad["Bad: Tests depend on each other"]
        B1["Test 1: Create user"] --> B2["Test 2: Log in as user"] --> B3["Test 3: Edit profile"]
    end

    subgraph Good["Good: Independent tests"]
        G1["Test 1: Create user"]
        G2["Test 2: Log in<br/>(create user via API)"]
        G3["Test 3: Edit profile<br/>(set up logged-in state via API)"]
    end

    style Bad fill:#ef4444,color:#fff
    style Good fill:#22c55e,color:#fff

Bad Example

// BAD: Tests depend on each other
describe('User management', () => {
  it('creates a user', () => {
    cy.visit('/register');
    cy.get('#name').type('John');
    cy.get('#email').type('john@example.com');
    cy.get('#submit').click();
  });

  // This test fails if Test 1 didn't run first!
  it('logs in as the created user', () => {
    cy.visit('/login');
    cy.get('#email').type('john@example.com');
    cy.get('#password').type('password');
    cy.get('#login-btn').click();
  });
});

Good Example

// GOOD: Each test is independent
describe('User management', () => {
  it('creates a user', () => {
    cy.visit('/register');
    cy.get('#name').type('John');
    cy.get('#email').type('john@example.com');
    cy.get('#submit').click();
    cy.url().should('include', '/dashboard');
  });

  it('logs in', () => {
    // Create user via API (no UI dependency)
    cy.request('POST', '/api/users', {
      name: 'John',
      email: 'john@example.com',
      password: 'password',
    });

    cy.visit('/login');
    cy.get('#email').type('john@example.com');
    cy.get('#password').type('password');
    cy.get('#login-btn').click();
    cy.url().should('include', '/dashboard');
  });
});

Proper Use of beforeEach / afterEach

beforeEach

Runs common setup logic before each test.

describe('Dashboard', () => {
  beforeEach(() => {
    // Set up logged-in state before each test
    cy.request('POST', '/api/login', {
      email: 'test@example.com',
      password: 'password',
    }).then((response) => {
      window.localStorage.setItem('token', response.body.token);
    });

    cy.visit('/dashboard');
  });

  it('displays a welcome message', () => {
    cy.get('.welcome').should('contain', 'Welcome');
  });

  it('displays the sidebar', () => {
    cy.get('.sidebar').should('be.visible');
  });

  it('displays statistics', () => {
    cy.get('.stats').should('be.visible');
  });
});

afterEach

Runs cleanup logic after each test.

describe('Data operations', () => {
  afterEach(() => {
    // Clean up data after each test
    cy.request('DELETE', '/api/test-data/cleanup');
  });

  it('adds an item', () => {
    cy.get('#add-btn').click();
    cy.get('.item-list').should('have.length', 1);
  });
});

Difference Between before/after and beforeEach/afterEach

Hook When It Runs Use Case
before Once per describe (at the start) Heavy operations like DB initialization
beforeEach Before every it block Page navigation, login
afterEach After every it block Data cleanup
after Once per describe (at the end) Final cleanup

The Page Object Pattern

The Page Object pattern encapsulates page-specific operations into classes or objects. It significantly improves test readability and maintainability.

flowchart TB
    subgraph Without["Without Page Object"]
        T1["Test A: cy.get('#email')..."]
        T2["Test B: cy.get('#email')..."]
        T3["Test C: cy.get('#email')..."]
    end

    subgraph With["With Page Object"]
        PO["LoginPage<br/>- enter email<br/>- enter password<br/>- submit login"]
        TA["Test A: loginPage.login()"]
        TB["Test B: loginPage.login()"]
        TC["Test C: loginPage.login()"]
        PO --> TA
        PO --> TB
        PO --> TC
    end

    style Without fill:#ef4444,color:#fff
    style With fill:#22c55e,color:#fff
    style PO fill:#3b82f6,color:#fff

Creating a Page Object

// cypress/pages/LoginPage.js
class LoginPage {
  // Selectors
  get emailInput() {
    return cy.get('#email');
  }

  get passwordInput() {
    return cy.get('#password');
  }

  get loginButton() {
    return cy.get('#login-btn');
  }

  get errorMessage() {
    return cy.get('.error-message');
  }

  // Actions
  visit() {
    cy.visit('/login');
    return this;
  }

  typeEmail(email) {
    this.emailInput.clear().type(email);
    return this;
  }

  typePassword(password) {
    this.passwordInput.clear().type(password);
    return this;
  }

  submit() {
    this.loginButton.click();
    return this;
  }

  login(email, password) {
    this.typeEmail(email);
    this.typePassword(password);
    this.submit();
    return this;
  }
}

export default new LoginPage();

Using It in Tests

// cypress/e2e/login.cy.js
import loginPage from '../pages/LoginPage';

describe('Login page', () => {
  beforeEach(() => {
    loginPage.visit();
  });

  it('logs in with valid credentials', () => {
    loginPage.login('test@example.com', 'password123');
    cy.url().should('include', '/dashboard');
  });

  it('shows an error with incorrect password', () => {
    loginPage.login('test@example.com', 'wrong');
    loginPage.errorMessage.should('contain', 'Incorrect password');
  });

  it('shows an error when email is empty', () => {
    loginPage.typePassword('password123').submit();
    loginPage.errorMessage.should('contain', 'Please enter your email');
  });
});

Benefits of the Page Object Pattern

Benefit Description
Maintainability When the UI changes, only the Page Object needs updating
Readability Tests clearly express their intent
Reusability The same operations can be shared across multiple tests
DRY Principle Eliminates duplicated selectors

Test Naming Conventions and File Organization

Recommended Directory Structure

cypress/
β”œβ”€β”€ e2e/                    # Test files
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ login.cy.js
β”‚   β”‚   β”œβ”€β”€ logout.cy.js
β”‚   β”‚   └── register.cy.js
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ overview.cy.js
β”‚   β”‚   └── settings.cy.js
β”‚   └── products/
β”‚       β”œβ”€β”€ list.cy.js
β”‚       β”œβ”€β”€ detail.cy.js
β”‚       └── cart.cy.js
β”œβ”€β”€ fixtures/               # Test data
β”‚   β”œβ”€β”€ users.json
β”‚   └── products.json
β”œβ”€β”€ pages/                  # Page Objects
β”‚   β”œβ”€β”€ LoginPage.js
β”‚   β”œβ”€β”€ DashboardPage.js
β”‚   └── ProductPage.js
β”œβ”€β”€ support/                # Helpers and custom commands
β”‚   β”œβ”€β”€ commands.js
β”‚   └── e2e.js
└── downloads/              # Downloaded files

Naming Conventions

// describe: by feature or page
describe('Login page', () => {

  // context: by condition or scenario
  context('with valid credentials', () => {

    // it: expected behavior
    it('redirects to the dashboard', () => {
      // ...
    });

    it('displays a welcome message', () => {
      // ...
    });
  });

  context('with invalid credentials', () => {
    it('displays an error message', () => {
      // ...
    });

    it('stays on the login page', () => {
      // ...
    });
  });
});

File Naming Rules

Pattern Example Use Case
feature.cy.js login.cy.js Tests for a single feature
feature-action.cy.js product-search.cy.js Tests for a specific action
page.cy.js dashboard.cy.js Tests scoped to a page

Summary

Concept Description
cy.debug() Pauses in the DevTools debugger for inspection
cy.pause() Pauses test execution for step-by-step control
Time Travel View past DOM states from the command log
Screenshots Save the screen as an image at a specific point
Video Recording Automatically record the entire test run
Retries Automatically re-run flaky tests
Test Isolation Design each test to be independent of others
Page Object A pattern for encapsulating page operations
Naming Conventions Hierarchical structure with describe/context/it

Key Takeaways

  1. Time Travel is one of Cypress's greatest strengths
  2. Know when to use cy.pause() vs. cy.debug()
  3. Always keep test isolation in mind
  4. Use beforeEach for shared setup
  5. Improve maintainability with the Page Object pattern

Exercises

Basics

  1. Insert cy.pause() in the middle of a test and try stepping through it in the Test Runner.
  2. Use cy.screenshot('my-screenshot') to capture a screenshot at any point during a test.
  3. Set the retry count to runMode: 2 in cypress.config.js.

Intermediate

  1. Create a Page Object for a login page and use it in your tests.
  2. Refactor your tests to use beforeEach for setting up initial state before each test.
  3. Organize your tests using the hierarchical describe / context / it naming convention.

Challenge

  1. Design an E2E test spanning multiple pages (registration, login, profile editing) where each test runs independently. Use API calls to set up test preconditions.

References


Next up: In Day 10, you will learn about CI/CD and Best Practices. We will cover running tests automatically with GitHub Actions, parallel test execution, performance optimization, and other practical approaches to test operations!