Once the Cypress basics are comfortable, the biggest improvements usually come from learning how Cypress handles retryability, browser network traffic, direct API calls, reusable commands, Node-side tasks, environment configuration, authentication state and continuous integration. These capabilities let QA engineers build faster, more deterministic end-to-end suites while avoiding common anti-patterns such as fixed waits, repeated UI setup and uncontrolled dependencies on backend data.

Advanced Cypress: the main ideas

Advanced Cypress is less about learning more commands and more about understanding where code runs, what Cypress retries and which layer should own a particular test responsibility.

Retry-aware assertions

Use Cypress queries and assertions so dynamic applications can reach the expected state without arbitrary sleeps.

Network control

Observe, wait for, modify or stub browser requests with cy.intercept().

Direct API access

Use cy.request() for API checks and fast test-data setup without navigating the UI.

Browser ↔ Node bridge

Use cy.task() for operations that belong outside the browser, such as database scripts or filesystem work.

Reusable sessions

Cache authentication state with cy.session() instead of logging in through the UI before every test.

CI execution

Run deterministic Cypress suites automatically on pushes, pull requests or deployment pipelines.

Retryability first: .should() vs .then()

Cypress applications often re-render asynchronously. Understanding the difference between .should() and .then() is therefore one of the most important advanced concepts.

Use .then() when you want to work with the yielded value once

cy.get('[data-cy="card"]') .then(($cards) => { expect($cards.eq(0)).to.contain.text('milk') expect($cards.eq(1)).to.contain.text('bread') expect($cards.eq(2)).to.contain.text('juice') })

The callback receives the current subject and executes once. If the DOM is temporarily in the wrong state and then becomes correct a moment later, the assertions inside .then() are not automatically retried.

Use .should(callback) for retryable assertions

cy.get('[data-cy="card"]') .should(($cards) => { expect($cards.eq(0)).to.contain.text('milk') expect($cards.eq(1)).to.contain.text('bread') expect($cards.eq(2)).to.contain.text('juice') })

Cypress retries the relevant query chain and callback assertions until they pass or the command timeout is reached.

Rule of thumb: if you are asserting against dynamic application state, prefer .should(). Use .then() when you genuinely need one-time transformation, branching or access to a yielded value.

Simple built-in assertion syntax is still excellent

cy.get('[data-cy="status"]') .should('be.visible') .and('contain.text', 'Ready')

Cypress bundles Chai assertion capabilities, so you can mix readable built-in assertion strings with callback-style expect() assertions when a scenario needs more control.

Understand Cypress retry boundaries

Cypress retries queries and assertions; commands that change application state generally execute once. This distinction helps avoid flakiness.

OperationTypical behaviourExample
QueryRetried as part of a query chain.cy.get(), .find(), .contains()
AssertionRetried until passing or timing out..should(), .and()
ActionExecuted once after actionability checks succeed..click(), .type()
One-time callbackRuns once with the yielded subject..then()

Prefer state conditions over fixed waits

// Avoid cy.wait(3000) cy.get('[data-cy="list"]').should('not.exist') // Prefer waiting for the application event that matters cy.intercept('GET', '/api/lists*').as('getLists') cy.visit('/board/1') cy.wait('@getLists') cy.get('[data-cy="list"]').should('not.exist')

Fixed waits are occasionally useful while debugging, but they are a weak synchronisation strategy because they are either longer than necessary or still too short under slower conditions.

Intercept browser API requests

cy.intercept() lets Cypress spy on or modify HTTP traffic generated by the application in the browser. It is one of the strongest tools for synchronisation, behavioural assertions and edge-case testing.

Wait for a request instead of waiting for time

cy.intercept( 'GET', '/api/lists?boardId=1' ).as('getLists') cy.visit('/board/1') cy.wait('@getLists') .its('response.statusCode') .should('eq', 200)

Register the intercept before the request can occur

If page load triggers the request, define the intercept before cy.visit(). If a button click triggers it, define the intercept before .click().

