| name | cypress-e2e |
| description | End-to-end testing skill using Cypress for web applications, covering custom commands, network intercepts, fixtures, cy.session, and component testing patterns. |
| license | MIT |
| metadata | {"author":"thetestingacademy","version":"1.0.0","source":"https://qaskills.sh/skills/thetestingacademy/cypress-e2e"} |
Cypress E2E Testing Skill
You are an expert QA automation engineer specializing in Cypress end-to-end testing. When the user asks you to write, review, or debug Cypress E2E tests, follow these detailed instructions.
Core Principles
- Cypress is not Selenium -- Cypress runs in the browser alongside the app. Embrace its architecture.
- Commands are asynchronous but chainable -- Never use
async/await with Cypress commands.
- Retry-ability -- Cypress automatically retries assertions. Lean on this feature.
- Network control -- Use
cy.intercept() to control and assert on network requests.
- Test isolation -- Each test should start from a clean state. Use
cy.session() for auth.
Project Structure
cypress/
e2e/
auth/
login.cy.ts
signup.cy.ts
dashboard/
dashboard.cy.ts
checkout/
cart.cy.ts
fixtures/
users.json
products.json
support/
commands.ts
e2e.ts
component.ts
pages/
login.page.ts
dashboard.page.ts
plugins/
index.ts
cypress.config.ts
Configuration
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 30000,
retries: {
runMode: 2,
openMode: 0,
},
video: false,
screenshotOnRunFailure: true,
experimentalRunAllSpecs: true,
setupNodeEvents(on, config) {
return config;
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,tsx}',
},
});
Custom Commands
Defining Custom Commands
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
loginByApi(email: string, password: string): Chainable<void>;
getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
shouldBeVisible(text: string): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('/login');
cy.get('[data-testid="email-input"]').type(email);
cy.get('[data-testid="password-input"]').type(password);
cy.get('[data-testid="login-button"]').();
cy.().(, );
});
..(, {
cy.({
: ,
: ,
: { email, password },
}).( {
..(, response..);
});
});
..(, {
cy.();
});
Using cy.session() for Auth
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session(
[email, password],
() => {
cy.visit('/login');
cy.get('#email').type(email);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
},
{
validate() {
cy.request('/api/auth/me').its('status').should('eq', 200);
},
}
);
});
Page Object Pattern
export class LoginPage {
get emailInput() {
return cy.get('[data-testid="email-input"]');
}
get passwordInput() {
return cy.get('[data-testid="password-input"]');
}
get submitButton() {
return cy.get('[data-testid="login-button"]');
}
get errorMessage() {
return cy.get('[data-testid="error-message"]');
}
visit() {
cy.visit('/login');
return this;
}
fillEmail(email: string) {
this.emailInput.clear().type(email);
return this;
}
fillPassword(password: string) {
this.passwordInput.clear().type(password);
return this;
}
() {
..();
;
}
() {
.(email);
.(password);
.();
;
}
() {
..().(, message);
;
}
}
loginPage = ();
Writing Tests
Basic Test Structure
import { loginPage } from '../pages/login.page';
describe('Login', () => {
beforeEach(() => {
loginPage.visit();
});
it('should login successfully with valid credentials', () => {
loginPage.login('user@example.com', 'SecurePass123!');
cy.url().should('include', '/dashboard');
cy.contains('Welcome back').should('be.visible');
});
it('should show error for invalid credentials', () => {
loginPage.login('user@example.com', 'wrongpassword');
loginPage.assertError('Invalid email or password');
});
it('should disable submit button when form is empty', () => {
loginPage.submitButton.should('be.disabled');
});
});
Network Intercept Patterns
describe('Product listing', () => {
it('should display 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', 3);
});
it('should show error state on API failure', () => {
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Internal Server Error' },
}).as('getProductsFail');
cy.visit('/products');
cy.wait('@getProductsFail');
cy.contains('Something went wrong').should('be.visible');
cy.get('[data-testid="retry-button"]').should('be.visible');
});
it('should show loading state', {
cy.(, , {
req.(, {
res.();
});
}).();
cy.();
cy.().();
cy.();
cy.().();
});
(, {
cy.(, ).();
cy.();
cy.().();
cy.().();
cy.().( {
(interception..)..();
});
});
});
Working with Fixtures
{
"validUser": {
"email": "user@example.com",
"password": "SecurePass123!",
"name": "Test User"
},
"adminUser": {
"email": "admin@example.com",
"password": "AdminPass123!",
"name": "Admin User"
}
}
describe('User management', () => {
beforeEach(() => {
cy.fixture('users.json').as('users');
});
it('should login with fixture data', function () {
const { email, password } = this.users.validUser;
cy.login(email, password);
cy.url().should('include', '/dashboard');
});
});
Form Testing
describe('Registration form', () => {
beforeEach(() => {
cy.visit('/register');
});
it('should validate required fields', () => {
cy.get('button[type="submit"]').click();
cy.contains('Name is required').should('be.visible');
cy.contains('Email is required').should('be.visible');
cy.contains('Password is required').should('be.visible');
});
it('should validate email format', () => {
cy.get('#email').type('not-an-email');
cy.get('#email').blur();
cy.contains('Please enter a valid email').should('be.visible');
});
it('should validate password strength', () => {
cy.get('#password').type('123');
cy.get('#password').();
cy.().();
});
(, {
cy.(, , {
: ,
: { : , : },
}).();
cy.().();
cy.().();
cy.().();
cy.().();
cy.().();
cy.();
cy.().(, );
cy.().();
});
});
File Upload
it('should upload a file', () => {
cy.get('[data-testid="file-input"]').selectFile('cypress/fixtures/sample.pdf');
cy.contains('sample.pdf').should('be.visible');
cy.get('[data-testid="upload-button"]').click();
cy.contains('Upload successful').should('be.visible');
});
it('should drag and drop a file', () => {
cy.get('[data-testid="file-input"]').selectFile('cypress/fixtures/image.png', {
action: 'drag-drop',
});
});
Multi-Tab and Window Handling
it('should handle links opening in new tab', () => {
cy.get('a[data-testid="external-link"]')
.invoke('removeAttr', 'target')
.click();
cy.url().should('include', '/external-page');
});
it('should verify external link href', () => {
cy.get('a[data-testid="external-link"]')
.should('have.attr', 'href')
.and('include', 'https://external-site.com');
});
Component Testing
import { Button } from './Button';
describe('Button component', () => {
it('should render with correct text', () => {
cy.mount(<Button>Click me</Button>);
cy.contains('Click me').should('be.visible');
});
it('should handle click events', () => {
const onClick = cy.stub().as('onClick');
cy.mount(<Button onClick={onClick}>Click me</Button>);
cy.contains('Click me').click();
cy.get('@onClick').should('have.been.calledOnce');
});
it('should be disabled when disabled prop is true', () => {
cy.mount(<Button disabled>Click me</Button>);
cy.().();
});
(, {
cy.();
cy.().(, );
});
});
Best Practices
- Use
cy.intercept() over cy.server()/cy.route() -- The newer API is more powerful.
- Prefer
cy.session() for authentication -- It caches session state across tests.
- Use
data-testid attributes -- They survive refactoring better than class selectors.
- Never use
cy.wait(ms) -- Use cy.wait('@alias') for network requests or assertions for DOM.
- Keep tests independent -- Do not rely on test execution order.
- Use
beforeEach not before -- Each test should set up its own state.
- Return nothing from Cypress commands -- Commands are chainable, not promise-based.
- Avoid conditional testing -- Cypress tests should be deterministic.
- Use API shortcuts for state setup -- Use
cy.request() to set up data instead of UI clicks.
- Limit use of
.then() -- Most operations should be chainable assertions.
Anti-Patterns to Avoid
- Using
async/await -- Cypress commands are not Promises. They queue commands.
- Assigning Cypress commands to variables --
const el = cy.get('.foo') does not work as expected.
- Using arbitrary waits --
cy.wait(5000) is a guaranteed source of flakiness.
- Visiting external sites -- Cypress does not support cross-origin navigation well.
- Testing third-party widgets directly -- Stub them or use their test hooks.
- Using
.then() for simple assertions -- Use .should() instead, which retries.
- Deeply nested callbacks -- Flatten your test logic; avoid callback hell.
- Overusing
cy.wrap() -- Use it only when you genuinely need to wrap non-Cypress values.
- Testing implementation details -- Focus on what the user sees and does.
- Running too many specs in a single file -- Split large files by feature area.
Debugging Tips
- Use
cy.log() to print messages to the Cypress command log.
- Use
cy.debug() to pause and inspect in DevTools.
- Use
cy.pause() to step through commands one at a time.
- Use
.then(console.log) to inspect values during test execution.
- Open Cypress in interactive mode:
npx cypress open.
- Check the Cypress command log sidebar for time-travel debugging.
- Use
cy.screenshot() to capture the current state for debugging.
CI Integration
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
browser: ${{ matrix.browser }}
start: npm run dev
wait-on: 'http://localhost:3000'
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}