cypress-playwright-setup
SKILL.md
Cypress & Playwright Setup
Configure comprehensive end-to-end testing for web applications.
Core Workflow
- Choose tool: Cypress or Playwright
- Configure project: Browser and test settings
- Create page objects: Reusable selectors
- Write tests: User journey coverage
- Setup fixtures: Test data
- Integrate CI: Automated testing
Playwright Setup
Installation
npm init playwright@latest
Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
Page Object Model
// e2e/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async goto(path: string = '') {
await this.page.goto(path);
}
async waitForLoad() {
await this.page.waitForLoadState('networkidle');
}
getByTestId(testId: string): Locator {
return this.page.getByTestId(testId);
}
}
// e2e/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByTestId('error-message');
this.forgotPasswordLink = page.getByRole('link', { name: 'Forgot password?' });
}
async goto() {
await super.goto('/login');
await this.waitForLoad();
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
async expectLoginSuccess() {
await expect(this.page).toHaveURL(/\/dashboard/);
}
}
// e2e/pages/DashboardPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly welcomeMessage: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
readonly sidebar: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.getByTestId('welcome-message');
this.userMenu = page.getByTestId('user-menu');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
this.sidebar = page.getByTestId('sidebar');
}
async goto() {
await super.goto('/dashboard');
await this.waitForLoad();
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
await expect(this.page).toHaveURL('/login');
}
async expectWelcome(name: string) {
await expect(this.welcomeMessage).toContainText(`Welcome, ${name}`);
}
}
Test Examples
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
test.describe('Authentication', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login', async ({ page }) => {
await loginPage.login('test@example.com', 'password123');
await loginPage.expectLoginSuccess();
const dashboard = new DashboardPage(page);
await dashboard.expectWelcome('Test User');
});
test('invalid credentials', async () => {
await loginPage.login('test@example.com', 'wrongpassword');
await loginPage.expectError('Invalid email or password');
});
test('empty fields validation', async () => {
await loginPage.submitButton.click();
await expect(loginPage.page.getByText('Email is required')).toBeVisible();
await expect(loginPage.page.getByText('Password is required')).toBeVisible();
});
test('forgot password flow', async ({ page }) => {
await loginPage.forgotPasswordLink.click();
await expect(page).toHaveURL('/forgot-password');
});
});
Fixtures
// e2e/fixtures/auth.fixture.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
interface AuthFixtures {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage;
}
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page }, use) => {
// Login before test
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('test@example.com', 'password123');
await loginPage.expectLoginSuccess();
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
export { expect };
// e2e/dashboard.spec.ts
import { test, expect } from './fixtures/auth.fixture';
test.describe('Dashboard', () => {
test('shows user data', async ({ authenticatedPage }) => {
await authenticatedPage.expectWelcome('Test User');
});
test('logout redirects to login', async ({ authenticatedPage }) => {
await authenticatedPage.logout();
});
});
Cypress Setup
Installation
npm install -D cypress @testing-library/cypress
npx cypress open
Configuration
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
retries: {
runMode: 2,
openMode: 0,
},
experimentalStudio: true,
setupNodeEvents(on, config) {
// Tasks and plugins
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
},
});
Support Commands
// cypress/support/commands.ts
import '@testing-library/cypress/add-commands';
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
mockApi(fixture: string): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('[data-testid="email-input"]').type(email);
cy.get('[data-testid="password-input"]').type(password);
cy.get('[data-testid="submit-button"]').click();
cy.url().should('include', '/dashboard');
});
});
Cypress.Commands.add('getByTestId', (testId: string) => {
return cy.get(`[data-testid="${testId}"]`);
});
Cypress.Commands.add('mockApi', (fixture: string) => {
cy.intercept('GET', '/api/**', { fixture }).as('apiCall');
});
Cypress Tests
// cypress/e2e/auth.cy.ts
describe('Authentication', () => {
beforeEach(() => {
cy.visit('/login');
});
it('logs in successfully', () => {
cy.get('[data-testid="email-input"]').type('test@example.com');
cy.get('[data-testid="password-input"]').type('password123');
cy.get('[data-testid="submit-button"]').click();
cy.url().should('include', '/dashboard');
cy.getByTestId('welcome-message').should('contain', 'Welcome');
});
it('shows error for invalid credentials', () => {
cy.get('[data-testid="email-input"]').type('test@example.com');
cy.get('[data-testid="password-input"]').type('wrongpassword');
cy.get('[data-testid="submit-button"]').click();
cy.getByTestId('error-message').should('be.visible');
cy.url().should('include', '/login');
});
it('validates required fields', () => {
cy.get('[data-testid="submit-button"]').click();
cy.contains('Email is required').should('be.visible');
cy.contains('Password is required').should('be.visible');
});
});
API Mocking in Cypress
// cypress/e2e/products.cy.ts
describe('Products', () => {
beforeEach(() => {
cy.login('test@example.com', 'password123');
});
it('displays products from API', () => {
cy.intercept('GET', '/api/products', {
fixture: 'products.json',
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.getByTestId('product-card').should('have.length', 3);
});
it('handles API error gracefully', () => {
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Server Error' },
}).as('getProductsError');
cy.visit('/products');
cy.wait('@getProductsError');
cy.getByTestId('error-message').should('contain', 'Failed to load products');
});
it('filters products', () => {
cy.intercept('GET', '/api/products?category=electronics', {
fixture: 'products-electronics.json',
}).as('getElectronics');
cy.visit('/products');
cy.getByTestId('category-filter').select('electronics');
cy.wait('@getElectronics');
cy.getByTestId('product-card').should('have.length', 2);
});
});
CI Integration
GitHub Actions
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Build
run: npm run build
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
cypress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm start
wait-on: 'http://localhost:3000'
browser: chrome
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
Accessibility Testing
// e2e/accessibility.spec.ts (Playwright)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility', () => {
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('login page has no accessibility violations', async ({ page }) => {
await page.goto('/login');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
});
// cypress/e2e/accessibility.cy.ts
import 'cypress-axe';
describe('Accessibility', () => {
beforeEach(() => {
cy.injectAxe();
});
it('homepage has no accessibility violations', () => {
cy.visit('/');
cy.checkA11y();
});
it('login form is accessible', () => {
cy.visit('/login');
cy.checkA11y('[data-testid="login-form"]');
});
});
Best Practices
- Use page objects: Maintainable selectors
- Use test IDs: Stable element selection
- Avoid sleep: Use proper waits
- Isolate tests: No dependencies between tests
- Mock external APIs: Reliable, fast tests
- Test accessibility: Include a11y checks
- Parallel execution: Faster CI
- Meaningful assertions: Clear expectations
Output Checklist
Every E2E setup should include:
- Playwright/Cypress configuration
- Page object model
- Custom commands/fixtures
- API mocking setup
- Authentication handling
- Multi-browser testing
- Accessibility tests
- CI integration
- Reporting configuration
- Screenshot/video on failure
Weekly Installs
12
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
antigravity9
claude-code9
gemini-cli8
windsurf8
github-copilot8
codex8