| name | e2e-testing |
| description | Playwright E2E testing with Page Objects, API mocking, and Chrome DevTools. Use when writing end-to-end tests, testing user flows, or debugging UI behavior. |
| when_to_use | When writing Playwright tests, creating Page Objects, mocking API responses, or testing user flows end-to-end. |
E2E Testing Skill
Patterns for Playwright end-to-end testing.
When to Use This Skill
- Writing E2E tests for user flows
- Creating Page Objects
- Mocking API responses
- Debugging with Chrome DevTools
- Testing responsive layouts
Reference Documentation
For detailed patterns and conventions, see:
Quick Reference
Basic Test Structure
import { test, expect, Page } from '@playwright/test'
test.describe('Task Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
})
test('creates a new task', async ({ page }) => {
await page.click('[data-testid="add-task-btn"]')
await page.fill('[data-testid="task-title-input"]', 'New E2E Task')
await page.selectOption('[data-testid="status-select"]', 'Todo')
await page.click('[data-testid="save-task-btn"]')
await expect(page.locator('[data-testid="task-list"]'))
.toContainText('New E2E Task')
})
test('filters tasks by status', async ({ page }) => {
await page.click('[data-testid="filter-status"]')
await page.click('[data-testid="filter-option-done"]')
const tasks = page.locator('[data-testid="task-card"]')
await expect(tasks).toHaveCount(2)
})
})
Page Object Pattern
import { Page, Locator } from '@playwright/test'
export class TasksPage {
readonly page: Page
readonly addTaskButton: Locator
readonly taskList: Locator
readonly taskCards: Locator
constructor(page: Page) {
this.page = page
this.addTaskButton = page.locator('[data-testid="add-task-btn"]')
this.taskList = page.locator('[data-testid="task-list"]')
this.taskCards = page.locator('[data-testid="task-card"]')
}
async goto() {
await this.page.goto('/tasks')
await this.page.waitForLoadState('networkidle')
}
() {
..()
..(, title)
..(, status)
..()
}
(): <> {
..()
}
(: ): {
..({ : title })
}
}
(, ({ page }) => {
tasksPage = (page)
tasksPage.()
tasksPage.(, )
( tasksPage.()).()
})
Critical Rules
- Use
data-testid selectors – never CSS classes
- Wait for
networkidle before assertions
- Use Page Objects for complex pages
- Never use hard-coded waits (
waitForTimeout)
- Mock API for isolation when needed
API Mocking
test('shows error on API failure', async ({ page }) => {
await page.route('**/api/tasks', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' })
})
})
await page.goto('/tasks')
await expect(page.locator('[data-testid="error-message"]'))
.toBeVisible()
})
test('shows empty state when no tasks', async ({ page }) => {
await page.route('**/api/tasks', route => {
route.fulfill({
status: 200,
body: JSON.stringify([])
})
})
await page.goto('/tasks')
await expect(page.locator('[data-testid="empty-state"]'))
.toBeVisible()
})
Waiting Strategies
await page.waitForSelector('[data-testid="task-card"]')
await page.waitForLoadState('networkidle')
await page.waitForResponse('**/api/tasks')
await expect(page.locator('[data-testid="btn"]'))
.toBeEnabled()
Form Testing
test('validates form fields', async ({ page }) => {
await page.click('[data-testid="submit-btn"]')
await expect(page.locator('[data-testid="title-error"]'))
.toHaveText('Title is required')
})
test('fills form completely', async ({ page }) => {
await page.fill('[data-testid="title-input"]', 'Task Title')
await page.fill('[data-testid="description-input"]', 'Description')
await page.selectOption('[data-testid="priority-select"]', 'High')
await page.check('[data-testid="vital-checkbox"]')
await page.click('[data-testid="submit-btn"]')
await expect(page.locator('[data-testid="success-toast"]'))
.toBeVisible()
})
Visual Testing
test('matches screenshot', async ({ page }) => {
await page.goto('/tasks')
await expect(page).toHaveScreenshot('tasks-page.png')
})
test('component visual regression', async ({ page }) => {
const card = page.locator('[data-testid="task-card"]').first()
await expect(card).toHaveScreenshot('task-card.png')
})
DevTools Integration
Use Chrome DevTools MCP for:
Commands
npx playwright test
npx playwright test frontend/e2e/tasks.spec.ts
npx playwright test --debug
npx playwright test --ui
npx playwright show-report
npx playwright test --update-snapshots
Test Coverage Checklist
For each user flow: