Cypress E2E Testing | Selectors· cy.intercept
이 글의 핵심
This English guide follows the Korean Cypress E2E article—first tests, data-testid discipline, cy.intercept fixtures, API login helpers, GitHub Actions—and adds internals on Cypress’s event-driven architecture, network stubbing model, and how teams pair Cypress with unit tests and staging checks before production.
Introduction
Cypress is an end-to-end (E2E) testing framework that runs in the browser and optimizes for fast feedback, deterministic assertions, and excellent debugging. Teams adopt it to guard critical user journeys: checkout, signup, role-based access, and integrations that unit tests cannot fully represent.
I once spent the better part of an afternoon on a cy.get that “worked in the runner but not in CI” before admitting the selector was fine and our preview deploy was pointing at a stale API. Cypress gets blamed; half the time the culprit is environment parity, not the framework.
This guide assumes you are shipping a web application (React, Vue, Svelte, or classic multi-page apps) and need a repeatable, CI-friendly suite. You will see how to:
- Bootstrap Cypress with both scaffolded and manual setups.
- Stabilize UI automation with
data-cy/data-testidand avoid brittle CSS selectors. - Use
cy.interceptto mock REST and GraphQL-style HTTP, synchronize tests with aliases, and test failure paths. - Encode login once and reuse it via sessions and custom commands.
- Run everything in GitHub Actions with artifacts and sensible parallelism options.
A practical mindset: E2E tests are expensive—they are slower and flakier than unit tests. Reserve them for paths that, if broken, would embarrass you in front of customers. Pair them with Vitest/Jest for modules, API contract tests for backends, and observability in production. This article complements the [Cypress Complete Guide](/en/blog/cypress-complete-guide/. A direct tool comparison (including Playwright E2E Testing comes up front in the next section so you can sanity-check the stack before you invest in config.
When Cypress shines (and when it does not)
| Scenario | Favor Cypress | Consider alternatives |
|---|---|---|
| Single-browser regression on Chrome-family | Strong DX, time-travel | Playwright for broader matrix |
| Tight network stubbing during UI flows | cy.intercept is first-class | Selenium if stuck on legacy stack |
| Small team, JS-first, fast iteration | Onboarding is smooth | If team is Python-first, Pytest + Playwright |
Production insight: Most teams run Cypress against a preview app per PR and a smoke slice on staging after deploy. Running the full E2E suite against production is rare, controlled, and usually read-only (health checks), not a substitute for load testing or monitoring.
Comparison: Cypress, Playwright, and Selenium
| Dimension | Cypress | Playwright | Selenium |
|---|---|---|---|
| Primary appeal | DX, time-travel, ecosystem | Multi-browser, tracing, speed | Language flexibility, WebDriver std |
| Typical test language | JavaScript/TypeScript | JS/TS, Python, C#, Java, … | Many |
| Parallelism (native) | Cloud / CI strategies | First-class, built-in sharding | Depends on grid setup |
| Network control | cy.intercept | page.route | Less uniform; app-dependent |
| Test architecture | In-browser (with product evolution) | Out-of-process driver + browsers | Out-of-process WebDriver |
How to pick: if your org is TS-first and you focus on a tight browser set, Cypress remains compelling. If you need WebKit + Chromium + Firefox on every PR with one codebase, Playwright is often the winner. If you are locked into a Java enterprise WebDriver stack, Selenium may remain the right integration path.
See [Playwright E2E Testing Guide](/en/blog/playwright-e2e-testing-guide/ for a parallel perspective. The rest of this guide is Cypress-first; the comparison is here on purpose so you are not 6,000 words in before you second-guess the tool.
Prerequisites
Before you install anything, line up the basics:
- Node.js LTS and a package manager (npm, pnpm, or Yarn). Pin versions in CI.
- A dev or preview URL for the app, or a start script (for example
npm startwithPORT=3000). - Agreement on selector strategy (
data-cyvsdata-testid) and test data (seed accounts, feature flags, API environments). - A decision on where tests live—commonly
cypress/e2ewith support files for shared setup.
If your app requires CORS or cookie rules, verify the baseUrl in Cypress config matches the origin your app actually uses. Mismatched origins are a top cause of “works locally, fails in CI.”
Installation: three practical paths
Option A: Official scaffold (quickest for greenfield projects)
npm create cypress@latest
# Follow the wizard: E2E, TypeScript, example specs—pick what matches your stack.
This generates cypress.config.ts, cypress/, and npm scripts. Commit the folder and keep example specs only if you want onboardings—most teams delete demo tests after the first week.
Option B: Add to an existing monorepo package
In the workspace package (for example apps/web):
cd apps/web
npm install -D cypress
npx cypress open
# First run downloads the binary; then choose E2E and create an empty spec.
Add scripts to package.json:
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"e2e:ci": "start-server-and-test start http://localhost:3000 cypress:run"
}
}
start-server-and-test (or wait-on plus your server) is the pattern that starts the app, waits for readiness, then runs headless—critical for CI, discussed later.
Option C: Dockerized runner (headless in CI or air-gapped)
For reproducible CI images, use the official Cypress Docker images that bundle browsers and system libraries. A minimal pattern: install deps in the image, copy the repo, run cypress run with video/Screenshot off if you need speed. In GitHub Actions, the cypress-io/github-action image often provides the same result with less maintenance.
Production insight: Lock Cypress version and the Cypress binary in lockfiles; upgrades should be a deliberate PR with a small “smoke” run on CI, not a silent patch-day surprise.
Configuration essentials
A typical cypress.config.ts for modern apps looks like the following. Adjust baseUrl, timeouts, and viewport to match your product.
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{ts,js}',
supportFile: 'cypress/support/e2e.ts',
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10_000,
requestTimeout: 15_000,
responseTimeout: 15_000,
// retries: { runMode: 2, openMode: 0 }, // optional in CI for known infra flakes
},
});
baseUrl: Lets you writecy.visit('/')instead of full URLs; keeps specs portable between localhost and preview hosts via env overrides.- Timeouts: Raise only when the app has verifiably slow paths; default inflation hides real performance regressions.
- Retries: Controversial. Some teams enable CI-only retries to absorb infra noise; others ban retries to force fixing root causes. Document the policy.
Environment variables
// cypress.config.ts (fragment)
e2e: {
env: {
API_URL: 'http://localhost:4000',
},
setupNodeEvents(on, config) {
// merge process.env, inject secrets from CI, etc.
return config;
},
},
In tests, read with Cypress.env('API_URL'). In CI, inject test credentials via GitHub encrypted secrets, never hard-code.
Performance tips
Before the deep architecture dive: if the suite is slow, nobody trusts it, and you will fight for CI budget forever. I am biased toward turning video off in CI unless I am actively debugging a failure streak—re-enable it for that branch when you need the recording. Otherwise, splitting specs and caching node_modules properly usually moves wall-clock time more than micro-optimizing a single it block.
- Test fewer journeys deeply rather than “everything, shallowly.”
- Prefer
cy.requestorcy.sessionfor auth when the UI is not under test in that file. - Turn video off in CI for speed, enable only on failure if your version supports the pattern, or use artifacts selectively.
- Split specs to parallelize on workers (Cloud) or matrix jobs in GitHub Actions. Avoid a single 40-minute
spec. - Cache
node_modulesin CI, but do not cachenode_modulesblindly across Node major upgrades—npm ciis usually the safer anchor.
Production insight: E2E slowness often tracks data volume. Seed tiny datasets for lists and use intercepts to cap payload sizes, unless you are explicitly testing virtualization at scale.
Architecture: why Cypress “feels” different
Unlike classic WebDriver (remote process driving a detached browser), Cypress historically colocated the test driver next to your app in the browser (with product evolution and Cloud features layered on top). Consequences for day-to-day work:
- A single command queue serializes test steps; Cypress retries assertions until a timeout, similar in spirit to Playwright’s auto-waiting. That is why a bare
cy.getoften “just works” when the UI renders late—within limits. - The application under test and the test code share timing constraints. Async bugs usually come from mixing raw Promises with Cypress commands, or from stale application state between tests, not from WebDriver’s classic “lost synchronization” class of bugs—though flakiness still exists.
flowchart LR
subgraph browser["Browser window"]
app["App under test"]
cypress["Cypress command queue"]
end
spec["Test spec (commands)"] --> cypress
cypress --> app
app -->|DOM / XHR| cypress
Rule of thumb: Prefer chainable cy.* commands; when you need async logic, wrap it with cy.then() and keep side effects idempotent for retries.
If this section feels late, that is intentional: you already have config and a rough sense of what makes suites heavy; now the queue model is easier to map to the pain you have actually felt in the runner.
Your first E2E test
Create cypress/e2e/smoke/home.cy.ts:
/// <reference types="cypress" />
describe('Home page', () => {
it('renders the hero', () => {
cy.visit('/');
cy.get('[data-cy="hero-title"]').should('be.visible').and('contain', 'Welcome');
});
});
Why the assertion chain: should retries until the timeout, absorbing render latency without cy.wait(1000)—a classic anti-pattern covered later.
Support file (cypress/support/e2e.ts) should import commands and global before/after hooks if needed:
// cypress/support/e2e.ts
import './commands';
Production insight: The first spec should be boring and stable. If the landing page is redesigned weekly, do not hang the org’s signal-to-noise on that test—start with a route that changes rarely (login, settings shell).
Selectors: data-cy and data-testid
The discipline
| Strategy | Pros | Cons |
|---|---|---|
data-cy | Explicitly “for tests”, unlikely to be removed by accident | Need lint rules so devs do not remove |
data-testid (Testing Library) | Ecosystem alignment for RTL and Cypress | Name collision if both overlap |
id / name | Sometimes stable on forms | Often omitted or dynamic |
| CSS classes / structure | Tempting early | Brittle on refactors |
We had a long, surprisingly heated thread once about data-cy versus data-testid; the “right” answer was less about purity and more about one convention and lint/review stickiness. I still default to data-cy when the team only touches Cypress, but I will happily align with Testing Library if the component tests already own data-testid.
Team agreement: pick data-cy or data-testid, not both, and add ESLint or code review rules. Example with data-cy:
// App.tsx (React—any framework applies)
export function LoginForm() {
return (
<form data-cy="login-form">
<input data-cy="email" type="email" name="email" autoComplete="username" />
<input data-cy="password" type="password" name="password" autoComplete="current-password" />
<button data-cy="submit" type="submit">Sign in</button>
</form>
);
}
Spec:
it('submits login', () => {
cy.visit('/login');
cy.get('[data-cy="email"]').type('[email protected]');
cy.get('[data-cy="password"]').type('hunter2{enter}'); // {enter} submits when wired
cy.get('[data-cy="login-form"]').submit();
cy.url().should('include', '/app');
});
Anti-patterns to ban in review: cy.get('button').eq(2), cy.get('div > div > span'), copying minified class names, or contains on i18n text that flips with locale. If you need text, scope it: cy.get('[data-cy=hero]').contains('Welcome') after product confirms copy stability.
cy.intercept: the network control plane
Cypress can stub and observe HTTP traffic with cy.intercept. Use it to:
- Return fixtures for list endpoints, keeping tests fast and hermetic.
- Force 4xx/5xx to verify error UI and retry messaging.
- Delay responses to test loading and skeletons.
- Assert on request bodies and headers for high-value writes.
Baseline: static JSON
it('shows posts from a stub', () => {
cy.intercept('GET', '/api/posts', {
statusCode: 200,
body: [{ id: 1, title: 'Hello' }],
}).as('getPosts');
cy.visit('/posts');
cy.wait('@getPosts');
cy.get('[data-cy=post-list]').should('contain', 'Hello');
});
cy.wait('@getPosts') synchronizes the test to one completed response, replacing arbitrary cy.wait(500) calls.
Error path and headers
it('renders the error state', () => {
cy.intercept('GET', '/api/user', (req) => {
req.reply({
statusCode: 500,
body: { error: 'db_down' },
headers: { 'content-type': 'application/json' },
});
}).as('user');
cy.visit('/account');
cy.wait('@user');
cy.get('[data-cy=error-banner]').should('be.visible');
});
GraphQL and POST bodies
For GraphQL over HTTP, intercept the POST to /graphql and match operationName in the request body. Keep helpers DRY in cypress/support/intercepts.ts so 20 tests do not duplicate 40-line matchers.
Production insight: If your GraphQL client batches queries, a naive intercept can fire more than once; prefer per-operation handling or split tests by feature flag. Log (req) => req handlers in short-lived debug runs only; noisy logging collapses signal in CI.
Fixture files
For large payloads, use cy.fixture:
it('lists countries from fixture', () => {
cy.fixture('countries.json').then((countries) => {
cy.intercept('GET', '/api/countries', countries).as('countries');
});
cy.visit('/register');
cy.wait('@countries');
cy.get('[data-cy=country-select] option').should('have.length.greaterThan', 1);
});
Store fixtures under cypress/fixtures/ and version-control them. Treat them like test doubles, not a second source of truth for production data.
Authentication patterns
API login in beforeEach (fast, stable)
When you can POST credentials to an auth endpoint, avoid driving the UI every time; still keep at least one UI login test to ensure the form itself works.
// cypress/support/auth.ts
export function loginViaApi() {
cy.request('POST', `${Cypress.env('API_URL')}/auth/login`, {
email: '[email protected]',
password: Cypress.env('E2E_PASSWORD'),
}).then((resp) => {
expect(resp.status).to.eq(200);
// Example: if your app uses localStorage for tokens (adjust to your app!)
cy.window().then((win) => {
win.localStorage.setItem('access_token', resp.body.token);
});
});
}
Security: Never log secrets; rotate E2E-only users regularly; scope the account to a sandbox tenant. In GitHub Actions, set CYPRESS_E2E_PASSWORD from secrets, not the repository default env.
cy.session (cache login across specs)
Cypress 10+ cy.session is ideal for reusing a stable session without repeating heavy UI flows:
beforeEach(() => {
cy.session('e2e-user', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('[email protected]');
cy.get('[data-cy=password]').type(Cypress.env('E2E_PASSWORD')!);
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/app');
});
});
cy.session validates by re-checking a callback you provide (optional) to detect expired tokens—read the official docs for your Cypress version, as the API has evolved.
Anti-pattern: cy.request + manual cookie juggling for many domains in cross-origin iframes—gets brittle fast. Prefer a single e2e-friendly auth domain for tests, or re-architect to token injection the app team supports.
Custom commands
Encapsulate repeated flows in cypress/support/commands.ts to keep specs readable and consistent.
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
/** Logs in with default E2E user (UI) */
loginUI(): Chainable<void>;
}
}
}
Cypress.Commands.add('loginUI', () => {
cy.visit('/login');
cy.get('[data-cy=email]').clear().type('[email protected]');
cy.get('[data-cy=password]').type(Cypress.env('E2E_PASSWORD')!);
cy.get('[data-cy=submit]').click();
cy.get('[data-cy=app-shell]', { timeout: 20_000 }).should('be.visible');
});
// Ensure TS picks up the global augmentation
export {};
Guidelines: one command, one user intent; return chainables; avoid hidden global state unless cleared in afterEach when a test opts in.
Production insight: Custom commands that hide assertions can surprise readers—name them honestly (assertLoggedIn) or document what they check.
Test organization strategies
A scalable layout:
cypress/
e2e/
auth/
checkout/
admin/
fixtures/
support/
commands.ts
e2e.ts
intercepts.ts
downloads/ (if you test file export)
Tagging and splitting: use separate files (fast to parallelize) or grep tags if your runner supports it. A common CI pattern is a @smoke suite on every PR, full nightly on main against staging, and release-candidate runs before prod.
| Strategy | When it helps | Trade-off |
|---|---|---|
| Page objects (classes mapping UI) | Large QA teams, heavy reuse | Indirection, higher boilerplate in JS tests |
| Specs close to product areas with small helpers | Devtools-first teams, faster refactors | Need discipline to avoid copy-paste |
Feature folders in e2e/ | Mirrors app architecture | Slight import path overhead |
Cypress is JavaScript-friendly; many successful teams use “lightweight” helpers over heavy OOP page objects, but either works if the team is consistent.
CI/CD: GitHub Actions (complete example)
A typical workflow builds or serves the app, runs Cypress, and uploads screenshots/videos on failure.
# .github/workflows/cypress-e2e.yml
name: Cypress E2E
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
concurrency:
group: cypress-${{ github.ref }}
cancel-in-progress: true
jobs:
cypress:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
run: npm run build
env:
NEXT_PUBLIC_API_URL: http://127.0.0.1:4010
- name: Start app and run Cypress
uses: cypress-io/github-action@v6
with:
start: npm start
wait-on: 'http://127.0.0.1:3000'
wait-on-timeout: 120
browser: chrome
headed: false
env:
CYPRESS_E2E_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
CYPRESS_BASE_URL: http://127.0.0.1:3000
CYPRESS_API_URL: http://127.0.0.1:4010
- name: Upload Cypress artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
path: |
cypress/screenshots
cypress/videos
Notes for real repositories:
- If you need a backend, add service containers or a docker-compose step; keep ports aligned with
CYPRESS_API_URL. - Record to Cypress Cloud (optional) by setting
CYPRESS_RECORD_KEYandrecord: truein the action’swithblock—gives parallelization and a timeline view, at a cost. - For flaky infrastructure, add idempotent database seeds and isolated tenants per test file.
Production insight: Pin cypress-io/github-action to a major version, but let patch updates in; the ecosystem moves quickly and security patches matter for Node actions too.
flowchart TB pr["Pull request"] --> install["npm ci + build"] install --> start["Start app (localhost)"] start --> cypress["cypress run"] cypress -->|pass| done["Green check"] cypress -->|fail| art["Upload screenshots / videos"]
Best practices and anti-patterns
I am allergic to nth-of-type chains in E2E; they look clever until a designer adds a wrapper div and your suite turns red. Stable selectors (data-cy / data-testid) are the closest thing to boring progress you get in this layer—boring is the goal.
Readiness should come from DOM or network evidence, not from cy.wait(500) “to be safe.” If you are synchronizing, use intercept aliases or an assertion that Cypress can retry. Fixed sleeps are how you hide race conditions and train everyone to run CI slower than necessary.
Setup needs to be idempotent at the test or file level. “Fixing” order-dependent tests with it.only locally is a fake green. The fix is to reset data or re-navigate to a clean route, not to hope execution order in CI.
Seeds from an API or a tiny SQL task beat “someone manually clicked around in staging first.” The latter does not scale and it will fail at 2 a.m. when you care most.
Quarantine is honest: if a test is a known flake, own it. Silent retries on main forever is how you learn to distrust the whole pipeline. I would rather a quarantined test with a ticket than a green check that means nothing.
Timeouts deserve the same discipline: if you keep raising defaultCommandTimeout, something is slow or wrong (selector, missing intercept, bad seed). Bumping the timeout to green the build without fixing the cause is how performance regressions ship quietly.
Skips need ownership: a @skip in a shared suite without a work item is often permanent. Treat skipped tests with the same energy as open defects you would not leave on a customer-facing issue board.
Third-party network in E2E is a liability: rate limits, compliance questions, and non-determinism. Stub at the HTTP boundary you own—backend mock, intercept, or dedicated fake—unless the point of the test is explicitly to exercise a live integration (and then do it in a controlled job, not on every PR).
Troubleshooting common issues
Q: I see Timed out retrying: cy.get...—where do I start?
A: Usually the selector is wrong, a feature flag differs from local, or data never loaded. I reach for the Cypress command log first, and when I am truly stuck, a single cy.debug() in headed mode. Confirm seeds and env match CI; baseUrl typos are embarrassingly common.
Q: cy.wait('@alias') never resolves. Why?
A: The intercept is not matching method or path. Log actual request URLs, widen the matcher with intent (not with **), and watch for SPA path prefixes—/app/api/... versus /api/... trips people constantly.
Q: Passes in cypress open, fails headless. Now what?
A: Timezone, locale, GPU, or a race that only shows under load. Set timezone in the container, reduce parallel job pressure for a test run, and add explicit DOM gates you can assert. “Works in headed” is not a spec; it is a hint.
Q: Sporadic 401s mid-suite.
A: Often stale cy.session, clock skew, or a token that expired between steps. Revisit your session validation callback; ensure CI stays NTP-synced if your auth is time-sensitive.
Debugging checklist: run headed with DevTools, enable Cypress command log, verify baseUrl, and compare local vs CI environment variables. If your app is Next.js or Nuxt, confirm SSR vs client-only routes—Cypress can only assert what the browser really renders for that navigation.
Production insight: When failure artifacts show blank screenshots, the browser often crashed OOM; reduce parallel jobs or downscale test viewport to mirror real user devices, not 4K admin monitors.
Advanced patterns (brief, actionable)
Component testing (Cypress CT)
Cypress can mount isolated components (React, Vue) with a dev server, separate from E2E. It is not a drop-in replacement for Jest/RTL—it shines when you want to exercise wiring, styles, and events in a real browser. Keep component tests for stateful widgets; leave pure functions to unit tests for speed.
Visual regression (plugin ecosystem)
Percy, Chromatic, or self-hosted pixel diff tools integrate as CI steps. E2E + visual is powerful, but flaky if animations and fonts are not pinned. Stabilize by disabling animations in test builds, using consistent system fonts, and masking dynamic regions (timers, avatars, ads).
Production insight: teams often run component-level visual tests for design systems, and a handful of E2E visual checks for the shell and checkout only.
Real project example: “shop” E2E layout
A realistic, maintainable project might structure journeys like this (names illustrative):
cypress/e2e/
smoke/
can-load-home.cy.ts
auth/
can-login-and-logout.cy.ts
purchase/
can-add-to-cart-and-checkout.cy.ts (tagged @smoke)
admin/
can-export-orders.cy.ts
cypress/support/
commands.ts # cy.loginUI, cy.seedCart
intercepts.ts # common GET /api/products, etc.
smoke/is kept minimal and fast; PR gating only runs this folder in tight budgets.purchase/is your highest business value and gets the most intercepts to avoid flaking on inventory microservices in lower envs.admin/may require separate credentials; isolate roles in separatecy.sessionkeys.
Data setup: a small tasks plugin or a one-call POST /e2e/reset in non-prod keeps tests independent. If your platform forbids that endpoint, your tests will be slower and more stateful—make that tradeoff explicit with stakeholders.
Conclusion
Cypress E2E is most valuable when you treat it as a product-quality signal, not a coverage vanity metric. Build selectors that survive design iterations, use cy.intercept to own your network contracts in tests, script auth the boring way, and run lean smoke in CI with fast feedback on PRs. Pair this with unit tests for logic and observability in production, and you have a defensible test pyramid for modern web apps.
For deeper product coverage, continue with the [Cypress Complete Guide | E2E Testing, Automation & CI/CD](/en/blog/cypress-complete-guide/ and the [Playwright Complete Guide | E2E Testing](/en/blog/playwright-complete-guide/. The [Vitest Complete Guide](/en/blog/vitest-complete-guide/ layers fast unit and component tests beneath your Cypress suite.
Related reading
- [Playwright E2E Testing | Automation· Locators](/en/blog/playwright-e2e-testing-guide/
- [Playwright Complete Guide | E2E Testing](/en/blog/playwright-e2e-testing-guide/
- [Cypress Complete Guide | E2E Testing, Automation & CI/CD](/en/blog/cypress-complete-guide/
Frequently asked questions (in depth)
Why avoid cy.wait(milliseconds)? Fixed delays either mask a race (still flaky) or slow CI. Prefer waiting on an observable: an element, a route alias, or a Cypress query that naturally retries. If you must debounce, fix the app’s test hook or add a debug-only data-cy=ready gate rather than a sleep.
Should Cypress call production APIs? As a default, no. E2E should target isolated data and predictable backends. If you need one end-to-end contract test against a staging stack that mirrors prod, do it in a controlled job, with SLAs, rate limits, and monitoring—not on every PR.
How do I keep tests from depending on each other? Each spec should start from a known state. Seed with API helpers, or navigate from a clean cy.visit path. If order creeps in, the suite will be impossible to parallelize and painful to debug.
What is the “right” number of E2E tests? Enough to cover critical money paths and a few unhappy paths, not so many that CI becomes the bottleneck. The exact number is a function of team size, release cadence, and defect history—track escape defects and size the suite to prevent repeats.
How do I compare against Playwright in a pilot? Take one user journey, implement it in both tools, measure authoring time, CI runtime, flakiness over 100 runs, and developer preference in code review. Numbers beat slogans; your stack constraints may differ from generic benchmarks online.
Where do I put shared intercepts? Centralize in cypress/support/intercepts.ts and import from specs or a before hook. Duplicated intercepts rot quickly when the API adds pagination or headers you forgot in three tests but not the fourth.