Playwright E2E testing setup and configuration. Use when setting up end-to-end tests.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers Playwright end-to-end testing setup for React applications.
Use this skill when:
TEST USER JOURNEYS - E2E tests verify complete user flows. Focus on critical paths that matter to users.
npm init playwright@latest
Or manually:
npm install -D @playwright/test
npx playwright install
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/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' }],
['list'],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
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 dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
tests/
├── e2e/
│ ├── pages/ # Page Objects
│ │ ├── LoginPage.ts
│ │ ├── DashboardPage.ts
│ │ └── BasePage.ts
│ ├── fixtures/ # Custom fixtures
│ │ └── index.ts
│ ├── auth.spec.ts
│ ├── dashboard.spec.ts
│ └── checkout.spec.ts
└── playwright.config.ts
import { test, expect } from '@playwright/test';
test.describe('Home Page', () => {
test('displays welcome message', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
test('navigates to about page', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL('/about');
});
});
// tests/e2e/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export class BasePage {
readonly page: Page;
readonly header: Locator;
readonly footer: Locator;
constructor(page: Page) {
this.page = page;
this.header = page.getByRole('banner');
this.footer = page.getByRole('contentinfo');
}
async navigate(path: string): Promise<void> {
await this.page.goto(path);
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
}
// tests/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;
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.getByRole('alert');
}
async goto(): Promise<void> {
await this.navigate('/login');
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string): Promise<void> {
await expect(this.errorMessage).toContainText(message);
}
async expectLoggedIn(): Promise<void> {
await expect(this.page).toHaveURL('/dashboard');
}
}
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test.describe('Authentication', () => {
test('logs in with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await loginPage.expectLoggedIn();
});
test('shows error with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrongpassword');
await loginPage.expectError('Invalid credentials');
});
});
// tests/e2e/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
interface Pages {
loginPage: LoginPage;
dashboardPage: DashboardPage;
}
interface TestFixtures {
authenticatedPage: void;
}
export const test = base.extend<Pages & TestFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
authenticatedPage: async ({ page }, use) => {
// Setup: Login before test
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
// Run the test
await use();
// Teardown: Logout after test (if needed)
},
});
export { expect } from '@playwright/test';
// Usage
import { test, expect } from '../fixtures';
test.describe('Dashboard', () => {
test('shows user info when authenticated', async ({ page, authenticatedPage }) => {
await expect(page.getByText('Welcome back')).toBeVisible();
});
});
// tests/e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'tests/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'tests/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
test('displays mocked data', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Mock User')).toBeVisible();
});
# Run all tests
npx playwright test
# Run specific file
npx playwright test auth.spec.ts
# Run with UI mode
npx playwright test --ui
# Run in headed mode
npx playwright test --headed
# Run specific project
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
# Show report
npx playwright show-report
# Update snapshots
npx playwright test --update-snapshots
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
--debug to step through tests