Cypress Operations

SkillSearch

Cypress end-to-end and component testing operations - selector/retry-ability strategy, cy.intercept network stubbing, cy.session auth, component vs e2e, flake diagnosis, CI, Test Replay. Use for: cypress, e2e test, component test, cy.get, cy.intercept, cy.session, data-cy, data-test, retry-ability, flake, flaky test, cypress.config, cy.mount, Test Replay, custom commands, fixtures.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Cypress Operations skill

What this skill tells your AI

The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/cypress-ops/SKILL.md and read by ahel’s review.

Facts verified as of 2026-07.

Version context (verified against docs.cypress.io, 2026-06): Cypress 14.x, Test Replay (v13+), cy.session with cacheAcrossSpecs. APIs move — confirm against the live docs when a detail is load-bearing.

End-to-end and component testing with Cypress (cypress, TS/JS). The runner executes tests inside a real browser via the Cypress App (cypress open) or headlessly (cypress run). The defining mental model: cy.* commands are not promises — they enqueue onto an async command chain that Cypress drains for you. Internalise that and the agentic gotchas below disappear.

Quick Start

npm install -D cypress
npx cypress open                  # launch the Cypress App: pick E2E or Component, real browser
npx cypress run                   # headless run, all specs (CI default)
npx cypress run --spec "cypress/e2e/auth/*.cy.ts"
npx cypress run --component       # run component specs
npx cypress run --browser chrome --headed
npx cypress run --record --key <k>  # upload to Cypress Cloud (enables Test Replay, v13+)

Specs live in cypress/e2e/**/*.cy.ts (E2E) and beside components or cypress/component/ (component). Config is a single cypress.config.ts at the repo root.

The Async Command Queue (read this first)

cy.get(...) returns a Chainer, not the element and not a Promise. Commands are scheduled, then run in order after the test function returns. This is the source of nearly every Cypress mistake an agent makes.

// WRONG — cy.get does not return a value; `el` is a Chainer, this is meaningless
const el = cy.get('[data-test=total]');
if (el.text() === '$0') { /* never works */ }

// WRONG — async/await does nothing useful; cy commands aren't awaitable promises
const text = await cy.get('[data-test=total]');   // do NOT do this

// RIGHT — yield the value into a callback; assertions inside .should() retry
cy.get('[data-test=total]').should('have.text', '$0');

// RIGHT — need the raw value? use .then() (but it does NOT retry — see below)
cy.get('[data-test=total]').invoke('text').then((text) => {
  // text is a string here; runs after the queue reaches this point
});

Rules that follow from this:

  • No const/let to "store" a command result. Use .as() aliases + cy.get('@alias').
  • No async/await on cy.*. The queue handles ordering. Mixing in real promises? wrap them with cy.then(() => promise) or cy.wrap(promise).
  • No if/else on element state read synchronously. Conditional testing is an anti-pattern in Cypress (the DOM may not have settled); make the app deterministic, or drive the branch off a server/cy.intercept state you control. Deep dive: references/network-and-auth.md.

Retry-ability (why you almost never need waits)

Cypress retries queries and assertions until they pass or the command times out (default 4s). It does not retry actions (.click(), .type(), .select()) — those fire once, though the queries leading up to them retry until the element is actionable (visible, not disabled, not animating).

ConstructRetries?Use for
cy.get / .find / .contains / .its / .invoke (queries)Yes — whole chain re-queriesLocating/reading DOM that may not be ready
.should(...) / expect inside itYes — the callback re-runsAssertions; conditional waits on settled state
.click / .type / .select (actions)No — fire onceInteractions (leading queries still retry)
.then(cb)No — runs once, no retry protectionExtracting a value; NOT for assertions
// .should(callback) retries the whole callback — safe for racy DOM
cy.get('[data-test=rows] li').should(($li) => {
  expect($li).to.have.length(3);
  expect($li.first()).to.contain('Alice');
});

// .then() does NOT retry — capturing $el here then asserting later races the render

If you reach for cy.wait(3000), you're missing an assertion or an aliased intercept. The only legitimate cy.wait takes an alias (cy.wait('@getUsers')), never a number.

Selector Strategy

Prefer a dedicated test attribute over CSS classes, IDs, or tag names — the latter are brittle and change with styling/refactors. Cypress recommends data-cy or data-test (the Cypress Real World App standardises on data-test); pick one and enforce it.

