| 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
bun create playwright
bun add --dev @playwright/test
bunx playwright install
bunx playwright install --with-deps
bunx playwright --version
Configuration (playwright.config.ts)
Minimal Setup
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
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
bunx playwright test
bunx playwright test tests/login.spec.ts
bunx playwright test --headed
bunx playwright test --debug
bunx playwright test --project=chromium
bunx playwright test --ui
bunx playwright codegen http://localhost:3000
bunx playwright show-report
bunx playwright show-trace trace.zip
bunx playwright test --update-snapshots
Writing Tests
Basic Test Structure
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
import { test, expect } from '@playwright/test';
test('selectors', async ({ page }) => {
await page.goto('/');
await page.getByText('Sign in').click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('user@example.com');
await page.getByPlaceholder('Enter your name').fill('John');
await page.getByTestId('login-button').click();
await page.locator('.button-primary').click();
await page.locator('xpath=//button[text()="Submit"]').click();
await page
.locator('.card')
.filter({ hasText: 'Product' })
.getByRole('button')
.click();
});
Assertions
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('My App');
await expect(page).toHaveURL(/dashboard/);
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/);
const input = page.getByLabel('Email');
await expect(input).toHaveValue('user@example.com');
await expect(input).toBeEmpty();
await expect(page.locator('.item')).toHaveCount(5);
await expect(button).not.toBeDisabled();
});
Page Object Model
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();
}
}
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
import { test } from '@playwright/test';
test('built-in fixtures', async ({ page, context, browser }) => {
await page.goto('/');
await context.clearCookies();
const newPage = await browser.newPage();
});
Custom Fixtures
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);
},
});
import { test } from '../fixtures/auth-fixture';
test('access dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
});
Authentication State
Save Authentication State
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
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
API Testing
import { test, expect } from '@playwright/test';
test('api testing', async ({ 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);
const createResponse = await request.post('https://api.example.com/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
},
});
expect(createResponse.ok()).toBeTruthy();
const authResponse = await request.get('https://api.example.com/profile', {
headers: {
Authorization: 'Bearer token123',
},
});
expect(authResponse.ok()).toBeTruthy();
});
Network Interception
import { test, expect } from '@playwright/test';
test('mock api response', async ({ page }) => {
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]),
});
});
await page.goto('/users');
await expect(page.getByText('John')).toBeVisible();
});
test('block images', async ({ page }) => {
await page.route('**/*.{png,jpg,jpeg}', (route) => route.abort());
await page.goto('/');
});
test('intercept and modify', async ({ page }) => {
await page.route('**/api/config', async (route) => {
const response = await route.fetch();
const json = await response.json();
json.feature_flag = true;
await route.fulfill({ json });
});
await page.goto('/');
});
Visual Regression Testing
import { test, expect } from '@playwright/test';
test('visual regression', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
const header = page.locator('header');
await expect(header).toHaveScreenshot('header.png');
await expect(page).toHaveScreenshot('homepage-mobile.png', {
fullPage: true,
maxDiffPixels: 100,
});
});
Mobile Emulation
import { test, devices } from '@playwright/test';
test.use({
...devices['iPhone 13'],
});
test('mobile test', async ({ page }) => {
await page.goto('/');
});
Parallel Execution
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 1 : undefined,
});
test.describe.serial('checkout flow', () => {
test('add to cart', async ({ page }) => {
});
test('proceed to checkout', async ({ page }) => {
});
});
Debugging
Debug Mode
bunx playwright test --debug
bunx playwright test tests/login.spec.ts --debug
bunx playwright test --headed --pause-on-failure
Trace Viewer
export default defineConfig({
use: {
trace: 'on-first-retry',
},
});
bunx playwright show-trace trace.zip
Trace viewer shows:
- Timeline of actions
- Screenshots at each step
- Network activity
- Console logs
- Source code
CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Playwright Browsers
run: bunx playwright install --with-deps
- name: Run Playwright tests
run: bunx playwright test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
GitLab CI
playwright:
image: mcr.microsoft.com/playwright:v1.40.0-jammy
stage: test
script:
- bun install --frozen-lockfile
- bunx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 1 week
Best Practices
Use Built-in Locators
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('user@example.com');
await page.getByText('Welcome').click();
await page.locator('#submit-btn-123').click();
await page.locator('div > div > input').fill('user@example.com');
Auto-waiting
await page.getByRole('button').click();
await page.waitForTimeout(1000);
await page.getByRole('button').click();
Isolate Tests
test('test 1', async ({ page }) => {
await page.goto('/');
});
test('test 2', async ({ page }) => {
await page.goto('/');
});
Use Test IDs for Dynamic Content
<button data-testid="submit-btn">Submit</button>
await page.getByTestId('submit-btn').click();
Troubleshooting
Tests Timing Out
test('slow test', async ({ page }) => {
test.setTimeout(60000);
await page.goto('/slow-page');
});
export default defineConfig({
timeout: 60000,
expect: {
timeout: 10000,
},
});
Flaky Tests
bunx playwright test --repeat-each=10
bunx playwright test --retries=3
Element Not Found
await page.waitForSelector('.element');
await expect(page.locator('.element')).toBeVisible();
Browser Not Found
bunx playwright install
bunx playwright install --help
References