Claude Skill

cypress-ops

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-t

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_cypress-ops-3dfaf0b.zip · 15 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/cypress-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Cypress Operations

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).

Construct Retries? Use for
cy.get / .find / .contains / .its / .invoke (queries) Yes — whole chain re-queries Locating/reading DOM that may not be ready
.should(...) / expect inside it Yes — the callback re-runs Assertions; conditional waits on settled state
.click / .type / .select (actions) No — fire once Interactions (leading queries still retry)
.then(cb) No — runs once, no retry protection Extracting 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.

E2E Component
Entry cy.visit('/path') cy.mount(<Comp/>)
Needs running app server Yes No (bundler dev server only)
Spec location cypress/e2e/**/*.cy.ts beside the component / cypress/component/
Support file cypress/support/e2e.ts cypress/support/component.ts (registers cy.mount)
Best for User flows, integration, auth Props/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 }}
Decision Guidance
Start the app Start it before Cypress (start + wait-on), kill after — never cy.exec a server mid-test
Parallelism cypress run --record --parallel splits specs across machines — requires Cypress Cloud (paid). Free alternative: shard specs manually across matrix jobs with --spec
Retries Config retries: { runMode: 2, openMode: 0 } — surface flakes as a queue, don't paper over them
Debugging CI failures Test 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.

Symptom Likely cause Fix
"element detached from DOM" re-render between query and action split the chain; let the action's leading query retry
passes alone, fails in suite inter-test state coupling reset server state in beforeEach; it.only to confirm
cy.wait(number) "fixes" it racing the network replace with cy.intercept(...).as() + cy.wait('@alias')
value read with .then() is stale .then doesn't retry move 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)

Factor Cypress Playwright
Execution model In-browser, async command queue (no await) Out-of-process, real async/await
Browsers Chrome-family, Firefox, Electron; WebKit experimental Chromium, Firefox, WebKit (real Safari)
Parallelism Cypress Cloud (paid) or manual sharding Free, built-in, shardable
Multi-tab / multi-origin Constrained (cy.origin for cross-origin) Native
Component testing Mature, first-class Experimental
Interactive DX The original benchmark (Cypress App, time-travel) UI mode (excellent)
API testing cy.request / cy.intercept Built-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

