- created
- 2025-12-16T00:00:00.000Z
- modified
- 2025-12-16T00:00:00.000Z
- reviewed
- 2025-12-16T00:00:00.000Z
- name
- Playwright Testing
- description
- Playwright end-to-end testing for web applications. Cross-browser testing (Chromium, Firefox, WebKit),
visual regression, API testing, mobile emulation. Use when writing E2E tests, testing across browsers,
or setting up automated UI testing workflows.
- allowed-tools
- Glob, Grep, Read, Bash, Edit, Write, TodoWrite, WebFetch, WebSearch, BashOutput, KillShell
# Playwright Testing
Playwright is a modern end-to-end testing framework for web applications. It provides reliable, fast, and cross-browser testing with excellent developer experience.
## Core Expertise
**What is Playwright?**
- **Cross-browser**: Test on Chromium, Firefox, WebKit (Safari)
- **Reliable**: Auto-wait, auto-retry, no flaky tests
- **Fast**: Parallel execution, browser context isolation
- **Modern**: TypeScript-first, async/await, auto-complete
- **Multi-platform**: Windows, macOS, Linux
**Key Capabilities**
- End-to-end UI testing
- API testing (REST, GraphQL)
- Visual regression testing
- Mobile emulation
- Network interception and mocking
- Authentication state management
- Parallel test execution
- Video and screenshot capture
- Trace viewer for debugging
## Installation
```bash
# Initialize Playwright (recommended)
bun create playwright
# Or install manually
bun add --dev @playwright/test
# Install browsers
bunx playwright install
# Install with system dependencies (Linux)
bunx playwright install --with-deps
# Verify installation
bunx playwright --version
```
## Configuration (playwright.config.ts)
### Minimal Setup
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
```
### Recommended Production Setup
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
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' }],
],
timeout: 30000,
expect: {
timeout: 5000,
},
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000,
},
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 13'] },
},
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
```
## Essential Commands
```bash
# Run all tests
bunx playwright test
# Run specific test file
bunx playwright test tests/login.spec.ts
# Run tests in headed mode (see browser)
bunx playwright test --headed
# Run tests in debug mode
bunx playwright test --debug
# Run specific project (browser)
bunx playwright test --project=chromium
# Run tests in UI mode
bunx playwright test --ui
# Generate tests (record interactions)
bunx playwright codegen http://localhost:3000
# Open last test report
bunx playwright show-report
# Open trace viewer
bunx playwright show-trace trace.zip
# Update snapshots
bunx playwright test --update-snapshots
```
## Writing Tests
### Basic Test Structure
```typescript
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});
test.describe('login flow', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
});
```
### Selectors and Locators
```typescript
import { test, expect } from '@playwright/test';
test('selectors', async ({ page }) => {
await page.goto('/');
// Text selector (recommended)
await page.getByText('Sign in').click();
await page.getByRole('button', { name: 'Submit' }).click();
// Label selector
await page.getByLabel('Email').fill('user@example.com');
// Placeholder selector
await page.getByPlaceholder('Enter your name').fill('John');
// Test ID selector
await page.getByTestId('login-button').click();
// CSS selector
await page.locator('.button-primary').click();
// XPath selector
await page.locator('xpath=//button[text()="Submit"]').click();
// Chaining selectors
await page
.locator('.card')
.filter({ hasText: 'Product' })
.getByRole('button')
.click();
});
```
### Assertions
```typescript
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('/');
// Page assertions
await expect(page).toHaveTitle('My App');
await expect(page).toHaveURL(/dashboard/);
// Element assertions
const button = page.getByRole('button');
await expect(button).toBeVisible();
await expect(button).toBeEnabled();
await expect(button).toHaveText('Submit');
await expect(button).toContainText('Sub');
await expect(button).toHaveAttribute('type', 'submit');
await expect(button).toHaveClass(/btn-primary/);
// Input assertions
const input = page.getByLabel('Email');
await expect(input).toHaveValue('user@example.com');
await expect(input).toBeEmpty();
// Count assertions
await expect(page.locator('.item')).toHaveCount(5);
// Negation
await expect(button).not.toBeDisabled();
});
```
### Page Object Model
```typescript
// page-objects/login-page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../page-objects/login-page';
test('login with page object', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
```
## Fixtures
### Built-in Fixtures
```typescript
import { test } from '@playwright/test';
test('built-in fixtures', async ({ page, context, browser }) => {
// page - isolated browser page
await page.goto('/');
// context - browser context (cookies, storage)
await context.clearCookies();
// browser - browser instance
const newPage = await browser.newPage();
});
```
### Custom Fixtures
```typescript
// fixtures/auth-fixture.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../page-objects/login-page';
type AuthFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await use(page);
},
});
// tests/dashboard.spec.ts
import { test } from '../fixtures/auth-fixture';
test('access dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
// User is already logged in
});
```
## Authentication State
### Save Authentication State
```typescript
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@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 });
});
```
### Use Saved State
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
```
## API Testing
```typescript
import { test, expect } from '@playwright/test';
test('api testing', async ({ request }) => {
// GET request
const response = await request.get('https://api.example.com/users');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toHaveLength(10);
// POST request
const createResponse = await request.post('https://api.example.com/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
},
});
View on GitHub