Skip to content

Fix: Cypress Not Working — Tests Timing Out, Elements Not Found, or cy.intercept Not Matching

FixDevs ·

Quick Answer

How to fix Cypress issues — element selection strategies, async command chaining, cy.intercept for network stubbing, component testing, authentication handling, and flaky test debugging.

The Problem

A Cypress test fails because an element can’t be found:

cy.get('#submit-btn').click();
// CypressError: Timed out retrying after 4000ms:
// Expected to find element: '#submit-btn', but never found it.

Or cy.intercept() doesn’t match the request:

cy.intercept('GET', '/api/users', { fixture: 'users.json' });
cy.visit('/dashboard');
// Real API is still called — intercept didn't match

Or a test passes locally but fails in CI:

CypressError: Timed out retrying after 10000ms:
cy.its('status') timed out waiting for the specified property to exist.

Or the test runner launches but the app never loads:

Cypress could not verify that this server is running: http://localhost:3000

Why This Happens

Cypress runs inside the browser alongside your application. Its command model and timing behavior are different from other testing tools:

  • Cypress commands are asynchronous but not Promisescy.get() doesn’t return the element immediately. It enqueues a command that retries until the element is found or a timeout is reached. Mixing cy commands with synchronous JavaScript logic (like storing a value in a variable) breaks the command chain.
  • cy.intercept matches by URL pattern at registration time — intercepts must be registered before the request fires. If the app makes the request before cy.intercept() runs (e.g., during cy.visit()), the intercept misses it. URL patterns are also exact by default — /api/users won’t match /api/users?page=1.
  • CI environments are slower — animations take longer, API responses are delayed, and the app renders slower. Hard-coded waits (cy.wait(1000)) that work locally fail in CI. Cypress’s built-in retry mechanism is the correct solution.
  • The dev server must be running before Cypress — Cypress opens a browser that navigates to your app’s URL. If the server isn’t running, the initial cy.visit() fails. Use cypress.config.ts to configure a devServer or start the server separately.

Fix 1: Select Elements Reliably

Stop using fragile selectors that break when CSS or structure changes:

// FRAGILE — breaks when class names change
cy.get('.btn-primary-large').click();

// FRAGILE — breaks when DOM structure changes
cy.get('div > div > button:nth-child(3)').click();

// ROBUST — use data-testid attributes
cy.get('[data-testid="submit-button"]').click();

// ROBUST — use data-cy (Cypress convention)
cy.get('[data-cy="submit-button"]').click();

// ROBUST — use accessible roles and text
cy.contains('button', 'Submit').click();
cy.get('button').contains('Submit').click();

// Find within a specific container
cy.get('[data-cy="login-form"]').within(() => {
  cy.get('input[name="email"]').type('[email protected]');
  cy.get('input[name="password"]').type('secret123');
  cy.get('button[type="submit"]').click();
});

// Wait for element state — don't use cy.wait()
cy.get('[data-cy="submit-button"]').should('be.visible').click();
cy.get('[data-cy="submit-button"]').should('not.be.disabled').click();
cy.get('[data-cy="loading"]').should('not.exist');  // Wait for loading to finish

Add data-cy attributes to your components:

// In your React/Vue/Svelte component
<button data-cy="submit-button" onClick={handleSubmit}>
  Submit
</button>

Fix 2: Handle Async Operations and Timing

Cypress retries automatically — use assertions instead of waits:

// WRONG — arbitrary wait
cy.get('[data-cy="submit"]').click();
cy.wait(3000);  // Hoping the API responds in 3 seconds
cy.get('[data-cy="success-message"]').should('exist');

// CORRECT — Cypress retries until assertion passes (up to timeout)
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="success-message"]', { timeout: 10000 })
  .should('be.visible');

// Wait for a specific network request
cy.intercept('POST', '/api/users').as('createUser');
cy.get('[data-cy="submit"]').click();
cy.wait('@createUser').its('response.statusCode').should('eq', 201);

// Chain commands correctly — cy commands are NOT promises
// WRONG
const text = cy.get('[data-cy="title"]').text();  // text is undefined

// CORRECT — use .then() or .invoke()
cy.get('[data-cy="title"]').invoke('text').then((text) => {
  expect(text).to.include('Dashboard');
});

// CORRECT — use .should() for assertions
cy.get('[data-cy="title"]').should('have.text', 'Dashboard');
cy.get('[data-cy="items"]').should('have.length', 5);

// Wait for multiple conditions
cy.get('[data-cy="table"]')
  .should('be.visible')
  .find('tr')
  .should('have.length.greaterThan', 0);

Fix 3: Network Stubbing with cy.intercept

Register intercepts before the action that triggers the request:

// WRONG — intercept registered after visit (request already fired)
cy.visit('/dashboard');
cy.intercept('GET', '/api/users', { fixture: 'users.json' });

// CORRECT — intercept registered before visit
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/dashboard');
cy.wait('@getUsers');

// Match with query parameters — use glob pattern or regex
cy.intercept('GET', '/api/users?*').as('getUsers');         // Glob
cy.intercept('GET', /\/api\/users\?page=\d+/).as('getUsers');  // Regex

// Match any method
cy.intercept('/api/users').as('usersRequest');

// Return custom response
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ],
}).as('getUsers');

// Simulate errors
cy.intercept('GET', '/api/users', {
  statusCode: 500,
  body: { error: 'Internal Server Error' },
}).as('getUsers');

