Installation & Setup
Project Initialization
# Install Playwright
npm init playwright@latest
# Or add to an existing project
npm install -D @playwright/test
npx playwright installConfiguration File
// 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',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile testing
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
// Start server before tests
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});webServer option
The webServer config
automatically starts the dev server before running tests.
Writing Basic Tests
// e2e/todo.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Todo App', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display the home page', async ({ page }) => {
// Verify page title
await expect(page).toHaveTitle(/Todo/);
// Verify main heading
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
test('should create a new todo', async ({ page }) => {
// Type text in the input field
await page.getByPlaceholder('Enter a todo').fill('New todo item');
// Click the add button
await page.getByRole('button', { name: 'Add' }).click();
// Verify it was added to the list
await expect(page.getByText('New todo item')).toBeVisible();
});
test('should toggle todo completion', async ({ page }) => {
// Create a todo
await page.getByPlaceholder('Enter a todo').fill('Test todo');
await page.getByRole('button', { name: 'Add' }).click();
// Click the checkbox
await page.getByRole('checkbox').first().click();
// Verify completed state (strikethrough style)
await expect(page.getByText('Test todo')).toHaveClass(/line-through/);
});
test('should delete a todo', async ({ page }) => {
// Create a todo
await page.getByPlaceholder('Enter a todo').fill('Todo to delete');
await page.getByRole('button', { name: 'Add' }).click();
// Click the delete button
await page.getByRole('button', { name: 'Delete' }).click();
// Verify it was removed from the list
await expect(page.getByText('Todo to delete')).not.toBeVisible();
});
});Running Tests
# Run all tests
npx playwright test
# Run in UI mode (useful for debugging)
npx playwright test --ui
# Specific browser only
npx playwright test --project=chromium
# Specific file only
npx playwright test e2e/todo.spec.ts
# Disable headless mode (show browser)
npx playwright test --headed
# Open test report
npx playwright show-reportSelector Best Practices
Recommended
getByRole('button', {name: 'Add'})getByText('Todo List')getByPlaceholder('Enter...')getByTestId('todo-list')
Avoid
locator('.btn-primary')locator('#submit-btn')locator('div > button:nth-child(2)')
Priority order
getByRole >
getByText >
getByTestId >
locator
API Mocking
You can test the frontend without a real backend.
Use page.route() to mock API responses.
// e2e/with-mock.spec.ts
import { test, expect } from '@playwright/test';
test('should display mocked todos', async ({ page }) => {
// Mock API response
await page.route('**/api/v1/todos**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [
{ id: '1', title: 'Mocked Todo 1', is_completed: false },
{ id: '2', title: 'Mocked Todo 2', is_completed: true },
],
total: 2,
}),
});
});
await page.goto('/');
// Verify mocked data
await expect(page.getByText('Mocked Todo 1')).toBeVisible();
await expect(page.getByText('Mocked Todo 2')).toBeVisible();
});
test('should handle API error', async ({ page }) => {
// Mock API error
await page.route('**/api/v1/todos**', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' }),
});
});
await page.goto('/');
// Verify error message
await expect(page.getByText(/error|failed/i)).toBeVisible();
});Visual Regression Testing
Compare screenshots to detect unintended UI changes.
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test';
test('visual regression test', async ({ page }) => {
await page.goto('/');
// Compare full page screenshot
await expect(page).toHaveScreenshot('home-page.png');
});
test('component screenshot', async ({ page }) => {
await page.goto('/');
// Screenshot of a specific component only
const todoList = page.locator('[data-testid="todo-list"]');
await expect(todoList).toHaveScreenshot('todo-list.png');
});
test('full page screenshot with interactions', async ({ page }) => {
await page.goto('/');
// Screenshot after adding a todo
await page.getByPlaceholder('Enter a todo').fill('Screenshot test');
await page.getByRole('button', { name: 'Add' }).click();
await expect(page).toHaveScreenshot('with-todo.png');
});# First run: generate baseline screenshots
npx playwright test --update-snapshots
# Subsequent runs: compare against baseline
npx playwright testOS-specific differences
Screenshots may render differently across operating systems.
For consistent results in CI, use Docker or configure a
threshold.
CI/CD Integration
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30Playwright browser caching
To reduce browser installation time in CI, refer to the caching guide.
Debugging Tips
# UI mode (most recommended)
npx playwright test --ui
# Debug mode (breakpoint support)
npx playwright test --debug
# Trace recording (trace.zip)
npx playwright test --trace on
# Codegen: convert browser actions to code
npx playwright codegen localhost:5173UI Mode
Run tests step by step and inspect the DOM state at each step.
Codegen
Interact with the browser directly and test code is automatically generated.
Best Practices
DO
- ✅ Test only critical user scenarios
- ✅ Use meaningful selectors
- ✅ Ensure stability with API mocking
- ✅ Capture screenshots on failure
- ✅ Run in parallel on CI
DON'T
- ❌ Test everything with E2E
- ❌ Rely on CSS selectors
- ❌ Use fixed wait times
- ❌ Share data between tests
- ❌ Write too many E2E tests
Testing complete!
You've completed the entire testing guide. Now go back to the Learning Roadmap to explore other topics, or start writing tests in your own projects.