cy.intercept({ method: 'DELETE', url: '/api/lists/*', }).as('deleteList') cy.contains('Delete list').click() cy.wait('@deleteList') .its('response.statusCode') .should('eq', 200)

Match dynamic URLs with glob patterns

cy.intercept('DELETE', '/api/lists/*') cy.intercept('GET', '/api/boards/**') cy.intercept({ method: 'GET', pathname: '/api/cards' })

Use the smallest matcher that expresses the intent. Overly broad intercepts can accidentally capture unrelated traffic and make tests difficult to diagnose.

Assert on intercepted request and response data

cy.wait('@alias') yields an interception object containing request and response information.

cy.intercept('POST', '/api/boards').as('createBoard') cy.get('[data-cy="new-board-input"]') .type('Release testing{enter}') cy.wait('@createBoard') .then(({ request, response }) => { expect(request.body.name).to.eq('Release testing') expect(response.statusCode).to.eq(201) expect(response.body).to.have.property('id') })

This is especially useful when the UI only proves part of a business interaction. The browser can confirm the user-visible result while the intercepted request verifies the frontend sent the correct payload.

Do not turn every UI test into a network-contract test. Assert request details only when they are important to the behaviour being tested. Excessive network assertions can tightly couple UI tests to implementation details.

Mock and stub network responses

Interception becomes even more powerful when Cypress provides the response instead of the real backend. This is commonly called stubbing or network mocking.

Simulate an empty state

cy.intercept('GET', '/api/boards', { statusCode: 200, body: [], }) cy.visit('/') cy.contains('Create your first board') .should('be.visible')

Provide controlled application data

cy.intercept('GET', '/api/boards', { statusCode: 200, body: [ { id: 11, name: 'TAU board', starred: true, }, ], })

Return data from a fixture

cy.intercept('GET', '/api/boards', { fixture: 'boards.json', })

Simulate a server error

cy.intercept('POST', '/api/boards', { statusCode: 500, body: { message: 'Unexpected server error', }, }) cy.visit('/') cy.get('[data-cy="new-board-input"]') .type('My board{enter}') cy.get('[data-cy="error-message"]') .should('be.visible') .and('contain.text', 'server error')

Failure states are often difficult or dangerous to reproduce against a real backend. Stubbing makes them deterministic and easy to exercise.

When mocking helps — and when it does not

ScenarioMocking useful?Why
Empty-state UIYesEasy to control without deleting shared data.
500 / 503 response handlingYesDeterministic failure without damaging backend services.
Slow-response loading stateYesAllows controlled delay and loading verification.
Rare edge-case payloadYesCreates data that may be hard to produce through normal workflows.
True frontend-backend integrationNo, not exclusivelyA mocked response cannot prove the real API contract still works.
Production API compatibilityNoRequires integration, contract or environment-level testing against the real service.

Keep a clear distinction between frontend behaviour with controlled data and end-to-end integration with real services. Both are useful, but they provide different evidence.

API testing with cy.request()

cy.request() sends an HTTP request directly from Cypress' Node process. It can be used for API tests, health checks, authentication and fast test-data setup.

Create data directly

cy.request({ method: 'POST', url: '/api/boards', body: { name: 'Created by API', }, }) .its('status') .should('eq', 201)

Make richer API assertions

cy.request({ method: 'GET', url: '/api/boards', headers: { Accept: 'application/json', }, }).then((response) => { expect(response.status).to.eq(200) expect(response.body).to.be.an('array') expect(response.body[0].id).to.be.a('number') })

Test expected error responses

cy.request() fails automatically on non-2xx/3xx status codes by default. For a negative API test, disable that behaviour explicitly.

cy.request({ method: 'GET', url: '/api/boards/not-found', failOnStatusCode: false, }).then((response) => { expect(response.status).to.eq(404) expect(response.body).to.have.property('message') })

cy.request() and cy.intercept() are different tools

This distinction is easy to miss:

  • cy.intercept() observes or stubs requests that the application makes from the browser.
  • cy.request() sends a request from the Cypress Node process.
  • A cy.request() call therefore does not pass through cy.intercept().
  • cy.request() calls also do not appear in the browser DevTools Network tab.