// Modify the real response (spy + modify)
cy.intercept('GET', '/api/users', (req) => {
  req.continue((res) => {
    // Modify the actual response
    res.body.push({ id: 99, name: 'Injected User' });
    res.send();
  });
}).as('getUsers');

// Delay response
cy.intercept('GET', '/api/users', (req) => {
  req.reply({
    delay: 2000,  // 2 second delay
    body: [],
  });
}).as('getUsers');

// Assert on the request body
cy.intercept('POST', '/api/users').as('createUser');
cy.get('[data-cy="submit"]').click();
cy.wait('@createUser').then((interception) => {
  expect(interception.request.body).to.deep.include({
    name: 'Alice',
    email: '[email protected]',
  });
  expect(interception.response.statusCode).to.eq(201);
});

Fix 4: Authentication in Tests

Avoid logging in through the UI for every test:

// cypress/support/commands.ts — custom login command
Cypress.Commands.add('login', (email: string, password: string) => {
  // API-based login — much faster than UI login
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
  }).then((response) => {
    // Store token in localStorage or cookie
    window.localStorage.setItem('authToken', response.body.token);
  });
});

// Type definition for custom commands
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
    }
  }
}

// Usage in tests
describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('[email protected]', 'password123');
    cy.visit('/dashboard');
  });

  it('shows user data', () => {
    cy.get('[data-cy="welcome"]').should('contain', 'admin');
  });
});

// Session caching — reuse login across tests (Cypress 12+)
Cypress.Commands.add('login', (email: string, password: string) => {
  cy.session([email, password], () => {
    cy.request('POST', '/api/auth/login', { email, password })
      .then((res) => {
        window.localStorage.setItem('authToken', res.body.token);
      });
  });
});

// OAuth login — use cy.origin for third-party domains
it('logs in with Google', () => {
  // Better: bypass OAuth in tests by seeding the session directly
  cy.setCookie('session', 'test-session-token');
  cy.visit('/dashboard');
});

Fix 5: Configuration and Project Setup

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    // Increase default timeout for slow CI
    defaultCommandTimeout: 8000,
    requestTimeout: 10000,
    responseTimeout: 30000,
    // Viewport
    viewportWidth: 1280,
    viewportHeight: 720,
    // Retry failed tests
    retries: {
      runMode: 2,    // CI: retry twice
      openMode: 0,   // Local: no retries
    },
    // Video and screenshots
    video: false,           // Disable video in CI to speed up
    screenshotOnRunFailure: true,
    // Spec pattern
    specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
    // Setup node events
    setupNodeEvents(on, config) {
      // Seed database before tests
      on('task', {
        async seedDatabase() {
          // Reset test database
          return null;
        },
        async clearDatabase() {
          return null;
        },
      });
    },
  },
  // Component testing
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
    specPattern: 'src/**/*.cy.{ts,tsx}',
  },
});
// package.json scripts
{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:ci": "start-server-and-test dev http://localhost:3000 cy:run"
  }
}
npm install -D start-server-and-test

Fix 6: Debug Flaky Tests

Flaky tests usually have timing issues or shared state:

// 1. Isolate test state — reset before each test
beforeEach(() => {
  cy.task('seedDatabase');       // Reset DB to known state
  cy.clearLocalStorage();
  cy.clearCookies();
  cy.intercept('GET', '/api/**').as('apiCall');  // Catch all API calls
});

// 2. Use cy.clock() for time-dependent tests
it('shows expired message after timeout', () => {
  cy.clock();
  cy.visit('/session');
  cy.tick(30 * 60 * 1000);  // Advance 30 minutes
  cy.get('[data-cy="expired"]').should('be.visible');
});

// 3. Debug with cy.pause() and cy.debug()
it('debugging a failing test', () => {
  cy.visit('/dashboard');
  cy.pause();  // Opens interactive debugger — step through commands
  cy.get('[data-cy="chart"]').debug();  // Logs element to DevTools console
});

// 4. Check visibility — element might exist but be hidden
cy.get('[data-cy="dropdown"]').should('be.visible');  // Not just 'exist'

// 5. Force interactions on covered elements (last resort)
cy.get('[data-cy="button"]').click({ force: true });

// 6. Snapshot comparison for visual regressions
cy.get('[data-cy="chart"]').matchImageSnapshot('dashboard-chart');

Still Not Working?

cy.visit() fails with “server not running” — the dev server must be started before Cypress. Use start-server-and-test to start the server and wait for it to respond before launching Cypress. For CI, add cy:ci script: "start-server-and-test dev http://localhost:3000 cy:run".

Tests pass individually but fail when run together — shared state between tests. Each it() should be independent. Use beforeEach to reset state, not before. Also check that cy.intercept() isn’t leaking between tests — intercepts are automatically cleared between tests, but aliases set in before() persist.

TypeScript errors in Cypress files — create a cypress/tsconfig.json separate from your project’s tsconfig.json. Include "types": ["cypress"] in compilerOptions and set "include": ["./**/*.ts"]. Don’t extend the root tsconfig.json if it targets different module settings.

cy.intercept matches in one test but not another — intercept order matters. The last registered intercept for a URL takes precedence. If a beforeEach registers a broad intercept and a test registers a specific one, the specific one should be registered after. Also check for typos in the URL pattern — /api/users won’t match /api/users/ (trailing slash).

For related testing issues, see Fix: Vitest Setup Not Working and Fix: MSW Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles