cypress-automation
Discovery Questions
Before generating Cypress tests, ask. Check .agents/qa-project-context.md first -- if it exists, use it and skip questions already answered there.
- Component testing, E2E, or both? Component testing mounts individual components in isolation. E2E tests the full application through the browser. Most projects need both. Component testing requires a framework-specific mount (React, Vue, Angular, Svelte).
- Cypress Cloud? Cloud provides parallelization, flake detection, analytics, and test replay. If the team uses it, configure the
projectIdandrecordkey. If not, everything runs locally or in CI without Cloud. - TypeScript? Strongly recommended. Cypress supports TypeScript natively since v13. All examples in this skill use TypeScript.
- Framework and bundler? React + Vite, Next.js + Webpack, Vue + Vite, Angular -- component testing configuration depends on this.
- Existing test suite or fresh start? If migrating from another tool, start with the flakiest or most critical tests, not a big-bang rewrite.
Core Principles
1. Commands Are Enqueued, Not Executed Immediately
This is the single most important concept in Cypress. Cypress commands (cy.get, cy.click, cy.type) do not execute when called. They are added to a queue and executed serially, asynchronously. You cannot use async/await with Cypress commands. You cannot store the return value in a variable.
// WRONG -- this looks like synchronous code but it is not
const button = cy.get('[data-testid="submit"]'); // button is a Chainable, not an element
button.click(); // this works only by accident because of chaining
// CORRECT -- chain commands, use .then() for values
cy.get('[data-testid="submit"]').click();
// CORRECT -- when you need a value, use .then() or .as()
cy.get('[data-testid="price"]').invoke('text').then((text) => {
const price = parseFloat(text.replace('$', ''));
expect(price).to.be.greaterThan(0);
});
2. Retry-ability Is Built-In (For Queries, Not Actions)
Cypress automatically retries queries (cy.get, cy.find, cy.contains) and assertions until they pass or time out. It does not retry actions (cy.click, cy.type, cy.select). This means:
cy.get('.loading').should('not.exist')will wait for the loading indicator to disappearcy.get('.item').should('have.length', 5)will wait for 5 items to appearcy.click()executes once -- if the element is not actionable, it fails
3. Network Control with cy.intercept
cy.intercept is the most powerful tool in Cypress. It intercepts HTTP requests at the network layer, allowing you to stub responses, wait for requests to complete, and assert on request bodies. Mastering cy.intercept is the difference between flaky and stable tests.
4. Isolation: Each Test Starts Clean
Every it() block runs in a fresh browser state. Cypress clears cookies, localStorage, and sessionStorage between tests by default. Tests must not depend on other tests' state or execution order. Use beforeEach hooks for shared setup, not inter-test dependencies.
5. Data Attributes for Test Selectors
Use data-testid, data-cy, or data-test attributes for selectors. They are immune to CSS refactors, class name changes, and content localization. Configure the preferred attribute in cypress.config.ts.
Project Structure
project-root/
├── cypress.config.ts
├── cypress/
│ ├── e2e/ # E2E test specs, organized by feature
│ ├── component/ # Component test specs (*.cy.tsx)
│ ├── fixtures/ # Static test data (JSON), including api-responses/
│ ├── support/
│ │ ├── commands.ts # Custom commands
│ │ ├── e2e.ts # E2E support file
│ │ ├── component.ts # Component support file
│ │ └── index.d.ts # TypeScript declarations for custom commands
│ └── downloads/ # Git-ignored
├── cypress.env.json # Git-ignored, environment-specific variables
└── tsconfig.json
cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
projectId: process.env.CYPRESS_PROJECT_ID, // For Cypress Cloud
e2e: {
baseUrl: process.env.CYPRESS_BASE_URL ?? 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.ts',
supportFile: 'cypress/support/e2e.ts',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 30000,
video: false, // Enable in CI if needed
screenshotOnRunFailure: true,
retries: {
runMode: 2, // Retries in CI (cypress run)
openMode: 0, // No retries in interactive mode
},
experimentalRunAllSpecs: true, // Run all specs in a single tab (faster)
setupNodeEvents(on, config) {
// Task plugins, code coverage, etc.
return config;
},
},
component: {
devServer: {
framework: 'react', // 'react' | 'vue' | 'angular' | 'svelte'
bundler: 'vite', // 'vite' | 'webpack'
},
specPattern: 'cypress/component/**/*.cy.tsx',
supportFile: 'cypress/support/component.ts',
},
});
Add "types": ["cypress"] to tsconfig.json compilerOptions and include "cypress/**/*.ts" in the include array.
Custom Commands
Custom commands encapsulate repeated actions and provide a clean API. Always type them for autocomplete and compile-time safety.
Defining Commands
// cypress/support/commands.ts
// Login command -- avoid UI login in every test
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password },
}).then((response) => {
expect(response.status).to.eq(200);
window.localStorage.setItem('auth_token', response.body.token);
});
});
});
// Data attribute selector shorthand
Cypress.Commands.add('getByTestId', (testId: string) => {
return cy.get(`[data-testid="${testId}"]`);
});
// Assert toast notification appears and disappears
Cypress.Commands.add('shouldShowToast', (message: string) => {
cy.get('[role="alert"]')
.should('be.visible')
.and('contain.text', message);
cy.get('[role="alert"]').should('not.exist');
});
TypeScript Declarations
// cypress/support/index.d.ts
declare namespace Cypress {
interface Chainable {
/**
* Log in via API and cache the session.
* @example cy.login('user@example.com', 'password123')
*/
login(email: string, password: string): Chainable<void>;
/**
* Select element by data-testid attribute.
* @example cy.getByTestId('submit-button').click()
*/
getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
/**
* Assert a toast notification appears with the given message.
* @example cy.shouldShowToast('Profile updated')
*/
shouldShowToast(message: string): Chainable<void>;
}
}
For retryable element lookups, use Cypress.Commands.addQuery() (Cypress 12+) instead of Cypress.Commands.add() -- custom queries retry automatically like built-in queries.
cy.intercept Patterns
Stub a Response
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: { products: [{ id: '1', name: 'Widget', price: 29.99 }] },
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.getByTestId('product-card').should('have.length', 1);
Spy on Requests (No Stubbing)
cy.intercept('POST', '/api/orders').as('createOrder');
cy.getByTestId('place-order').click();
cy.wait('@createOrder').then((interception) => {
expect(interception.request.body).to.have.property('items');
expect(interception.request.body.items).to.have.length(2);
expect(interception.response?.statusCode).to.eq(201);
});
Conditional Responses
let callCount = 0;
cy.intercept('GET', '/api/status', (req) => {
callCount += 1;
if (callCount <= 2) {
req.reply({ statusCode: 202, body: { status: 'processing' } });
} else {
req.reply({ statusCode: 200, body: { status: 'complete', url: '/download/report.pdf' } });
}
}).as('pollStatus');
Simulate Network Errors
// Simulate server error
cy.intercept('POST', '/api/checkout', { statusCode: 500, body: { error: 'Internal Server Error' } }).as('checkoutFail');
// Simulate network failure
cy.intercept('POST', '/api/checkout', { forceNetworkError: true }).as('networkError');
// Simulate slow response
cy.intercept('GET', '/api/dashboard', (req) => {
req.reply({
delay: 5000,
statusCode: 200,
body: { widgets: [] },
});
}).as('slowDashboard');
Modify Real Response
cy.intercept('GET', '/api/feature-flags', (req) => {
req.continue((res) => {
res.body.flags['new-checkout'] = true;
res.send();
});
}).as('featureFlags');
Using Fixture Files
// Load response from cypress/fixtures/api-responses/checkout-success.json
cy.intercept('POST', '/api/checkout', { fixture: 'api-responses/checkout-success.json' }).as('checkout');
Component Testing
Component testing mounts a single component in a real browser without running the full application. It is faster than E2E and gives more visual feedback than unit tests.
React Component Test
// cypress/component/ProductCard.cy.tsx
import { ProductCard } from '../../src/components/ProductCard';
describe('ProductCard', () => {
const product = { id: '1', name: 'Widget', price: 29.99, image: '/widget.png' };
it('renders product information', () => {
cy.mount(<ProductCard product={product} onAddToCart={cy.stub()} />);
cy.contains('Widget').should('be.visible');
cy.contains('$29.99').should('be.visible');
cy.get('img').should('have.attr', 'src', '/widget.png');
});
it('calls onAddToCart with product id when button clicked', () => {
const onAddToCart = cy.stub().as('addToCart');
cy.mount(<ProductCard product={product} onAddToCart={onAddToCart} />);
cy.contains('button', 'Add to Cart').click();
cy.get('@addToCart').should('have.been.calledOnceWith', '1');
});
it('shows out of stock state', () => {
cy.mount(<ProductCard product={{ ...product, inStock: false }} onAddToCart={cy.stub()} />);
cy.contains('button', 'Add to Cart').should('be.disabled');
cy.contains('Out of Stock').should('be.visible');
});
});
For Vue, use cy.mount(Component, { props: { ... } }) with cy.spy() for event assertions. The pattern mirrors React but uses Vue's prop/event conventions.
Data-Driven Testing with Fixtures
Static Fixture Data
// Load from cypress/fixtures/users.json
describe('Role-based access', () => {
beforeEach(function () {
cy.fixture('users').as('users');
});
it('admin sees admin panel', function () {
const admin = this.users.find((u: { role: string }) => u.role === 'admin');
cy.login(admin.email, admin.password);
cy.visit('/admin');
cy.getByTestId('admin-panel').should('be.visible');
});
});
Dynamic Test Data via cy.task
Use cy.task for operations that need Node.js context (API calls, database seeding):
// cypress.config.ts -- register tasks in setupNodeEvents
on('task', {
async seedTestUser(role: string) {
const response = await fetch(`${config.env.API_URL}/test/seed-user`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }),
});
return response.json();
},
});
// In test:
beforeEach(() => {
cy.task('seedTestUser', 'admin').then((user) => {
cy.login(user.email, user.password);
});
});
Environment-Specific Configuration
// Run with: npx cypress run --env ENVIRONMENT=staging
setupNodeEvents(on, config) {
const envConfig = { local: { baseUrl: 'http://localhost:3000' }, staging: { baseUrl: 'https://staging.example.com' } };
return { ...config, ...envConfig[config.env.ENVIRONMENT || 'local'] };
}
CI Integration
With Cypress Cloud
Set projectId in cypress.config.ts. Run with npx cypress run --record --key $CYPRESS_RECORD_KEY. Cloud provides parallelization, flake detection, test replay, and analytics.
# GitHub Actions -- Cloud parallelization
jobs:
cypress:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
record: true
parallel: true
group: 'E2E Tests'
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
Without Cloud
# GitHub Actions -- standalone
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
wait-on: 'http://localhost:3000'
browser: chrome
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-artifacts
path: |
cypress/screenshots
cypress/videos
Anti-Patterns
cy.wait(milliseconds) for Synchronization
// BAD
cy.get('[data-testid="submit"]').click();
cy.wait(3000);
// GOOD -- wait for network
cy.intercept('POST', '/api/submit').as('submit');
cy.get('[data-testid="submit"]').click();
cy.wait('@submit');
Only acceptable for throttle/debounce testing. For everything else, wait for a network alias or a DOM assertion.
Conditional Testing Based on DOM State
Do not check $body.find(selector).length > 0 to conditionally act. Tests should control state deterministically. Stub the API that controls the conditional element.
CSS Selectors Over Data Attributes
cy.get('.btn.btn-primary > span') breaks on every CSS refactor. Use cy.getByTestId('submit') or cy.contains('button', 'Place Order').
Sharing State Between Tests
Module-level let orderId set in one it() and read in another creates order-dependent, parallel-unsafe tests. Each test must set up its own data via cy.request or cy.task in beforeEach.
Testing Third-Party Iframes
Do not reach into Stripe/PayPal iframes. Mock the payment API with cy.intercept and assert on your own UI.
Not Using cy.session() for Login
UI-based login in every test is slow and fragile. Use cy.session() (shown in custom commands above) to authenticate via API once and cache the session.
Running All Tests Serially in CI
Parallelize once the suite exceeds 5 minutes. Use Cypress Cloud, cypress-split, or manual sharding across CI matrix jobs.
Done When
cypress.config.tsexists with a correctbaseUrl(not hardcoded tolocalhostin CI) and explicitviewportWidth/viewportHeight- Custom commands extracted to
cypress/support/commands.tswith TypeScript declarations incypress/support/index.d.ts cy.interceptused for all API dependencies that could be slow or unreliable — no tests relying on real network calls for determinism- Tests pass in CI with either a recorded Cypress Cloud run (parallel) or local video/screenshot artifacts uploaded on failure
- Component tests co-located with source files (e.g.
ProductCard.cy.tsxnext toProductCard.tsx) rather than in a separate top-level directory
Related Skills
- ci-cd-integration -- Pipeline templates for running Cypress in GitHub Actions and GitLab CI, including parallelization and artifact management.
- visual-testing -- Visual regression testing approaches that complement Cypress functional tests; Cypress does not have built-in visual comparison.
- unit-testing -- Unit tests with Jest/Vitest for logic that does not need a browser; Cypress component tests fill the gap between unit and E2E.
- test-data-management -- Strategies for seeding, managing, and cleaning up test data used by Cypress tests.
- test-reliability -- Patterns for fixing flaky Cypress tests, retry strategies, and stability metrics.
- qa-project-context -- The project context file that captures framework choices, CI platform, and testing conventions.