// GOOD — decoupled from styling and structure
cy.get('[data-test=submit]').click();

// AVOID — couples the test to CSS/markup that changes for non-test reasons
cy.get('.btn-primary').click();
cy.get('#submit').click();

Wrap the convention in a custom command so specs stay terse:

// cypress/support/commands.ts
Cypress.Commands.add('getBySel', (sel, ...args) =>
  cy.get(`[data-test=${sel}]`, ...args));
Cypress.Commands.add('getBySelLike', (sel, ...args) =>
  cy.get(`[data-test*=${sel}]`, ...args));  // substring match
// usage: cy.getBySel('submit').click();

Reserve cy.contains('Log In') for when the visible text itself is what you're asserting; otherwise it couples tests to copy.

Network Stubbing — cy.intercept

cy.intercept is the single API for spying on and stubbing network traffic. Set it up before the action that triggers the request, alias it, then wait on the alias.

// Stub with a fixture, alias, wait
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');                       // resolves when the request fires

// Inline body / status
cy.intercept('POST', '/api/login', { statusCode: 401, body: { error: 'nope' } }).as('login');

// routeMatcher object (method + glob/regex url) + dynamic reply
cy.intercept({ method: 'GET', url: '/api/orders/*' }, (req) => {
  req.reply((res) => { res.body.hasMore = false; });   // tweak the real response
}).as('orders');

// Assert against the captured request/response
cy.wait('@login').its('response.statusCode').should('eq', 401);

// Wait on several at once
cy.wait(['@getUsers', '@orders']);

Stub what you don't own, exercise what you do. Stubbing third-party/slow endpoints makes tests fast and deterministic; hitting your real backend (seeded via cy.request) verifies the client↔server contract. Decide per endpoint. GraphQL, request modification, and seed-via-cy.request patterns: references/network-and-auth.md.

Authentication — cy.session

Log in once, cache the session, restore it across tests (and optionally specs). This is the biggest suite-speed win after stubbing.

// cypress/support/commands.ts
Cypress.Commands.add('login', (username: string, password: string) => {
  cy.session(
    [username, password],                   // cache key — array/object is stringified
    () => {                                  // setup: runs only on cache miss
      cy.visit('/login');
      cy.get('[data-test=name]').type(username);
      cy.get('[data-test=password]').type(password);
      cy.get('form').contains('Log In').click();
      cy.url().should('contain', '/dashboard');   // assert logged-in before caching!
    },
    {
      validate() {                           // runs after setup AND after each restore
        cy.getCookie('auth_token').should('exist');  // invalid -> setup re-runs
      },
      cacheAcrossSpecs: true,                // default false; true = reuse in every spec
    },
  );
});

Critical behaviour: cookies, localStorage, and sessionStorage across all domains are cleared before setup runs, regardless of testIsolation. Faster still: skip the UI and log in via cy.request inside setup, persisting the token. Patterns (API login, token priming, cy.origin for cross-origin SSO): references/network-and-auth.md.

Component vs E2E Testing

Same runner, two testing types. E2E drives a deployed app through cy.visit. Component mounts a single component in a real browser via cy.mount — no server, no navigation, props/events under direct control.

E2EComponent
Entrycy.visit('/path')cy.mount(<Comp/>)
Needs running app serverYesNo (bundler dev server only)
Spec locationcypress/e2e/**/*.cy.tsbeside the component / cypress/component/
Support filecypress/support/e2e.tscypress/support/component.ts (registers cy.mount)
Best forUser flows, integration, authProps/events/slots, edge states, visual
// cypress/support/component.ts  (React example)
import { mount } from 'cypress/react';
Cypress.Commands.add('mount', mount);

// Button.cy.tsx
cy.mount(<Button label="Save" onClick={cy.stub().as('onClick')} />);
cy.get('[data-test=button]').click();
cy.get('@onClick').should('have.been.calledOnce');

Frameworks: React 18–19, Vue 3, Angular 18–21, Svelte 5. Bundlers: Vite 5–8 (React/Vue/ Svelte) or webpack 5 (all + Next.js). Configured under component.devServer.{framework,bundler}. Mounting per framework, store/router mocking, slots: references/component-testing.md.

