cypress-testing
Use this skill when writing Cypress e2e or component tests, creating custom commands, intercepting network requests, or integrating Cypress in CI. Triggers on Cypress, cy.get, cy.intercept, cypress component testing, custom commands, fixtures, cypress-cucumber, and any task requiring Cypress test automation.
engineering cypresse2etestingcomponent-testingautomationWhat is cypress-testing?
Use this skill when writing Cypress e2e or component tests, creating custom commands, intercepting network requests, or integrating Cypress in CI. Triggers on Cypress, cy.get, cy.intercept, cypress component testing, custom commands, fixtures, cypress-cucumber, and any task requiring Cypress test automation.
cypress-testing
cypress-testing is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Writing Cypress e2e or component tests, creating custom commands, intercepting network requests, or integrating Cypress in CI.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill cypress-testing- The cypress-testing skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Cypress is a modern, developer-first end-to-end and component testing framework that runs directly in the browser. Unlike Selenium-based tools, Cypress operates inside the browser's execution context, giving it native access to the DOM, network layer, and application state. This skill covers writing reliable e2e tests, component tests, custom commands, network interception, auth strategies, and CI integration.
Tags
cypress e2e testing component-testing automation
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair cypress-testing with these complementary skills:
Frequently Asked Questions
What is cypress-testing?
Use this skill when writing Cypress e2e or component tests, creating custom commands, intercepting network requests, or integrating Cypress in CI. Triggers on Cypress, cy.get, cy.intercept, cypress component testing, custom commands, fixtures, cypress-cucumber, and any task requiring Cypress test automation.
How do I install cypress-testing?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill cypress-testing in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support cypress-testing?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Cypress Testing
Cypress is a modern, developer-first end-to-end and component testing framework that runs directly in the browser. Unlike Selenium-based tools, Cypress operates inside the browser's execution context, giving it native access to the DOM, network layer, and application state. This skill covers writing reliable e2e tests, component tests, custom commands, network interception, auth strategies, and CI integration.
When to use this skill
Trigger this skill when the user:
- Asks to write or debug a Cypress e2e test
- Wants to set up Cypress component testing
- Needs to intercept or stub network requests with
cy.intercept - Asks how to use
cy.get,cy.contains, or other Cypress commands - Wants to create reusable custom Cypress commands
- Asks about fixtures, aliases, or the Cypress command queue
- Is integrating Cypress into a GitHub Actions or other CI pipeline
Do NOT trigger this skill for:
- Unit testing with Jest, Vitest, or similar (those don't use the Cypress runner)
- Playwright or Puppeteer test authoring (different APIs entirely)
Key principles
Never use arbitrary waits -
cy.wait(2000)is a smell. Usecy.interceptaliases (cy.wait('@alias')),cy.contains, or assertion retries. Cypress retries automatically for up to 4 seconds by default.Select by
data-testid- Never select by CSS class, tag name, or text that changes. Adddata-testid="submit-btn"to elements and select withcy.get('[data-testid="submit-btn"]'). Classes are for styling; test IDs are for testing.Intercept network requests - never hit real APIs - Use
cy.interceptto stub all HTTP calls. Real API calls make tests slow, flaky, and environment-dependent. Stub responses with fixtures or inline JSON.Each test must be independent - Tests must not share state. Use
beforeEachto reset state, reseed fixtures, and re-stub routes. Never rely on test execution order. A test that only passes after another test ran is a bug.Use custom commands for reuse - Repeated multi-step setups (login, seed data, navigate to a page) belong in
cypress/support/commands.ts, not duplicated across spec files. Custom commands keep specs readable and DRY.
Core concepts
Command queue and chaining - Cypress commands are not synchronous. Each cy.*
call enqueues a command that runs asynchronously. You cannot use const el = cy.get()
and then use el later. Instead, chain commands: cy.get('.item').click().should('...').
Never mix async/await with Cypress commands - it breaks the queue.
Retry-ability - Cypress automatically retries cy.get, cy.contains, and most
assertions until they pass or the timeout is exceeded. This is the correct alternative
to cy.wait(N). Structure assertions so they express the desired end state; Cypress
will poll until it's reached.
Intercept vs stub - cy.intercept(method, url) passively observes traffic.
cy.intercept(method, url, response) stubs the response. Both return a route that
can be aliased with .as('alias') and waited on with cy.wait('@alias'), which blocks
until the matching request fires - the correct way to synchronize on async operations.
Component vs e2e - Component testing mounts a single component in isolation
(like Storybook but with assertions). E2e testing visits a full running app in a real
browser. Use component tests for UI logic and edge-case rendering; use e2e tests for
critical user journeys. They use different cypress.config.ts specPattern entries.
Common tasks
Write a page object pattern test
The Page Object pattern encapsulates selectors and actions behind readable methods, decoupling tests from DOM structure.
// cypress/pages/LoginPage.ts
export class LoginPage {
visit() {
cy.visit('/login');
}
fillEmail(email: string) {
cy.get('[data-testid="email-input"]').clear().type(email);
}
fillPassword(password: string) {
cy.get('[data-testid="password-input"]').clear().type(password);
}
submit() {
cy.get('[data-testid="login-btn"]').click();
}
errorMessage() {
return cy.get('[data-testid="login-error"]');
}
}
// cypress/e2e/login.cy.ts
import { LoginPage } from '../pages/LoginPage';
const login = new LoginPage();
describe('Login', () => {
beforeEach(() => {
cy.intercept('POST', '/api/auth/login').as('loginRequest');
login.visit();
});
it('redirects to dashboard on valid credentials', () => {
cy.intercept('POST', '/api/auth/login', { fixture: 'auth/success.json' }).as('loginRequest');
login.fillEmail('user@example.com');
login.fillPassword('password123');
login.submit();
cy.wait('@loginRequest');
cy.url().should('include', '/dashboard');
});
it('shows error on invalid credentials', () => {
cy.intercept('POST', '/api/auth/login', { statusCode: 401, body: { error: 'Invalid credentials' } }).as('loginRequest');
login.fillEmail('wrong@example.com');
login.fillPassword('wrongpass');
login.submit();
cy.wait('@loginRequest');
login.errorMessage().should('be.visible').and('contain', 'Invalid credentials');
});
});Intercept and stub API responses
// cypress/fixtures/products.json
// { "items": [{ "id": 1, "name": "Widget", "price": 9.99 }] }
describe('Product listing', () => {
it('renders products from API', () => {
cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="product-card"]').should('have.length', 1);
cy.contains('Widget').should('be.visible');
});
it('shows empty state when no products', () => {
cy.intercept('GET', '/api/products', { body: { items: [] } }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="empty-state"]').should('be.visible');
});
it('shows error state on 500', () => {
cy.intercept('GET', '/api/products', { statusCode: 500 }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="error-banner"]').should('be.visible');
});
});Create custom commands with TypeScript
// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session(
[email, password],
() => {
cy.request('POST', '/api/auth/login', { email, password })
.its('body.token')
.then((token) => {
window.localStorage.setItem('auth_token', token);
});
},
{ cacheAcrossSpecs: true }
);
});
Cypress.Commands.add('dataCy', (selector: string) => {
return cy.get(`[data-testid="${selector}"]`);
});
// cypress/support/index.d.ts
declare namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
dataCy(selector: string): Chainable<JQuery<HTMLElement>>;
}
}
// Usage in spec
cy.login('user@example.com', 'password123');
cy.dataCy('submit-btn').click();Component testing setup
// cypress.config.ts
import { defineConfig } from 'cypress';
import { devServer } from '@cypress/vite-dev-server';
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,tsx}',
},
});
// src/components/Button/Button.cy.tsx
import React from 'react';
import { Button } from './Button';
describe('Button', () => {
it('calls onClick when clicked', () => {
const onClick = cy.stub().as('onClick');
cy.mount(<Button onClick={onClick}>Submit</Button>);
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
it('is disabled when loading', () => {
cy.mount(<Button loading>Submit</Button>);
cy.get('button').should('be.disabled');
cy.get('[data-testid="spinner"]').should('be.visible');
});
});Handle auth - login programmatically
Avoid logging in via the UI in every test. Use cy.session to cache the session
across tests, and cy.request to authenticate via the API directly.
// cypress/support/commands.ts
Cypress.Commands.add('loginByApi', (role: 'admin' | 'user' = 'user') => {
const credentials = {
admin: { email: 'admin@example.com', password: Cypress.env('ADMIN_PASSWORD') },
user: { email: 'user@example.com', password: Cypress.env('USER_PASSWORD') },
};
cy.session(
role,
() => {
cy.request({
method: 'POST',
url: `${Cypress.env('API_URL')}/auth/login`,
body: credentials[role],
}).then(({ body }) => {
localStorage.setItem('token', body.token);
});
},
{
validate: () => {
cy.request(`${Cypress.env('API_URL')}/auth/me`).its('status').should('eq', 200);
},
cacheAcrossSpecs: true,
}
);
});
// In specs
beforeEach(() => {
cy.loginByApi('admin');
});Visual regression with screenshots
Use cypress-image-diff or @percy/cypress. Always stub dynamic content (timestamps,
counts) before snapshotting, and wait for all async data to resolve first.
// Requires cypress-image-diff: cy.compareSnapshot(name, threshold)
it('matches dashboard baseline', () => {
cy.loginByApi();
cy.intercept('GET', '/api/dashboard', { fixture: 'dashboard.json' }).as('getDashboard');
cy.visit('/dashboard');
cy.wait('@getDashboard');
cy.get('[data-testid="dashboard-chart"]').should('be.visible');
cy.get('[data-testid="current-time"]').invoke('text', '12:00 PM'); // freeze dynamic text
cy.compareSnapshot('dashboard-full', 0.1); // 10% pixel threshold
});CI integration with GitHub Actions
# .github/workflows/cypress.yml
name: Cypress Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
cypress-e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: cypress-io/github-action@v6
with:
start: npm run start:ci
wait-on: 'http://localhost:3000'
record: true
parallel: true
browser: chrome
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_ADMIN_PASSWORD: ${{ secrets.TEST_ADMIN_PASSWORD }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-${{ matrix.containers }}
path: cypress/screenshotsAnti-patterns
| Anti-pattern | Why it's wrong | What to do instead |
|---|---|---|
cy.wait(3000) |
Hard-codes arbitrary delay; flaky in CI and wastes time on fast machines | Use cy.wait('@alias') on intercepted requests or assertion retry-ability |
cy.get('.btn-primary') |
CSS classes change with restyling, breaking unrelated tests | Use cy.get('[data-testid="..."]') exclusively for test selectors |
| Hitting real APIs in tests | Tests become slow, environment-dependent, and can mutate production data | Stub all HTTP with cy.intercept and fixtures |
| Logging in via UI in every test | Repeating form fill + submit across 50 tests is slow and brittle | Use cy.session + cy.request to authenticate programmatically |
| Sharing state between tests | it blocks that depend on prior it blocks fail non-deterministically |
Reset state in beforeEach; each test must be self-contained |
Using async/await with Cypress commands |
Async/await bypasses the Cypress command queue, causing race conditions | Use .then() chaining for sequential async logic inside commands |
Gotchas
Mixing
async/awaitwith Cypress commands breaks the queue - Cypress commands return a Cypress chainable, not a real Promise. Usingawait cy.get(...)bypasses the command queue, causing commands to run out of order or against stale DOM state. Use.then()chaining for sequential logic inside commands; neverasync/awaitin spec bodies.cy.sessioncache invalidation surprises -cy.sessioncaches authentication state across tests. If the backend invalidates the session (token expiry, server restart during the test run), all subsequent tests fail with 401s in ways that look like unrelated test failures. Add avalidatecallback tocy.sessionthat confirms the session is still active before trusting the cache.Intercepting too broadly breaks test isolation - Using
cy.intercept('*', ...)to stub all requests catches requests you didn't intend to stub, including Cypress's own internal traffic and third-party scripts. Always use specific method + URL pattern matches and scope intercepts to the test that needs them.Component tests mounting without providers - Components that rely on React context, Redux store, router, or i18n providers will crash or render incorrectly when mounted without those providers in
cy.mount(). Always wrapcy.mount()with the necessary providers matching the app's actual setup.data-testidon dynamically rendered lists - Adding a singledata-testid="item"to a list renders multiple elements with the same selector.cy.get('[data-testid="item"]')returns a collection, and assertions on it behave unexpectedly. Use index-based IDs (data-testid="item-0") or use.eq(n)/.within()to scope assertions to specific list items.
References
For detailed content on specific topics, read the relevant file from references/:
references/commands-reference.md- Essential Cypress commands with real examples
Only load a references file when the current task requires deep detail on that topic.
References
commands-reference.md
Cypress Commands Reference
Quick reference for the most-used Cypress commands with practical examples.
Navigation
cy.visit('/path') // Visit a relative URL (uses baseUrl from config)
cy.visit('https://example.com') // Visit an absolute URL
cy.visit('/checkout', { timeout: 10000 }) // Custom timeout for slow pages
cy.reload() // Reload the current page
cy.go('back') // Browser back
cy.go('forward') // Browser forward
cy.url().should('include', '/dashboard') // Assert current URL
cy.title().should('eq', 'My App') // Assert page titleQuerying
// Preferred: always use data-testid
cy.get('[data-testid="submit-btn"]')
cy.get('[data-testid="user-list"] [data-testid="user-card"]') // nested
// By text content
cy.contains('Submit') // first element with text
cy.contains('button', 'Submit') // scoped to tag
cy.contains('[data-testid="nav"]', 'Home') // scoped to selector
// Scoped queries - search within a subject
cy.get('[data-testid="user-card"]').within(() => {
cy.get('[data-testid="user-name"]').should('contain', 'Alice');
cy.get('[data-testid="user-role"]').should('contain', 'Admin');
});
// Index-based access (use sparingly)
cy.get('[data-testid="product-card"]').eq(0).should('contain', 'Widget');
cy.get('[data-testid="product-card"]').first();
cy.get('[data-testid="product-card"]').last();
// Find within a subject
cy.get('[data-testid="form"]').find('input').should('have.length', 3);Assertions
// Visibility
cy.get('[data-testid="modal"]').should('be.visible');
cy.get('[data-testid="spinner"]').should('not.exist');
cy.get('[data-testid="error"]').should('exist');
// Text
cy.get('[data-testid="title"]').should('have.text', 'Welcome');
cy.get('[data-testid="message"]').should('contain', 'success');
// Attributes and state
cy.get('input[type="email"]').should('have.value', 'user@example.com');
cy.get('[data-testid="submit"]').should('be.disabled');
cy.get('[data-testid="checkbox"]').should('be.checked');
cy.get('[data-testid="link"]').should('have.attr', 'href', '/about');
cy.get('[data-testid="card"]').should('have.class', 'active');
// Count
cy.get('[data-testid="item"]').should('have.length', 5);
cy.get('[data-testid="item"]').should('have.length.greaterThan', 0);
// Chained: multiple assertions on one subject
cy.get('[data-testid="alert"]')
.should('be.visible')
.and('have.class', 'alert-error')
.and('contain', 'Something went wrong');Interactions
// Click
cy.get('[data-testid="btn"]').click();
cy.get('[data-testid="btn"]').click({ force: true }); // bypass visibility check
// Typing
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="search"]').type('query{enter}'); // special keys
cy.get('[data-testid="email"]').clear().type('new@example.com');
// Select dropdown
cy.get('[data-testid="role-select"]').select('admin');
cy.get('[data-testid="role-select"]').select(1); // by index
// Checkboxes and radios
cy.get('[data-testid="agree"]').check();
cy.get('[data-testid="agree"]').uncheck();
cy.get('[data-testid="plan-pro"]').check();
// File upload
cy.get('[data-testid="upload"]').selectFile('cypress/fixtures/avatar.png');
// Keyboard shortcuts
cy.get('[data-testid="editor"]').type('{ctrl+a}{del}');
cy.get('body').type('{esc}');
// Hover (triggers CSS :hover)
cy.get('[data-testid="menu-trigger"]').trigger('mouseover');
cy.get('[data-testid="tooltip-target"]').trigger('mouseenter');
// Focus and blur
cy.get('[data-testid="input"]').focus().blur();Network interception
// Observe (no stub) - still creates a waitable alias
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
// Stub with inline body
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Alice' }],
}).as('getUsers');
// Stub with fixture file
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
// Stub with delay to test loading states
cy.intercept('GET', '/api/users', {
delay: 1000,
fixture: 'users.json',
}).as('getUsers');
// Stub error states
cy.intercept('POST', '/api/orders', { statusCode: 500, body: { error: 'Server error' } }).as('createOrder');
// Pattern matching with wildcards
cy.intercept('GET', '/api/users/*').as('getUser');
cy.intercept('GET', '/api/products?*').as('getProducts'); // query params
// Inspect the request after it fires
cy.wait('@getUsers').then((interception) => {
expect(interception.request.headers).to.have.property('authorization');
expect(interception.response?.statusCode).to.eq(200);
});
// Wait on multiple requests
cy.wait(['@getUsers', '@getSettings']);Aliases
// Alias a DOM element (re-queries on use to avoid stale references)
cy.get('[data-testid="submit"]').as('submitBtn');
cy.get('@submitBtn').click();
// Alias a route (see intercept examples above)
cy.intercept('GET', '/api/data').as('getData');
cy.wait('@getData');
// Alias a fixture value
cy.fixture('users.json').as('usersData');
cy.get('@usersData').then((users) => {
cy.get('[data-testid="user-row"]').should('have.length', users.length);
});Fixtures and test data
// Load fixture inline
cy.fixture('users.json').then((users) => {
// use users array in test
});
// Load fixture as alias and use in intercept
cy.fixture('products.json').as('productsData');
cy.intercept('GET', '/api/products', { fixture: 'products.json' });
// cypress/fixtures/users.json
// [{ "id": 1, "name": "Alice", "role": "admin" }]Session and storage
// cy.session caches login state across tests (Cypress 9+)
cy.session('user-session', () => {
cy.request('POST', '/api/auth/login', { email: 'u@example.com', password: 'pw' })
.its('body.token')
.then((token) => localStorage.setItem('token', token));
});
// Direct storage manipulation
cy.clearLocalStorage();
cy.clearCookies();
window.localStorage.setItem('key', 'value'); // inside cy.window().then()
// Read cookies
cy.getCookie('session_id').should('exist');
cy.setCookie('feature_flag', 'enabled');Tasks and plugins (Node.js bridge)
// cypress.config.ts - define tasks
on('task', {
seedDatabase: async (data) => {
await db.seed(data);
return null; // tasks must return a value or null
},
clearDatabase: async () => {
await db.truncateAll();
return null;
},
});
// In spec - invoke tasks
beforeEach(() => {
cy.task('clearDatabase');
cy.task('seedDatabase', { users: 3, products: 10 });
});Viewport and environment
// Set viewport
cy.viewport(1440, 900);
cy.viewport('iphone-14');
cy.viewport('ipad-2');
// Read env vars (set in cypress.env.json or --env flag)
const apiUrl = Cypress.env('API_URL');
// Conditional based on environment
if (Cypress.env('CI')) {
// CI-specific behavior
}
// Browser info
Cypress.browser.name // 'chrome', 'firefox', 'electron'Debugging
// Pause execution and open DevTools
cy.pause();
// Print subject to console without breaking the chain
cy.get('[data-testid="item"]').debug();
// Log to Cypress command log
cy.log('Current step: navigating to checkout');
// .then() to inspect subject mid-chain
cy.get('[data-testid="total"]').then(($el) => {
console.log('Total text:', $el.text());
});
// Take screenshot (manual)
cy.screenshot('checkout-page');
cy.screenshot('error-state', { capture: 'viewport' });Configuration reference (cypress.config.ts)
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
supportFile: 'cypress/support/e2e.ts',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 4000, // cy.get retry timeout
requestTimeout: 10000, // cy.request timeout
responseTimeout: 30000, // network response timeout
video: false, // disable in CI to save time
screenshotOnRunFailure: true,
retries: {
runMode: 2, // retry twice in CI
openMode: 0, // no retries in interactive mode
},
env: {
API_URL: 'http://localhost:3001',
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,tsx}',
supportFile: 'cypress/support/component.ts',
},
}); Frequently Asked Questions
What is cypress-testing?
Use this skill when writing Cypress e2e or component tests, creating custom commands, intercepting network requests, or integrating Cypress in CI. Triggers on Cypress, cy.get, cy.intercept, cypress component testing, custom commands, fixtures, cypress-cucumber, and any task requiring Cypress test automation.
How do I install cypress-testing?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill cypress-testing in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support cypress-testing?
cypress-testing works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.