Qaskills Cypress E2E Testing
End-to-end testing skill using Cypress for web applications, covering custom commands, network intercepts, fixtures, cy.session, and component testing patterns.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/cypress-e2e" ~/.claude/skills/pramoddutta-qaskills-cypress-e2e-testing && rm -rf "$T"
manifest:
seed-skills/cypress-e2e/SKILL.mdsource content
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
with Cypress commands.async/await - Retry-ability -- Cypress automatically retries assertions. Lean on this feature.
- Network control -- Use
to control and assert on network requests.cy.intercept() - Test isolation -- Each test should start from a clean state. Use
for auth.cy.session()
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
// cypress.config.ts 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) { // Register plugins here return config; }, }, component: { devServer: { framework: 'react', bundler: 'vite', }, specPattern: 'src/**/*.cy.{ts,tsx}', }, });
Custom Commands
Defining Custom Commands
// cypress/support/commands.ts 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"]').click(); cy.url().should('include', '/dashboard'); }); Cypress.Commands.add('loginByApi', (email: string, password: string) => { cy.request({ method: 'POST', url: '/api/auth/login', body: { email, password }, }).then((response) => { window.localStorage.setItem('authToken', response.body.token); }); }); Cypress.Commands.add('getByTestId', (testId: string) => { return cy.get(`[data-testid="${testId}"]`); });
Using cy.session()
for Auth
cy.session()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
// cypress/pages/login.page.ts 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; } submit() { this.submitButton.click(); return this; } login(email: string, password: string) { this.fillEmail(email); this.fillPassword(password); this.submit(); return this; } assertError(message: string) { this.errorMessage.should('be.visible').and('contain.text', message); return this; } } export const loginPage = new 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.intercept('GET', '/api/products', (req) => { req.on('response', (res) => { res.setDelay(2000); }); }).as('getProductsSlow'); cy.visit('/products'); cy.get('[data-testid="loading-spinner"]').should('be.visible'); cy.wait('@getProductsSlow'); cy.get('[data-testid="loading-spinner"]').should('not.exist'); }); it('should send correct query parameters', () => { cy.intercept('GET', '/api/products*').as('getProducts'); cy.visit('/products'); cy.get('[data-testid="search-input"]').type('laptop'); cy.get('[data-testid="search-button"]').click(); cy.wait('@getProducts').then((interception) => { expect(interception.request.url).to.include('q=laptop'); }); }); });
Working with Fixtures
// cypress/fixtures/users.json { "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').blur(); cy.contains('Password must be at least 8 characters').should('be.visible'); }); it('should complete registration successfully', () => { cy.intercept('POST', '/api/auth/register', { statusCode: 201, body: { id: '123', email: 'new@example.com' }, }).as('register'); cy.get('#name').type('New User'); cy.get('#email').type('new@example.com'); cy.get('#password').type('SecurePass123!'); cy.get('#confirmPassword').type('SecurePass123!'); cy.get('button[type="submit"]').click(); cy.wait('@register'); cy.url().should('include', '/login'); cy.contains('Registration successful').should('be.visible'); }); });
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', () => { // Remove target="_blank" to keep navigation in same 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
// src/components/Button.cy.tsx 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.get('button').should('be.disabled'); }); it('should apply variant styles', () => { cy.mount(<Button variant="primary">Primary</Button>); cy.get('button').should('have.class', 'btn-primary'); }); });
Best Practices
- Use
overcy.intercept()
/cy.server()
-- The newer API is more powerful.cy.route() - Prefer
for authentication -- It caches session state across tests.cy.session() - Use
attributes -- They survive refactoring better than class selectors.data-testid - Never use
-- Usecy.wait(ms)
for network requests or assertions for DOM.cy.wait('@alias') - Keep tests independent -- Do not rely on test execution order.
- Use
notbeforeEach
-- Each test should set up its own state.before - 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
to set up data instead of UI clicks.cy.request() - Limit use of
-- Most operations should be chainable assertions..then()
Anti-Patterns to Avoid
- Using
-- Cypress commands are not Promises. They queue commands.async/await - Assigning Cypress commands to variables --
does not work as expected.const el = cy.get('.foo') - Using arbitrary waits --
is a guaranteed source of flakiness.cy.wait(5000) - Visiting external sites -- Cypress does not support cross-origin navigation well.
- Testing third-party widgets directly -- Stub them or use their test hooks.
- Using
for simple assertions -- Use.then()
instead, which retries..should() - Deeply nested callbacks -- Flatten your test logic; avoid callback hell.
- Overusing
-- Use it only when you genuinely need to wrap non-Cypress values.cy.wrap() - 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
to print messages to the Cypress command log.cy.log() - Use
to pause and inspect in DevTools.cy.debug() - Use
to step through commands one at a time.cy.pause() - Use
to inspect values during test execution..then(console.log) - Open Cypress in interactive mode:
.npx cypress open - Check the Cypress command log sidebar for time-travel debugging.
- Use
to capture the current state for debugging.cy.screenshot()
CI Integration
# .github/workflows/cypress.yml 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 }}