File Contents
references/network-and-auth.md cy.intercept matching/modifying/GraphQL, cy.session deep dive, API login, cy.origin, seed-via-request
references/component-testing.md Per-framework cy.mount, store/router/context mocking, slots/events, Vite vs webpack config
references/ci-and-flake.md Full GH Actions workflows, sharding, Test Replay, retry config, systematic flake playbook
assets/cypress.config.template.ts Commented production config template (E2E + component)
Files (claude-mods)
  • assets
    • cypress.config.template.ts 3.4 KB
      /**
       * Production Cypress config template (E2E + Component).
       *
       * Copy to cypress.config.ts at the repo root and adjust the marked sections.
       * Conventions baked in:
       *   - baseUrl set so specs use cy.visit('/relative') and relative cy.request
       *   - retries only in runMode (CI) — feel flakes immediately in openMode (local)
       *   - testIsolation left at its default (true) — each test starts from a clean slate
       *   - Test Replay assumed for CI debugging, so video is off (saves CI time)
       *   - one shared test-selector convention: [data-test=...]
       */
      import { defineConfig } from 'cypress';
      
      export default defineConfig({
        // Project-wide defaults (apply to both e2e and component unless overridden) ----------
        // Standard 1280x720; bump for wide-layout apps.
        viewportWidth: 1280,
        viewportHeight: 720,
      
        // Default 4000ms retry budget for queries/assertions. Raise only for genuinely slow
        // apps — a high global timeout masks real perf problems and slows failure feedback.
        defaultCommandTimeout: 4000,
      
        // Test Replay (v13+, Cloud, Chromium-only) is the better CI debugging artefact than
        // video and captures DOM/network/console. Turn video off when recording to Cloud.
        video: false,
        screenshotOnRunFailure: true,
      
        // Retries are flake telemetry, not a fix: a retried-then-passed test shows as "flaky".
        // 0 locally so you feel flakes the instant they appear; up to 2 in CI to keep PRs green
        // while you triage the flaky queue.
        retries: {
          runMode: 2,   // cypress run (CI)
          openMode: 0,  // cypress open (local)
        },
      
        // Fill from CI secrets via CYPRESS_RECORD_KEY env var; never hard-code it here.
        // projectId: 'abc123',   // set when recording to Cypress Cloud
      
        e2e: {
          // cy.visit('/login') and relative cy.request resolve against this. Override per
          // environment with the CYPRESS_BASE_URL env var.
          baseUrl: 'http://localhost:3000',
      
          specPattern: 'cypress/e2e/**/*.cy.{ts,tsx,js,jsx}',
          supportFile: 'cypress/support/e2e.ts',
      
          // testIsolation: true is the default — cookies/storage cleared and page reset to
          // about:blank before each test. Leave it on; reset SERVER state in beforeEach.
          // testIsolation: true,
      
          setupNodeEvents(on, config) {
            // Register Node-side plugins/tasks here, e.g. DB reset tasks, code coverage,
            // or env-specific config. Return config if you mutate it.
            //
            // on('task', { resetDb() { /* ... */ return null; } });
            return config;
          },
        },
      
        component: {
          // framework: which UI library; bundler: 'vite' or 'webpack'. Cypress infers the rest
          // of the dev-server wiring. See references/component-testing.md for the support matrix.
          devServer: {
            framework: 'react',   // 'react' | 'vue' | 'angular' | 'svelte' | 'next' | 'nuxt'
            bundler: 'vite',      // 'vite' | 'webpack'
          },
      
          // Co-locate component specs with the components, or point at cypress/component/.
          specPattern: 'src/**/*.cy.{ts,tsx,js,jsx}',
          supportFile: 'cypress/support/component.ts',  // must register cy.mount (see refs)
        },
      });
      
      // Notes:
      // - Add cypress/screenshots/, cypress/videos/, and cypress/downloads/ to .gitignore.
      // - For a multi-server app, start each server (app + api) before `cypress run` and use
      //   wait-on; never start servers inside a test via cy.exec/cy.task.
      // - The default selector convention here is [data-test=...] — wrap it in a getBySel
      //   custom command (see SKILL.md Selector Strategy) and enforce data-test on the frontend.
      
  • references
    • ci-and-flake.md 5.3 KB
      # CI & Flake Hunting
      
      ## GitHub Actions — the official action
      
      `cypress-io/github-action` wraps install, dependency caching, app boot, and the run. It is
      the path of least resistance.
      
      ```yaml
      name: e2e
      on: [push, pull_request]
      jobs:
        cypress:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v5
            - uses: cypress-io/github-action@v6
              with:
                build: npm run build
                start: npm start                 # boots the app
                wait-on: 'http://localhost:3000' # polls until the app answers — no `sleep`
                wait-on-timeout: 120
                browser: chrome
              env:
                CYPRESS_BASE_URL: http://localhost:3000
      ```
      
      Key point: **start the app outside the test run** (`start` + `wait-on`), never `cy.exec` a
      server inside a test. Port conflicts and lost stdout follow from in-test servers.
      
      ## Recording to Cypress Cloud (Test Replay)
      
      ```yaml
            - uses: cypress-io/github-action@v6
              with:
                start: npm start
                wait-on: 'http://localhost:3000'
                record: true
              env:
                CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      ```
      
      **Test Replay** (Cypress v13+) replaces video as the CI debugging artefact. It captures DOM
      mutations, network requests, JS errors, console logs, CSS, SVG, iframes, and shadow DOM —
      then lets you time-travel through the failed run in Cloud. Caveats: **Chromium-family
      browsers only** (Chrome, Edge, Electron — not Firefox/WebKit), and it does **not** capture
      video/audio elements, websockets, `localStorage`/cookies, or `cy.request` traffic. With Test
      Replay on, disable video (`video: false`) to save CI time.
      
      ## Parallelism
      
      ```yaml
        cypress:
          strategy:
            fail-fast: false
            matrix:
              containers: [1, 2, 3, 4]           # 4 machines
          steps:
            - uses: cypress-io/github-action@v6
              with:
                record: true
                parallel: true                   # Cloud balances specs across the 4
                group: 'e2e'
              env:
                CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
      ```
      
      `--parallel` **requires Cypress Cloud** (paid) — Cloud does the spec balancing. Without
      Cloud, shard manually by globbing distinct spec sets per matrix job:
      
      ```yaml
              with:
                spec: cypress/e2e/group-${{ matrix.shard }}/**/*.cy.ts
      ```
      
      This is cruder (no load balancing, you partition by hand) but free.
      
      ## Container image
      
      ```yaml
          container:
            image: cypress/browsers:node-22.11.0-chrome-131-ff-133
      ```
      
      `cypress/browsers` and `cypress/included` images pin browser + OS — the right choice when
      screenshot/visual stability matters or to avoid installing system deps each run.
      
      ## Retry configuration
      
      ```ts
      // cypress.config.ts
      export default defineConfig({
        retries: {
          runMode: 2,      // cypress run (CI): retry a failing test up to 2x
          openMode: 0,     // cypress open (local): never retry — feel flakes immediately
        },
      });
      ```
      
      Retries are **flake telemetry, not a cure**. A test that only passes on retry is a bug in
      the queue — Cypress flags it as flaky. Treat the flaky list as work, not noise.
      
      ---
      
      ## Flake playbook
      
      Most Cypress flake reduces to four root causes. Diagnose in this order.
      
      ### 1. Action chained where a query/assertion belonged
      
      ```ts
      // FLAKY — re-render between .find and .click detaches the element
      cy.get('[data-test=row]').find('[data-test=edit]').click().should('be.disabled');
      
      // STABLE — split so the action's leading query retries; assert separately
      cy.get('[data-test=row]').find('[data-test=edit]').click();
      cy.get('[data-test=edit]').should('be.disabled');
      ```
      
      "Element is detached from the DOM" almost always means this.
      
      ### 2. Racing the network with a numeric wait
      
      ```ts
      cy.wait(2000);                                  // FLAKY — guesses at timing
      // →
      cy.intercept('GET', '/api/data').as('getData'); // STABLE — wait on the actual request
      cy.get('[data-test=load]').click();
      cy.wait('@getData');
      ```
      
      ### 3. Stale value captured with `.then()`
      
      ```ts
      // FLAKY — .then doesn't retry; $count snapshot may be pre-update
      cy.get('[data-test=count]').then(($count) => {
        expect($count.text()).to.eq('5');
      });
      // →
      cy.get('[data-test=count]').should('have.text', '5');   // STABLE — .should retries
      ```
      
      ### 4. Inter-test state leakage
      
      A test that passes alone but fails in the suite is coupled to another test's state.
      - Verify with `it.only` — does it pass in isolation? If yes, it's coupling.
      - Reset **server-side** state in `beforeEach` (not `afterEach` — an `after` hook may be
        skipped if the run is interrupted or refreshed mid-test).
      - `testIsolation: true` (default) already clears browser state per test; don't disable it
        to "fix" a leak — that hides the real coupling.
      
      ### Diagnosis tooling
      
      | Tool | How | Use |
      |------|-----|-----|
      | Test Replay | `cypress run --record` → Cloud | Post-mortem a CI failure: DOM/network/console time-travel |
      | Cypress App | `cypress open` | Local time-travel: hover each command to see DOM snapshot |
      | Headed CI repro | `cypress run --headed --no-exit` | Watch the failing run locally |
      | Repeat to surface | `cypress run --spec <flaky> --env repeat=20` (loop in script) | Force intermittent flake to reproduce |
      | Screenshots | automatic on failure in `cypress/screenshots/` | Quick "what did the page look like" |
      
    • component-testing.md 3.9 KB
      # Component Testing
      
      Cypress Component Testing mounts a single component in a **real browser** (not jsdom) via a
      bundler dev server — no app server, no navigation. You get the same time-travel debugging,
      real CSS, and DevTools as E2E, scoped to one component.
      
      ## Supported matrix
      
      | Framework | Versions | Bundlers |
      |-----------|----------|----------|
      | React | 18–19 | Vite 5–8, webpack 5 |
      | Vue | 3 | Vite 5–8, webpack 5 |
      | Angular | 18–21 | webpack 5 |
      | Svelte | 5 | Vite 5–8, webpack 5 |
      | Next.js | 14–16 | webpack 5 |
      
      Configure under `component.devServer` — Cypress infers most of the dev-server wiring:
      
      ```ts
      // cypress.config.ts
      import { defineConfig } from 'cypress';
      
      export default defineConfig({
        component: {
          devServer: {
            framework: 'react',     // 'react' | 'vue' | 'angular' | 'svelte' | 'next' | 'nuxt' | ...
            bundler: 'vite',        // 'vite' | 'webpack'
          },
          specPattern: 'src/**/*.cy.{ts,tsx,js,jsx}',
        },
      });
      ```
      
      ## Registering cy.mount
      
      `cy.mount` is **not** built in — register it once in the component support file so every
      spec gets it (and so types resolve):
      
      ```ts
      // cypress/support/component.ts  (React)
      import { mount } from 'cypress/react';
      import './commands';
      
      Cypress.Commands.add('mount', mount);
      
      declare global {
        namespace Cypress {
          interface Chainable {
            mount: typeof mount;
          }
        }
      }
      ```
      
      Swap the import per framework: `cypress/react`, `cypress/vue`, `cypress/angular`,
      `cypress/svelte`.
      
      ## Mounting per framework
      
      ```tsx
      // React — JSX, props inline
      cy.mount(<Stepper initial={5} onChange={cy.stub().as('onChange')} />);
      
      // Vue 3 — props/slots via options object
      cy.mount(Stepper, {
        props: { count: 100 },
        slots: { default: 'Label text' },
      });
      
      // Angular — component class + config object
      cy.mount(StepperComponent, {
        componentProperties: { count: 100 },
      });
      
      // Svelte 5
      cy.mount(Stepper, { props: { count: 100 } });
      ```
      
      ## Asserting props, events, slots
      
      Spy on callbacks with `cy.stub().as(...)`, drive the component through the DOM, assert the
      spy fired:
      
      ```tsx
      it('emits on increment', () => {
        cy.mount(<Stepper initial={0} onChange={cy.stub().as('onChange')} />);
        cy.get('[data-test=increment]').click();
        cy.get('[data-test=count]').should('have.text', '1');
        cy.get('@onChange').should('have.been.calledWith', 1);
      });
      ```
      
      ## Mocking stores / router / context
      
      Components that consume a store, router, or context need a provider wrapper at mount.
      Compose it in a local helper so specs stay clean:
      
      ```tsx
      // React — wrap in providers
      function mountWithProviders(ui: React.ReactNode, { route = '/' } = {}) {
        window.history.pushState({}, '', route);
        return cy.mount(
          <MemoryRouter initialEntries={[route]}>
            <QueryClientProvider client={new QueryClient()}>{ui}</QueryClientProvider>
          </MemoryRouter>,
        );
      }
      ```
      
      ```ts
      // Vue — global plugins/stubs
      cy.mount(UserCard, {
        global: {
          plugins: [createTestingPinia()],
          stubs: { RouterLink: true },
        },
      });
      ```
      
      ## Network in component tests
      
      `cy.intercept` works in component tests exactly as in E2E — stub the component's data
      fetches:
      
      ```tsx
      cy.intercept('GET', '/api/user/1', { fixture: 'user.json' }).as('getUser');
      cy.mount(<UserProfile id={1} />);
      cy.wait('@getUser');
      cy.get('[data-test=name]').should('have.text', 'Alice');
      ```
      
      ## When component vs E2E
      
      | Test it as a **component** when… | Test it **E2E** when… |
      |----------------------------------|------------------------|
      | Verifying props/events/slots in isolation | Verifying a multi-page user flow |
      | Exercising many edge states (loading/error/empty) cheaply | Auth, routing, real backend integration |
      | Visual states of one component | Cross-component / cross-page behaviour |
      | No server or navigation required | The app must actually be running |
      
      A healthy suite uses **component tests for breadth** (many states, fast) and **E2E for the
      critical user journeys** — not E2E for everything.
      
    • network-and-auth.md 6.1 KB
      # Network Stubbing & Authentication
      
      Deep dive on `cy.intercept` and `cy.session`. The SKILL.md body covers the 80% path;
      this file is the rest.
      
      ## cy.intercept — matching
      
      ```ts
      // String shorthand: method + url glob
      cy.intercept('GET', '/api/users');
      cy.intercept('POST', '/api/users', { statusCode: 201 });
      
      // routeMatcher object — match on more than method+url
      cy.intercept({
        method: 'GET',
        url: '/api/orders/*',
        query: { status: 'open' },        // match only when ?status=open
        headers: { 'x-tenant': 'acme' },
      });
      
      // Regex urls
      cy.intercept(/\/api\/orders\/\d+$/);
      ```
      
      Glob (`*` one segment, `**` many) is the default for string URLs; pass a `RegExp` for
      precise control. A later `cy.intercept` for the same route **overrides** an earlier one
      within the same test — define the most specific matcher first if both could match.
      
      ## cy.intercept — stubbing variants
      
      ```ts
      // Fixture file (cypress/fixtures/users.json)
      cy.intercept('GET', '/api/users', { fixture: 'users.json' });
      
      // Inline body + status + headers
      cy.intercept('GET', '/api/users', {
        statusCode: 200,
        body: [{ id: 1, name: 'Alice' }],
        headers: { 'cache-control': 'no-store' },
      });
      
      // Force network failure / latency / offline
      cy.intercept('GET', '/api/slow', { forceNetworkError: true });
      cy.intercept('GET', '/api/slow', (req) => { req.reply({ delay: 2000, body: {} }); });
      ```
      
      ## cy.intercept — spying & modifying (the function form)
      
      The function form gives the request (`req`) for inspection/mutation; `req.reply` /
      `req.continue` control the response.
      
      ```ts
      // Spy only (no stub) — let it hit the server, just observe
      cy.intercept('POST', '/api/cart').as('addToCart');
      cy.get('[data-test=add]').click();
      cy.wait('@addToCart').then(({ request, response }) => {
        expect(request.body).to.deep.equal({ sku: 'ABC', qty: 1 });
        expect(response?.statusCode).to.eq(200);
      });
      
      // Mutate the outgoing request
      cy.intercept('GET', '/api/me', (req) => {
        req.headers['authorization'] = 'Bearer test-token';
        req.continue();                 // pass through to the real server
      });
      
      // Mutate the real response before it reaches the app
      cy.intercept('GET', '/api/feed', (req) => {
        req.reply((res) => {
          res.body.items = res.body.items.slice(0, 1);   // truncate for a deterministic test
        });
      });
      
      // Conditionally stub vs passthrough
      cy.intercept('GET', '/api/flags', (req) => {
        if (req.query.exp === 'B') req.reply({ body: { variant: 'B' } });
        else req.continue();
      });
      ```
      
      ## GraphQL (single endpoint, many operations)
      
      GraphQL POSTs everything to one URL, so match on the **operation name** in the body:
      
      ```ts
      const hasOperation = (req, name) =>
        req.body?.operationName === name;
      
      cy.intercept('POST', '/graphql', (req) => {
        if (hasOperation(req, 'GetUser')) {
          req.reply({ fixture: 'gql/getUser.json' });
        }
        if (hasOperation(req, 'ListOrders')) {
          req.alias = 'gqlListOrders';          // dynamic alias per operation
          req.reply({ fixture: 'gql/listOrders.json' });
        }
      });
      cy.wait('@gqlListOrders');
      ```
      
      ## Waiting on multiple / counted requests
      
      ```ts
      cy.wait(['@getUsers', '@getOrders']);     // both must fire
      
      // Nth occurrence of a repeated request
      cy.wait('@getUsers');                     // 1st
      cy.wait('@getUsers');                     // 2nd
      ```
      
      ---
      
      ## cy.session — full mechanics
      
      ```ts
      cy.session(id, setup);
      cy.session(id, setup, options);
      ```
      
      | Param / option | Behaviour |
      |----------------|-----------|
      | `id` | String / Array / Object cache key. Arrays & objects are deterministically stringified. Same id → same cached session |
      | `setup` | Runs **only on cache miss** (or when `validate` fails). Establishes the session (login) |
      | `validate()` | Runs after setup **and** after every restore. Throw / failing assertion → session invalid. After-restore failure re-runs `setup`; after-setup failure fails the test |
      | `cacheAcrossSpecs` | `false` (default) = session lives for the spec. `true` = global, restorable in any spec in the run |
      
      **Always-cleared invariant:** cookies, `localStorage`, and `sessionStorage` in **all
      domains** are cleared before `setup` runs, *regardless of `testIsolation`*. So `setup`
      starts from a clean slate every cache miss.
      
      **Assert a logged-in signal inside `setup` before it returns** — otherwise Cypress caches
      a half-authenticated state and every restore is broken.
      
      ## Faster auth: skip the UI
      
      UI login is slow and re-tests the login form on every session. Prefer a programmatic login
      in `setup`:
      
      ```ts
      Cypress.Commands.add('loginByApi', (username, password) => {
        cy.session([username, password], () => {
          cy.request('POST', '/api/login', { username, password }).then(({ body }) => {
            window.localStorage.setItem('auth_token', body.token);   // or set a cookie
          });
        }, {
          validate() { cy.window().its('localStorage.auth_token').should('exist'); },
          cacheAcrossSpecs: true,
        });
      });
      ```
      
      Keep **one** UI-driven login test that exercises the real form; everything else uses the
      API path.
      
      ## Cross-origin: cy.origin
      
      Cypress confines a test to one superdomain. To interact with another origin (SSO provider,
      OAuth consent screen on a domain you control), wrap those steps in `cy.origin`:
      
      ```ts
      cy.origin('https://auth.example.com', () => {
        cy.get('[data-test=email]').type('user@example.com');
        cy.get('[data-test=password]').type('secret');
        cy.get('[data-test=submit]').click();
      });
      ```
      
      Caveats: variables must be passed in via the `args` option (the callback runs in a separate
      context — no closure access); `data-test` selectors and commands work, but custom commands
      need re-registering inside or via `Cypress.require`. Avoid testing third-party social-login
      UIs you don't control (captchas, A/B tests, throttling, bans) — stub them or use `cy.request`
      against the provider's API instead.
      
      ## Seed-via-request, assert-via-UI
      
      The fastest way to set up state: create it through the API, verify through the UI.
      
      ```ts
      beforeEach(() => {
        cy.request('POST', '/api/test/reset');                       // reset server state
        cy.request('POST', '/api/projects', { name: 'Apollo' });     // seed
      });
      
      it('shows the seeded project', () => {
        cy.visit('/projects');
        cy.contains('[data-test=project]', 'Apollo').should('be.visible');
      });
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
  • SKILL.md 15.1 KB
    ---
    name: cypress-ops
    description: "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."
    when_to_use: "Use when writing or fixing Cypress e2e/component tests — e.g. 'pick stable selectors and data-cy', 'stub network with cy.intercept', 'share login with cy.session', 'diagnose a flaky retry-ability test'. For Playwright or other runners, use playwright-ops / testing-ops."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: "playwright-ops, testing-ops, ci-cd-ops"
    ---
    
    # Cypress Operations
    
    > 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
    
    ```bash
    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.
    
    ```ts
    // 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](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).
    
    | Construct | Retries? | Use for |
    |-----------|----------|---------|
    | `cy.get` / `.find` / `.contains` / `.its` / `.invoke` (queries) | Yes — whole chain re-queries | Locating/reading DOM that may not be ready |
    | `.should(...)` / `expect` inside it | Yes — the callback re-runs | Assertions; conditional waits on settled state |
    | `.click` / `.type` / `.select` (actions) | No — fire once | Interactions (leading queries still retry) |
    | `.then(cb)` | **No** — runs once, no retry protection | Extracting a value; NOT for assertions |
    
    ```ts
    // .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.
    
    ```ts
    // 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:
    
    ```ts
    // 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.
    
    ```ts
    // 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](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.
    
    ```ts
    // 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](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.
    
    | | E2E | Component |
    |---|---|---|
    | Entry | `cy.visit('/path')` | `cy.mount(<Comp/>)` |
    | Needs running app server | Yes | No (bundler dev server only) |
    | Spec location | `cypress/e2e/**/*.cy.ts` | beside the component / `cypress/component/` |
    | Support file | `cypress/support/e2e.ts` | `cypress/support/component.ts` (registers `cy.mount`) |
    | Best for | User flows, integration, auth | Props/events/slots, edge states, visual |
    
    ```ts
    // 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](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
    
    ```yaml
    # 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 }}
    ```
    
    | Decision | Guidance |
    |----------|----------|
    | Start the app | Start it **before** Cypress (`start` + `wait-on`), kill after — never `cy.exec` a server mid-test |
    | Parallelism | `cypress run --record --parallel` splits specs across machines — **requires Cypress Cloud** (paid). Free alternative: shard specs manually across matrix jobs with `--spec` |
    | Retries | Config `retries: { runMode: 2, openMode: 0 }` — surface flakes as a queue, don't paper over them |
    | Debugging CI failures | **Test 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](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.
    
    | Symptom | Likely cause | Fix |
    |---------|-------------|-----|
    | "element detached from DOM" | re-render between query and action | split the chain; let the action's leading query retry |
    | passes alone, fails in suite | inter-test state coupling | reset server state in `beforeEach`; `it.only` to confirm |
    | `cy.wait(number)` "fixes" it | racing the network | replace with `cy.intercept(...).as()` + `cy.wait('@alias')` |
    | value read with `.then()` is stale | `.then` doesn't retry | move 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](references/ci-and-flake.md).
    
    ## Cypress vs Playwright (one-table decision)
    
    | Factor | Cypress | Playwright |
    |--------|---------|-----------|
    | Execution model | In-browser, async command queue (no `await`) | Out-of-process, real `async/await` |
    | Browsers | Chrome-family, Firefox, Electron; WebKit experimental | Chromium, Firefox, **WebKit (real Safari)** |
    | Parallelism | Cypress Cloud (paid) or manual sharding | Free, built-in, shardable |
    | Multi-tab / multi-origin | Constrained (`cy.origin` for cross-origin) | Native |
    | Component testing | **Mature, first-class** | Experimental |
    | Interactive DX | The original benchmark (Cypress App, time-travel) | UI mode (excellent) |
    | API testing | `cy.request` / `cy.intercept` | Built-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](assets/cypress.config.template.ts)
    
    ```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
    
    | File | Contents |
    |------|----------|
    | [references/network-and-auth.md](references/network-and-auth.md) | `cy.intercept` matching/modifying/GraphQL, `cy.session` deep dive, API login, `cy.origin`, seed-via-request |
    | [references/component-testing.md](references/component-testing.md) | Per-framework `cy.mount`, store/router/context mocking, slots/events, Vite vs webpack config |
    | [references/ci-and-flake.md](references/ci-and-flake.md) | Full GH Actions workflows, sharding, Test Replay, retry config, systematic flake playbook |
    | [assets/cypress.config.template.ts](assets/cypress.config.template.ts) | Commented production config template (E2E + component) |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related