Test Isolation, Fixtures, Custom Commands

  • testIsolation: true (default, E2E) clears cookies/storage and resets to about:blank before each test. Each test must pass run in isolation (it.only to verify) — never rely on a previous test's state. Reset server-side state in beforeEach, not afterEach (an after hook may not run if you refresh mid-test).
  • Multiple assertions per test are fine — don't split into one-assertion tests; state reset between tests costs more than extra assertions.
  • Fixtures are static JSON in cypress/fixtures/, loaded by cy.fixture('users.json') or referenced directly in cy.intercept(..., { fixture: 'users.json' }).
  • Custom commands (Cypress.Commands.add) live in cypress/support/commands.ts; add the cypress/react (etc.) types and a declare global block for TS autocomplete.

CI

# GitHub Actions — the official cypress-io/github-action handles install + cache + run
- uses: actions/checkout@v5
- uses: cypress-io/github-action@v6
  with:
    build: npm run build
    start: npm start                 # boots app, waits on baseUrl before running
    wait-on: 'http://localhost:3000'
    browser: chrome
    record: true                     # upload to Cypress Cloud (Test Replay)
  env:
    CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
DecisionGuidance
Start the appStart it before Cypress (start + wait-on), kill after — never cy.exec a server mid-test
Parallelismcypress run --record --parallel splits specs across machines — requires Cypress Cloud (paid). Free alternative: shard specs manually across matrix jobs with --spec
RetriesConfig retries: { runMode: 2, openMode: 0 } — surface flakes as a queue, don't paper over them
Debugging CI failuresTest Replay (v13+, Chromium-only) over video: captures DOM, network, console, errors for time-travel debugging in Cloud

Full workflows (matrix sharding, containers, artifact upload): references/ci-and-flake.md.

Flake Diagnosis

Most Cypress flake traces to one of: an action chained where a query/assertion belonged, a missing aliased cy.wait, conditional logic on un-settled DOM, or leaked state between tests.

SymptomLikely causeFix
"element detached from DOM"re-render between query and actionsplit the chain; let the action's leading query retry
passes alone, fails in suiteinter-test state couplingreset server state in beforeEach; it.only to confirm
cy.wait(number) "fixes" itracing the networkreplace with cy.intercept(...).as() + cy.wait('@alias')
value read with .then() is stale.then doesn't retrymove the assertion into .should(cb)

Diagnosis tooling (Test Replay, cypress run --headed, time-travel in the App, screenshots/ video), retry config, and a systematic playbook: references/ci-and-flake.md.

Cypress vs Playwright (one-table decision)

FactorCypressPlaywright
Execution modelIn-browser, async command queue (no await)Out-of-process, real async/await
BrowsersChrome-family, Firefox, Electron; WebKit experimentalChromium, Firefox, WebKit (real Safari)
ParallelismCypress Cloud (paid) or manual shardingFree, built-in, shardable
Multi-tab / multi-originConstrained (cy.origin for cross-origin)Native
Component testingMature, first-classExperimental
Interactive DXThe original benchmark (Cypress App, time-travel)UI mode (excellent)
API testingcy.request / cy.interceptBuilt-in request context

Reach for Cypress when component-testing maturity, an existing Cypress investment, or its in-browser DX dominate. Default to Playwright for new E2E needing WebKit, free parallelism, or heavy multi-tab/multi-origin work. Sibling skill: playwright-ops.

Config Skeleton

Full commented production template: assets/cypress.config.template.ts

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',        // cy.visit('/path') resolves against this
    specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
    retries: { runMode: 2, openMode: 0 },    // retry in CI only
    setupNodeEvents(on, config) { return config; },
  },
  component: {
    devServer: { framework: 'react', bundler: 'vite' },
  },
  // testIsolation defaults true; viewportWidth/Height, defaultCommandTimeout tunable here
});

References

FileContents
references/network-and-auth.mdcy.intercept matching/modifying/GraphQL, cy.session deep dive, API login, cy.origin, seed-via-request
references/component-testing.mdPer-framework cy.mount, store/router/context mocking, slots/events, Vite vs webpack config
references/ci-and-flake.mdFull GH Actions workflows, sharding, Test Replay, retry config, systematic flake playbook
assets/cypress.config.template.tsCommented production config template (E2E + component)

Signals

GitHub stars
36
Forks
5
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
cypress-ops
Source
github.com/0xdarkmatter/claude-mods