| name | playwright |
| description | Playwright modern end-to-end testing framework with cross-browser automation, auto-wait, and built-in test runner |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":{"summary":"Playwright modern end-to-end testing framework with cross-browser automation, auto-wait, and built-in test runner","when_to_use":"When writing tests, implementing playwright-e2e-testing, or ensuring code quality.","quick_start":"1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."}} |
Playwright E2E Testing Skill
progressive_disclosure:
entry_point:
summary: "Modern E2E testing framework with cross-browser automation and built-in test runner"
when_to_use:
- "When testing web applications end-to-end"
- "When needing cross-browser testing"
- "When testing user flows and interactions"
- "When needing screenshot/video recording"
quick_start:
- "npm init playwright@latest"
- "Choose TypeScript and test location"
- "npx playwright test"
- "npx playwright show-report"
token_estimate:
entry: 75-90
full: 4200-5200
Overview
Playwright is a modern end-to-end testing framework that provides cross-browser automation with a built-in test runner, auto-wait mechanisms, and excellent developer experience.
Key Features
- Auto-wait: Automatically waits for elements to be ready
- Cross-browser: Chromium, Firefox, WebKit support
- Built-in runner: Parallel execution, retries, reporters
- Network control: Mock and intercept network requests
- Debugging: UI mode, trace viewer, inspector
Installation
npm init playwright@latest
npm install -D @playwright/test
npx playwright install
Configuration
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',
use: {
baseURL: 'http://localhost:3000',
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'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Fundamentals
Basic Test Structure
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://example.com');
const title = page.locator('h1');
await expect(title).toBeVisible();
await expect(title).toHaveText('Example Domain');
await expect(page).toHaveTitle(/Example/);
});
test.describe('User authentication', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'testuser');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'invalid');
await page.fill('[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toHaveText('Invalid credentials');
});
});
Test Hooks
import { test, expect } from '@playwright/test';
test.describe('Dashboard tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
});
test.afterEach(async ({ page }) => {
await page.close();
});
test.beforeAll(async ({ browser }) => {
console.log('Starting test suite');
});
test.afterAll(async ({ browser }) => {
console.log('Test suite complete');
});
test('displays user data', async ({ page }) => {
await expect(page.locator('.user-name')).toBeVisible();
});
});
Locator Strategies
Best Practice: Role-based Locators
import { test, expect } from '@playwright/test';
test('accessible locators', async ({ page }) => {
await page.goto('/form');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('link', { name: 'Learn more' }).click();
await page.getByLabel('Password').fill('secret123');
await page.getByPlaceholder('Search...').fill('query');
await page.getByText('Welcome back').click();
await page.getByText(/hello/i).isVisible();
await page.getByTestId('user-profile').click();
await page.getByTitle('Close dialog').click();
await page.getByAltText('User avatar').click();
});
CSS and XPath Locators
test('CSS and XPath locators', async ({ page }) => {
await page.locator('button.primary').click();
await page.locator('#user-menu').click();
await page.locator('[data-testid="submit-btn"]').click();
await page.locator('div.card:first-child').click();
await page.locator('xpath=//button[contains(text(), "Submit")]').click();
const form = page.locator('form#login-form');
await form.locator('input[name="email"]').fill('user@example.com');
await form.locator('button[type="submit"]').click();
await page.getByRole('listitem')
.filter({ hasText: 'Product 1' })
.getByRole('button', { name: 'Add to cart' })
.click();
});
Page Object Model
Page Class Pattern
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message: string) {
await this.errorMessage.waitFor({ state: 'visible' });
await expect(this.errorMessage).toHaveText(message);
}
}
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.locator('.welcome-message');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
}
async waitForLoad() {
await this.welcomeMessage.waitFor({ state: 'visible' });
}
async logout() {
await this.logoutButton.click();
}
}
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('successful login flow', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboard = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await dashboard.waitForLoad();
await expect(dashboard.welcomeMessage).toContainText('Welcome');
});
Component Pattern
import { Page, Locator } from '@playwright/test';
export class NavigationComponent {
readonly page: Page;
readonly homeLink: Locator;
readonly profileLink: Locator;
readonly searchInput: Locator;
constructor(page: Page) {
this.page = page;
const nav = page.locator('nav');
this.homeLink = nav.getByRole('link', { name: 'Home' });
this.profileLink = nav.getByRole('link', { name: 'Profile' });
this.searchInput = nav.getByPlaceholder('Search...');
}
async navigateToProfile() {
await this.profileLink.click();
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
}
}
User Interactions
Form Interactions
test('form interactions', async ({ page }) => {
await page.goto('/form');
await page.fill('input[name="email"]', 'user@example.com');
await page.type('textarea[name="message"]', 'Hello', { delay: 100 });
await page.check('input[type="checkbox"][name="subscribe"]');
await page.uncheck('input[type="checkbox"][name="spam"]');
await page.check('input[type="radio"][value="option1"]');
await page.selectOption('select[name="country"]', 'US');
await page.selectOption('select[name="color"]', { label: 'Blue' });
await page.selectOption('select[name="size"]', { value: 'large' });
await page.selectOption('select[multiple]', ['value1', 'value2']);
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', [
'file1.jpg',
'file2.jpg'
]);
await page.setInputFiles('input[type="file"]', []);
});
Mouse and Keyboard
test('mouse and keyboard interactions', async ({ page }) => {
await page.click('button');
await page.dblclick('button');
await page.click('button', { button: 'right' });
await page.click('button', { modifiers: ['Shift'] });
await page.hover('.tooltip-trigger');
await expect(page.locator('.tooltip')).toBeVisible();
await page.dragAndDrop('#draggable', '#droppable');
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');
await page.keyboard.type('Hello World');
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowDown');
await page.keyboard.up('Shift');
await page.focus('input[name="email"]');
await page.fill('input[name="email"]', 'test@example.com');
});
Waiting Strategies
test('waiting strategies', async ({ page }) => {
await page.waitForSelector('.dynamic-content');
await page.waitForSelector('.modal', { state: 'visible' });
await page.waitForSelector('.loading', { state: 'hidden' });
await page.waitForLoadState('load');
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
await page.waitForURL('**/dashboard');
await page.waitForURL(/\/product\/\d+/);
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length > 5;
});
await page.waitForTimeout(1000);
await page.waitForEvent('load');
await page.waitForEvent('popup');
});
Assertions
Common Assertions
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('.header')).toBeVisible();
await expect(page.locator('.loading')).toBeHidden();
await expect(page.locator('.optional')).not.toBeVisible();
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('h1')).toContainText('Dash');
await expect(page.locator('.message')).toHaveText(/welcome/i);
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('input')).toHaveAttribute('type', 'email');
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('.button')).toHaveClass('btn-primary');
await expect(page.locator('.button')).toHaveClass(/btn-/);
await expect(page.locator('.element')).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(page.locator('.item')).toHaveCount(5);
await expect(page).toHaveURL('http://localhost:3000/dashboard');
await expect(page).toHaveURL(/dashboard$/);
await expect(page).toHaveTitle('Dashboard - My App');
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveScreenshot('dashboard.png');
await expect(page.locator('.widget')).toHaveScreenshot('widget.png');
});
Custom Assertions
test('custom matchers', async ({ page }) => {
await expect.soft(page.locator('.title')).toHaveText('Welcome');
await expect.soft(page.locator('.subtitle')).toBeVisible();
const items = page.locator('.item');
await expect(items).toHaveCount(3);
await expect(items.nth(0)).toContainText('First');
await expect(items.nth(1)).toContainText('Second');
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.ok()).toBeTruthy();
}).toPass({
timeout: 10000,
intervals: [1000, 2000, 5000],
});
});
Authentication Patterns
Storage State Pattern
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'testuser');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: authFile,
},
dependencies: ['setup'],
},
],
});
test('view dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('.user-menu')).toBeVisible();
});
Multiple User Roles
import { test as base } from '@playwright/test';
type Fixtures = {
adminPage: Page;
userPage: Page;
};
export const test = base.extend<Fixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
import { test } from '../fixtures/auth';
test('admin can access admin panel', async ({ adminPage }) => {
await adminPage.goto('/admin');
await expect(adminPage.locator('.admin-panel')).toBeVisible();
});
test('regular user cannot access admin panel', async ({ userPage }) => {
await userPage.goto('/admin');
await expect(userPage.locator('.access-denied')).toBeVisible();
});
Network Control
Request Mocking
test('mock API responses', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
users: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
],
}),
});
});
await page.goto('/users');
await expect(page.locator('.user-list')).toContainText('John Doe');
});
test('mock with conditions', async ({ page }) => {
await page.route('**/api/**', route => {
const url = route.request().url();
if (url.includes('/users/1')) {
route.fulfill({
status: 200,
body: JSON.stringify({ id: 1, name: 'Test User' }),
});
} else if (url.includes('/users')) {
route.fulfill({
status: 200,
body: JSON.stringify({ users: [] }),
});
} else {
route.continue();
}
});
});
test('simulate network errors', async ({ page }) => {
await page.route('**/api/data', route => {
route.abort('failed');
});
await page.goto('/data');
await expect(page.locator('.error-message')).toBeVisible();
});
Request Interception
test('intercept and modify requests', async ({ page }) => {
await page.route('**/api/**', route => {
const headers = route.request().headers();
route.continue({
headers: {
...headers,
'X-Custom-Header': 'test-value',
},
});
});
await page.route('**/api/submit', route => {
const postData = route.request().postDataJSON();
route.continue({
postData: JSON.stringify({
...postData,
timestamp: Date.now(),
}),
});
});
});
test('wait for API response', async ({ page }) => {
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button#load-users');
const response = await responsePromise;
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.users).toHaveLength(10);
});
Test Organization
Custom Fixtures
import { test as base } from '@playwright/test';
type TodoFixtures = {
todoPage: TodoPage;
createTodo: (title: string) => Promise<void>;
};
export const test = base.extend<TodoFixtures>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
},
createTodo: async ({ page }, use) => {
const create = async (title: string) => {
await page.fill('.new-todo', title);
await page.press('.new-todo', 'Enter');
};
await use(create);
},
});
import { test } from '../fixtures/todos';
test('can create new todo', async ({ todoPage, createTodo }) => {
await createTodo('Buy groceries');
await expect(todoPage.todoItems).toHaveCount(1);
await expect(todoPage.todoItems).toHaveText('Buy groceries');
});
Test Tags and Filtering
test('smoke test', { tag: '@smoke' }, async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('Home');
});
test('regression test', { tag: ['@regression', '@critical'] }, async ({ page }) => {
});
Visual Testing
Screenshot Comparison
test('visual regression', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100,
});
await expect(page.locator('.widget')).toHaveScreenshot('widget.png');
await expect(page).toHaveScreenshot('full-page.png', {
fullPage: true,
});
await expect(page).toHaveScreenshot('masked.png', {
mask: [page.locator('.timestamp'), page.locator('.avatar')],
});
await expect(page).toHaveScreenshot('comparison.png', {
maxDiffPixelRatio: 0.05,
});
});
Video and Trace
export default defineConfig({
use: {
video: 'retain-on-failure',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});
test('record video', async ({ page }) => {
await page.goto('/');
});
Parallel Execution
Test Sharding
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined,
});
Serial Tests
test.describe.configure({ mode: 'serial' });
test.describe('order matters', () => {
let orderId: string;
test('create order', async ({ page }) => {
orderId = await createOrder(page);
});
test('verify order', async ({ page }) => {
await verifyOrder(page, orderId);
});
});
CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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 Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Docker
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
Debugging
UI Mode
npx playwright test --ui
npx playwright test --debug login.spec.ts
npx playwright test --headed --slow-mo=1000
Trace Viewer
test('with trace', async ({ page }) => {
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.goto('/');
await page.context().tracing.stop({ path: 'trace.zip' });
});
Console Logs
test('capture console', async ({ page }) => {
page.on('console', msg => console.log(`Browser: ${msg.text()}`));
page.on('pageerror', error => console.error(`Error: ${error.message}`));
await page.goto('/');
});
Best Practices
1. Use Stable Locators
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.click('button.btn-primary.submit-btn');
await page.fill('div > form > input:nth-child(3)');
2. Leverage Auto-Waiting
await page.click('button');
await expect(page.locator('.result')).toBeVisible();
await page.waitForTimeout(2000);
await page.click('button');
3. Use Page Object Model
const loginPage = new LoginPage(page);
await loginPage.login('user', 'pass');
await page.fill('[name="username"]', 'user');
await page.fill('[name="password"]', 'pass');
4. Parallel-Safe Tests
test('user signup', async ({ page }) => {
const uniqueEmail = `user-${Date.now()}@test.com`;
await signUp(page, uniqueEmail);
});
test('user signup', async ({ page }) => {
await signUp(page, 'test@test.com');
});
5. Handle Flakiness
await page.goto('/', { waitUntil: 'networkidle' });
await expect(page.locator('.data')).toBeVisible();
test.describe(() => {
test.use({ retries: 2 });
test('flaky test', async ({ page }) => {
});
});
Common Patterns
Multi-Page Scenarios
test('popup handling', async ({ page, context }) => {
const popupPromise = context.waitForEvent('page');
await page.click('a[target="_blank"]');
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup).toHaveTitle('New Window');
await popup.close();
});
Conditional Logic
test('handle optional elements', async ({ page }) => {
await page.goto('/');
const modal = page.locator('.modal');
if (await modal.isVisible()) {
await page.click('.modal .close-button');
}
const cookieBanner = page.locator('.cookie-banner');
if ((await cookieBanner.count()) > 0) {
await page.click('.accept-cookies');
}
});
Data-Driven Tests
const testCases = [
{ input: 'hello', expected: 'HELLO' },
{ input: 'World', expected: 'WORLD' },
{ input: '123', expected: '123' },
];
for (const { input, expected } of testCases) {
test(`transforms "${input}" to "${expected}"`, async ({ page }) => {
await page.goto('/transform');
await page.fill('input', input);
await page.click('button');
await expect(page.locator('.result')).toHaveText(expected);
});
}
Resources