Choose the tool by question: “Did my frontend make the correct request?” → cy.intercept(). “Does this API endpoint return the expected result?” → cy.request().

Use APIs for test setup

One of the best uses of direct requests in end-to-end testing is preparing state without navigating through unrelated UI flows.

Slower UI setup

cy.visit('/') cy.contains('Create board').click() cy.get('[data-cy="board-name"]').type('Regression') cy.contains('Save').click() cy.contains('Regression').click() // Now the actual test can begin...

Faster API setup

beforeEach(() => { cy.request('POST', '/api/test/reset') cy.request('POST', '/api/boards', { name: 'Regression', }) }) it('adds a card to an existing board', () => { cy.visit('/board/1') // Test only the behaviour that matters here. })

This keeps the UI test focused on the UI behaviour under test and reduces execution time.

Isolation still matters. Avoid designing specs where one it() block creates data that a later test requires. Each test should normally be runnable independently.

Custom commands

Custom commands are useful when the suite contains a repeated Cypress workflow that deserves a clear domain-oriented name.

A simple selector helper

Cypress.Commands.add('getByCy', (value) => { return cy.get(`[data-cy="${value}"]`) }) // Usage cy.getByCy('add-list-input') .type('Regression{enter}')

A workflow-oriented command

Cypress.Commands.add('createBoardByApi', (name) => { return cy.request('POST', '/api/boards', { name }) }) // Usage cy.createBoardByApi('Release validation')

A custom login command

Cypress.Commands.add('login', (email, password) => { cy.session([email], () => { cy.request('POST', '/api/login', { email, password, }).then(({ body }) => { window.localStorage.setItem('authToken', body.token) }) }) })

Prefer commands that express a reusable testing capability. Avoid wrapping every native Cypress command simply to make the framework look different.

Add useful command logging

Custom commands can create their own entries in the Cypress Command Log.

Cypress.Commands.add('getByCy', (value) => { Cypress.log({ name: 'getByCy', message: value, consoleProps: () => ({ selector: `[data-cy="${value}"]`, }), }) return cy.get(`[data-cy="${value}"]`, { log: false, }) })

Good custom logs improve debugging. They should add useful context rather than simply duplicating built-in logging.

Custom queries: a modern retryable alternative

If a reusable abstraction is fundamentally about finding DOM state and should behave like Cypress' built-in retryable queries, consider a custom query instead of a normal custom command.

Cypress.Commands.addQuery('getByCyQuery', function (value) { return () => { return Cypress.$(`[data-cy="${value}"]`) } })

Custom queries are synchronous, retryable and expected to be idempotent. They are useful when you need a selector abstraction with native-style retryability.

Keep abstractions small. Cypress tests are already readable JavaScript. Too many custom commands, queries or page-object layers can make a suite harder to understand than the product behaviour itself.

Move work from the browser to Node with cy.task()

Cypress test code runs in the browser, but some operations belong in Node.js: filesystem access, database scripts, external processes or other trusted test-environment utilities.

cy.task() provides a bridge between the browser-side test and a task registered inside setupNodeEvents().

Register a task

import { defineConfig } from 'cypress' import { seedDatabase } from './scripts/seed-database' export default defineConfig({ e2e: { setupNodeEvents(on, config) { on('task', { seedDatabase(data) { return seedDatabase(data) }, }) return config }, }, })

Call it from a test

cy.task('seedDatabase', { boards: [], cards: [], lists: [], users: [], })

Pass several values inside one object

cy.task('createTestUser', { email: 'qa@example.com', role: 'admin', enabled: true, })

The argument must be serialisable. If you need multiple logical parameters, package them into one object.

Always return something

on('task', { log(message) { console.log(message) // Explicitly return null if no useful value is needed. return null }, })

A task handler must not finish with undefined. Returning null clearly signals successful handling when no result is needed.

Good and bad uses of cy.task()

UseRecommendationReason
Seed a dedicated test databaseGoodFast, deterministic environment setup.
Read a file that may not existGoodNode filesystem access is appropriate.
Run an approved test utilityGoodSuitable browser-to-Node boundary.
Persist small Node-side state between specsSometimes usefulCan help specialised workflows but should remain predictable.
Start the application web serverAvoidServer lifecycle should normally be managed outside the test command.
Hide normal application behaviour behind tasksAvoidTests become unrealistic and harder to understand.

Switch configuration between environments

A Cypress project often runs against local, QA, staging and other environments. Configuration should be easy to switch without editing test files.

Keep a default base URL

import { defineConfig } from 'cypress' export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', setupNodeEvents(on, config) { return config }, }, })

Override configuration from the command line

npx cypress run \ --config baseUrl=https://qa.example.com

Use a simple environment selector

npx cypress run --env target=staging

The resolved value is available through the Cypress configuration/environment mechanisms and can also be inspected in project settings.

Environment-specific configuration files

For a small internal project, separate versioned files can be a readable way to map an environment name to public configuration.

cypress/ └── config/ ├── local.json ├── qa.json └── staging.json

Example qa.json

{ "baseUrl": "https://qa.example.com", "apiUrl": "https://api.qa.example.com" }

Resolve the environment in setupNodeEvents()

import fs from 'node:fs' import path from 'node:path' import { defineConfig } from 'cypress' export default defineConfig({ e2e: { setupNodeEvents(on, config) { const target = config.env.target || 'local' const filePath = path.resolve( 'cypress', 'config', `${target}.json` ) const environmentConfig = JSON.parse( fs.readFileSync(filePath, 'utf8') ) config.baseUrl = environmentConfig.baseUrl config.env.apiUrl = environmentConfig.apiUrl return config }, }, })
Important: return the modified config object from setupNodeEvents() so Cypress receives the changes.

Do not store secrets in environment JSON files

Environment selection and secret management are separate concerns. Public hosts, feature settings and test labels may be safe in versioned configuration. Passwords, API keys and tokens should normally come from the operating system or CI secret store.

export default defineConfig({ env: { apiUrl: 'https://api.qa.example.com', apiKey: process.env.QA_API_KEY, }, })

In CI, configure QA_API_KEY as a protected secret rather than committing it to the repository.

Current Cypress configuration note

Modern Cypress supports JavaScript and TypeScript config files in both ESM and CommonJS projects. Match your import/export or require/module.exports syntax to the project's module system rather than mixing styles accidentally.

Authentication: avoid UI login before every test

Logging in through the UI is valuable when the login journey itself is the behaviour under test. It is wasteful when login is only a prerequisite for hundreds of unrelated scenarios.

The repetitive approach

beforeEach(() => { cy.visit('/login') cy.get('[data-cy="email"]').type('qa@example.com') cy.get('[data-cy="password"]').type('secret') cy.get('[data-cy="login-submit"]').click() }) it('shows private boards', () => { cy.visit('/') ... })

The code may be wrapped in a custom command, but the browser still performs the same expensive login flow before every test.

Cache authentication with cy.session()

cy.session() caches and restores cookies, localStorage and sessionStorage so the suite can reuse a known authentication state.

UI-based session setup

Cypress.Commands.add('login', (email, password) => { cy.session( ['user', email], () => { cy.visit('/login') cy.get('[data-cy="email"]').type(email) cy.get('[data-cy="password"]').type(password) cy.get('[data-cy="login-submit"]').click() cy.url().should('include', '/dashboard') } ) })

Faster API-based setup

Cypress.Commands.add('loginByApi', (email, password) => { cy.session( ['api-user', email], () => { cy.request('POST', '/api/login', { email, password, }).then(({ body }) => { window.localStorage.setItem( 'authToken', body.token ) }) } ) })

For most tests, API-based session setup is faster and avoids coupling every spec to login-page markup.

Validate cached sessions

A cached token can expire or become invalid. Add a validate() function so Cypress checks the session before reusing it.

Cypress.Commands.add('loginByApi', (email, password) => { cy.session( ['api-user', email], () => { cy.request('POST', '/api/login', { email, password, }).then(({ body }) => { window.localStorage.setItem( 'authToken', body.token ) }) }, { validate() { cy.request('/api/me') .its('status') .should('eq', 200) }, cacheAcrossSpecs: true, } ) })

What cacheAcrossSpecs really means

  • The session can be reused by multiple specs during the same Cypress run.
  • The cache exists only for that run on that machine.
  • Parallel CI machines do not share the same in-memory session cache.
  • All specs should use the same shared session helper rather than duplicating slightly different cy.session() definitions.
Session IDs must not contain secrets. Use stable identifiers such as user name, user ID or role — never a password or token.

Test isolation and authentication

With normal Cypress test isolation, browser state is cleared between tests. cy.session() works with that model by restoring the required session data when needed.

A clean authenticated test

beforeEach(() => { cy.loginByApi('qa@example.com', Cypress.env('password')) cy.visit('/') }) it('shows the private board', () => { cy.contains('Private QA board') .should('be.visible') })

Do not assume cy.session() leaves the application page loaded for the next test. Visit the page needed by the test after restoring the session.

GitHub Actions: run Cypress in CI

A Cypress suite becomes much more valuable when it runs automatically. GitHub Actions workflows live under .github/workflows/.

Basic current workflow

name: Cypress E2E on: push: pull_request: jobs: cypress-run: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Cypress run uses: cypress-io/github-action@v7 with: start: npm start wait-on: 'http://localhost:3000' browser: chrome

The official Cypress GitHub Action handles common installation and cache behaviour and can start the application before running Cypress.

Run a focused spec

- name: Cypress smoke tests uses: cypress-io/github-action@v7 with: start: npm start wait-on: 'http://localhost:3000' spec: cypress/e2e/smoke/**/*.cy.ts

Pass CI secrets safely

- name: Cypress run uses: cypress-io/github-action@v7 with: start: npm start wait-on: 'http://localhost:3000' env: CYPRESS_TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

Never place real credentials directly in workflow YAML committed to the repository.

Upload useful failure artifacts

When a CI failure happens, the fastest path to diagnosis is usually a combination of Cypress output, screenshots and application logs.

Upload screenshots after failure

- name: Upload Cypress screenshots if: failure() uses: actions/upload-artifact@v4 with: name: cypress-screenshots path: cypress/screenshots if-no-files-found: ignore

Upload video only if your project records it

- name: Upload Cypress videos if: always() uses: actions/upload-artifact@v4 with: name: cypress-videos path: cypress/videos if-no-files-found: ignore

Video recording is not enabled by default in current Cypress, so only upload videos if your project explicitly records them.

CI strategy beyond one job

A larger suite can use different CI layers instead of running every test on every event.

Pipeline stageExample Cypress scopeGoal
Pull requestFast smoke + changed-area testsGive developers quick feedback.
Merge to mainFull E2E regressionProtect the shared branch.
Deployment to QAEnvironment smoke + integrationValidate deployed configuration and services.
NightlyCross-browser / broader matrixIncrease coverage without slowing every commit.
Release candidateCritical business journeysProvide explicit release confidence.

Build a balanced network-test strategy

Advanced Cypress gives you several ways to test the same feature. Choose the mechanism according to the evidence you need.

QuestionBest starting tool
Did the browser send the correct request?cy.intercept() + alias + request assertions
Does the frontend handle a 500 error?cy.intercept() with a stubbed 500 response
Does the real endpoint return the correct payload?cy.request()
Do I need fast deterministic setup?cy.request() or cy.task()
Does UI + real backend work together?Normal browser flow without stubbing the relevant endpoint
Do I need to simulate an otherwise rare payload?cy.intercept() fixture or static response

Advanced data-management patterns

Many flaky Cypress suites are actually data-management problems. Advanced commands are most valuable when they help each test begin from a known state.

Pattern 1: reset through an approved test API

beforeEach(() => { cy.request('POST', '/api/test/reset') })

Pattern 2: seed through a Node task

beforeEach(() => { cy.task('seedDatabase', { users: [...], boards: [...], }) })

Pattern 3: create only the data the test needs

beforeEach(() => { cy.request('POST', '/api/boards', { name: 'Board under test', }) })

Pattern 4: stub only the dependency relevant to the scenario

cy.intercept('GET', '/api/recommendations', { fixture: 'recommendations/empty.json', })
Avoid state leakage. A suite that only passes in a particular test order is not reliable regression coverage.

Advanced debugging workflow

When an advanced Cypress test fails, debug from the lowest level first rather than immediately adding waits or retries.

  1. Read the command log. Which command actually failed?
  2. Check the yielded subject. Did the selector return what you expected?
  3. Inspect the browser Network panel. Did the frontend call the expected endpoint?
  4. Check intercept registration timing. Was the intercept defined before the request happened?
  5. Inspect the alias result. Request body, response status and response body often reveal the issue.
  6. Check retryability. Is an assertion hidden inside a non-retrying .then()?
  7. Check state setup. Did a prior test or stale database record affect the scenario?
  8. Check session validation. Is the cached authentication state still valid?
  9. Check environment resolution. Is the suite using the intended base URL and credentials?
  10. Only then adjust timeouts. A larger timeout should solve a known latency requirement, not hide uncertainty.

Common advanced Cypress mistakes

MistakeWhy it hurtsBetter approach
Assertions in .then() for dynamic DOM stateNo automatic assertion retry.Use .should(callback).
cy.wait(5000) everywhereSlow and still race-prone.Wait on network calls or observable application state.
Defining cy.intercept() after the actionThe request may already have happened.Register intercept first.
Assuming cy.intercept() catches cy.request()They run through different paths.Use each tool for its intended responsibility.
Mocking every backend requestUI tests no longer prove real integration.Keep a mix of mocked and real integration coverage.
Creating custom commands for every selectorAdds unnecessary abstraction.Use native Cypress queries unless the abstraction adds value.
Using a normal custom command where query retryability is requiredCustom DOM lookup may not behave like a native query.Consider a custom query.
Using cy.task() as a generic escape hatchTests become opaque and unrealistic.Reserve tasks for Node-side concerns.
Committing secrets in config filesSecurity exposure.Inject secrets through CI/OS environment variables.
UI login before every testLarge execution penalty.Use API login + cy.session().
Sharing test state between it() blocksTests become order-dependent.Make each test self-contained.
Running only mocked tests in CIBackend integration regressions escape.Retain real service coverage for critical flows.

A practical advanced Cypress architecture

cypress/ ├── e2e/ │ ├── smoke/ │ ├── regression/ │ ├── api/ │ └── network/ ├── fixtures/ │ ├── boards/ │ └── errors/ ├── support/ │ ├── commands.ts │ └── e2e.ts └── config/ ├── local.json ├── qa.json └── staging.json scripts/ ├── seed-database.ts └── cleanup-test-data.ts .github/ └── workflows/ └── cypress.yml cypress.config.ts

The exact structure is less important than keeping responsibilities obvious: specs describe behaviour, fixtures hold controlled static data, support files contain shared Cypress capabilities and Node-side scripts stay separate from browser test logic.

Example: a robust board-creation test

The following example combines several advanced concepts without making the test unnecessarily complicated.

describe('Board creation', () => { beforeEach(() => { cy.task('seedDatabase', { users: [ { id: 1, email: 'qa@example.com', }, ], boards: [], }) cy.loginByApi('qa@example.com', Cypress.env('password')) }) it('creates a board and sends the correct request', () => { cy.intercept('POST', '/api/boards') .as('createBoard') cy.visit('/') cy.getByCy('new-board-input') .type('Release validation{enter}') cy.wait('@createBoard') .should(({ request, response }) => { expect(request.body.name) .to.eq('Release validation') expect(response.statusCode) .to.eq(201) }) cy.contains('Release validation') .should('be.visible') }) })

What this test proves

  • The test begins with controlled data.
  • Authentication does not waste time on repeated UI login.
  • The intercept exists before the browser triggers the request.
  • The request payload is correct.
  • The backend accepted the request.
  • The final user-visible board appears.

That is useful integration coverage because each assertion contributes different evidence. If the scenario were only about the frontend error component, stubbing the backend would be more appropriate.

Advanced Cypress checklist

  • Dynamic assertions use retryable Cypress patterns where appropriate.
  • .then() is not being used as a substitute for retryable .should() assertions.
  • Hard-coded waits are rare and justified.
  • Network intercepts are registered before the request can happen.
  • Intercept aliases have clear names describing the application event.
  • Network assertions focus on behaviour that matters rather than implementation trivia.
  • Mocked responses cover important empty, error and edge states.
  • Critical frontend-backend flows also have non-mocked integration coverage.
  • cy.request() is used for direct API validation and efficient setup where appropriate.
  • The team understands that cy.request() is not intercepted by cy.intercept().
  • Tests do not depend on state created by previous tests.
  • Custom commands add meaningful domain-level reuse.
  • DOM abstractions that need retryability consider the custom-query API.
  • cy.task() is limited to legitimate Node-side operations.
  • Task arguments are serialisable and handlers return a value or null.
  • Environment hosts are configurable without editing specs.
  • Secrets come from protected environment or CI configuration.
  • Authentication setup is cached with cy.session() when login itself is not under test.
  • Session validation detects expired or invalid authentication.
  • Cross-spec session caching is used only with a shared consistent session helper.
  • CI uses the supported Cypress GitHub Action version.
  • Failure artifacts are preserved where they help diagnosis.
  • PR jobs remain fast enough to provide useful developer feedback.
  • Broader regression and cross-browser execution happen at appropriate pipeline stages.

Advanced Cypress cheat sheet

GoalExample
Retry multiple assertions.should(($el) => { expect(...) })
Use yielded value once.then((value) => { ... })
Spy on browser requestcy.intercept('GET', '/api/items')
Alias request.as('getItems')
Wait for requestcy.wait('@getItems')
Assert response status.its('response.statusCode').should('eq', 200)
Stub empty responsecy.intercept('GET', '/api/items', [])
Stub fixture{ fixture: 'items.json' }
Stub error{ statusCode: 500 }
Direct HTTP requestcy.request('GET', '/api/items')
Test error statusfailOnStatusCode: false
Add custom commandCypress.Commands.add('name', fn)
Add retryable custom queryCypress.Commands.addQuery('name', fn)
Run Node-side taskcy.task('seedDatabase', data)
Register taskon('task', { seedDatabase })
Modify runtime configsetupNodeEvents(on, config) { ... return config }
Override base URL--config baseUrl=https://qa.example.com
Pass environment selector--env target=qa
Cache login sessioncy.session(id, setup, options)
Share session across specscacheAcrossSpecs: true
Run headlessnpx cypress run
GitHub Actionscypress-io/github-action@v7

Key takeaways

  • Understanding Cypress retryability is more valuable than adding arbitrary waits.
  • .should(callback) is retryable; assertions inside .then() execute once.
  • cy.intercept() is the main tool for observing and controlling browser network traffic.
  • Network mocking makes frontend edge cases deterministic but does not replace real integration coverage.
  • cy.request() is excellent for API testing and fast test-data preparation.
  • cy.request() runs from Node and does not pass through cy.intercept().
  • Custom commands should simplify meaningful repeated workflows, not hide every native Cypress command.
  • Custom queries are useful when a reusable DOM lookup needs Cypress-style retryability.
  • cy.task() bridges Cypress tests to trusted Node-side utilities.
  • Environment configuration should be flexible while credentials stay outside source control.
  • cy.session() can remove enormous repeated-login overhead in large suites.
  • A CI pipeline should preserve fast feedback while retaining enough real integration and browser coverage to detect meaningful regressions.

Useful links