Skip to content

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

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

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, and several of those differences are easy to get wrong on first contact. The single most important concept is that cy commands are not Promises. They are entries in an internal queue that Cypress drains in order, retrying each one until it passes or times out. You cannot await them, you cannot store their return value in a variable for later use, and if/else based on a command’s result will not work the way you expect.

A second source of confusion is isolation between tests. By default, Cypress 12+ wipes browser state between every it() — cookies, localStorage, sessionStorage, the page itself. That sounds helpful until you realize your cy.login() in a before() block runs once and then loses its session before the first test runs. The fix is cy.session(), which caches the login across the same spec and validates it with a callback.

A third trap is cross-origin work. Cypress traditionally only operated within the origin of the page under test. Visiting a SSO provider, an embedded payment iframe, or a different subdomain produced “cross-origin error” rejections. cy.origin() (Cypress 9.6+) opens a new command queue scoped to that origin, but its callback body is serialized and re-executed inside the foreign context — you can’t close over outer variables.

  • 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.
  • cy.intercept matches by URL pattern at registration time — intercepts must be registered before the request fires.
  • 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.
  • 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.

Diagnostic Timeline

A senior dev’s first guess for a flaky Cypress test is “increase the timeout.” That fix masks the bug rather than finding it. The real causes follow a different order.

Minute 0 — Open the Command Log replay. Cypress records every command. Click any failing command and look at the DOM snapshot just before failure. If the element you’re querying isn’t even in the DOM at that moment, your test is asserting too early — the page hasn’t reached the state you expect. The fix is an assertion before the click (should('be.visible')), not a longer timeout.

Minute 3 — Check for cross-origin iframes. If you see cy.fail({ error: 'cross-origin' }) or “Blocked a frame with origin,” the page navigated to a different origin (OAuth provider, 3DS payment, embedded help widget). Wrap that section in cy.origin('https://accounts.example.com', () => { ... }). Variables outside the callback are not accessible inside — pass them via the second argument’s args.

Minute 7 — Check cy.intercept registration order. If your stub isn’t matching, set a breakpoint on the request in DevTools and see what URL fires. Most missed-matches come from query strings (/api/users?page=1 doesn’t match /api/users), missing methods (cy.intercept('/users') matches any method but order of registration determines which one wins), or registering the intercept after cy.visit() triggered the request. Register intercepts first, alias them, then cy.wait('@alias').

Minute 10 — Check cy.session() isolation. If tests pass alone but fail in a suite, you almost certainly have leftover state. Run with --headed and watch what happens between tests. If you see the login flow re-run unexpectedly, the previous test mutated the session in a way that fails your cy.session() validate callback. Add a more robust validate (cy.request('/api/me').its('status').should('eq', 200)).

Minute 14 — Check viewport for mobile-only failures. Cypress’s default viewport is 1000x660. A button that’s visible on desktop may be inside a hamburger menu on mobile. If you set viewportWidth: 375, your “click button” command will time out because the menu must be opened first.

Minute 17 — Check force: true overuse. If you needed { force: true } to make a click work, you’re hiding a real bug. Cypress refuses to click elements that are covered, detached, or animating. The right fix is to assert that the cover element no longer exists before clicking, not to bypass the actionability check.

Minute 20 — Check the dev server. If you see “could not verify that this server is running,” start-server-and-test is the canonical fix. The script form is start-server-and-test "npm run dev" http://localhost:3000 "cypress run". The middle argument is a URL Cypress polls before launching.

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 and Cross-Origin

Avoid logging in through the UI for every test, and handle SSO with cy.origin:

// 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);
        });
    },
    {
      // Validate the session before reusing — re-run setup if invalid
      validate() {
        cy.request('/api/me').its('status').should('eq', 200);
      },
      cacheAcrossSpecs: true,
    }
  );
});

// Cross-origin OAuth — variables must be passed via args
it('logs in with Google', () => {
  cy.visit('/login');
  cy.contains('Sign in with Google').click();

  cy.origin('https://accounts.google.com', { args: { email: '[email protected]' } }, ({ email }) => {
    cy.get('input[type="email"]').type(email);
    cy.contains('Next').click();
  });

  // Back to your origin
  cy.url().should('include', '/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).

Cross-origin error during OAuth or 3D Secure flows — by default, Cypress can only command one origin per test. When the page redirects to a third party (Google, GitHub, Stripe), wrap interactions in cy.origin('https://example.com', () => { ... }). The callback runs in a fresh JavaScript context inside that origin — outside variables are not in scope. Pass data via { args: { ... } }.

cy.session() keeps re-running the setup — your validate callback is rejecting the cached session. A common cause: the validate makes an unauthenticated request because the auth token isn’t sent. Either drop the validate (Cypress will skip re-running setup) or use a request that intentionally checks auth (cy.request({ url: '/api/me', failOnStatusCode: false })).

Stubbed response arrives before the request is dispatched — when you assert on a response after a click, Cypress waits for the actual request. If your intercept replied synchronously but the SPA debounces the dispatch, cy.wait('@alias') will hit the timeout. Add a small req.reply({ delay: 50 }) to let the click animation finish, or assert on the UI state instead of the network call.

For related testing issues, see Fix: Vitest Setup Not Working, Fix: MSW Not Working, Fix: Playwright Not Working, and Fix: Jest Test Timeout.

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