Fix: Cypress Not Working — Tests Timing Out, Elements Not Found, or cy.intercept Not Matching
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 matchOr 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:3000Why 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 Promises —
cy.get()doesn’t return the element immediately. It enqueues a command that retries until the element is found or a timeout is reached. Mixingcycommands with synchronous JavaScript logic (like storing a value in a variable) breaks the command chain. cy.interceptmatches by URL pattern at registration time — intercepts must be registered before the request fires. If the app makes the request beforecy.intercept()runs (e.g., duringcy.visit()), the intercept misses it. URL patterns are also exact by default —/api/userswon’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. Usecypress.config.tsto configure adevServeror 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 finishAdd 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-testFix 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: MSW (Mock Service Worker) Not Working — Handlers Not Intercepting, Browser Not Mocking, or v2 Migration Errors
How to fix Mock Service Worker issues — browser vs Node setup, handler registration, worker start timing, passthrough requests, and common MSW v2 API changes from v1.
Fix: Playwright Not Working — Test Timeout, Selector Not Found, or CI Headless Fails
How to fix Playwright test issues — locator strategies, auto-waiting, network mocking, flaky tests in CI, trace viewer debugging, and common headless browser setup problems.
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: React Testing Library Not Finding Element — Unable to Find Role or Text
How to fix React Testing Library query failures — getByRole vs getByText, async queries, accessible names, waitFor patterns, custom queries, and common selector mistakes.