Introduction
Playwright is a modern end-to-end testing framework that enables reliable cross-browser automation. This guide covers everything from basic setup to advanced testing patterns.
Why Playwright?
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| Speed | โกโกโก | โกโก | โก |
| Browser Support | All major | All major | All major |
| API Testing | โ Built-in | Limited | โ |
| Mobile | โ Native | โ | Limited |
| Auto-wait | โ Smart | โ | โ |
Getting Started
Installation
# Install Playwright
npm init playwright@latest
# Install browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
Basic Test
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('homepage has title', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});
test('login flow works', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'password123');
await page.click('[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
Locators
Finding Elements
// By text
await page.click('text=Submit');
await page.click('button:has-text("Submit")');
// By CSS
await page.click('#submit-btn');
await page.click('.btn.primary');
// By XPath
await page.click('//button[@type="submit"]');
// By role
await page.click('button[name="submit"]');
await page.getByRole('button', { name: 'Submit' }).click();
// By label
await page.fill('label:has-text("Email") >> ..input', '[email protected]');
await page.getByLabel('Email').fill('[email protected]');
// By placeholder
await page.fill('[placeholder="Enter email"]', '[email protected]');
await page.getByPlaceholder('Enter email').fill('[email protected]');
// By test ID
await page.getByTestId('submit-btn').click();
Waiting
// Auto-wait is built-in!
// But you can explicitly wait:
// Wait for element visible
await page.waitForSelector('.loading', { state: 'hidden' });
// Wait for URL
await page.waitForURL('/dashboard');
// Wait for response
await page.waitForResponse(response => response.url().includes('/api'));
// Wait for navigation
await page.waitForLoadState('networkidle');
Interactions
Forms
// Fill form
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'password');
// Checkbox
await page.check('#remember-me');
await page.uncheck('#remember-me');
// Select
await page.selectOption('#country', 'US');
await page.selectOption('#country', ['US', 'CA']);
// File upload
await page.setInputFiles('#upload', 'path/to/file.pdf');
// Drag and drop
await page.dragAndDrop('#source', '#target');
Keyboard & Mouse
// Keyboard
await page.press('[name="search"]', 'Enter');
await page.press('[name="search"]', 'Control+a');
await page.keyboard.press('Tab');
// Hover
await page.hover('.menu-item');
// Click with options
await page.click('.btn', { button: 'right' }); // right click
await page.dblclick('.btn');
await page.click('.btn', { modifiers: ['Shift'] });
Assertions
Built-in Assertions
// Expect
await expect(page).toHaveTitle('Dashboard');
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveScreenshot('homepage.png');
// Element assertions
await expect(page.locator('.user-name')).toHaveText('John Doe');
await expect(page.locator('.user-name')).toContainText('John');
await expect(page.locator('#checkbox')).toBeChecked();
await expect(page.locator('#email')).toBeVisible();
await expect(page.locator('#email')).toBeEnabled();
await expect(page.locator('#submit')).toBeDisabled();
// Count
await expect(page.locator('.item')).toHaveCount(5);
// Custom
await expect(page.locator('.total')).toPass({
timeout: 5000,
hasNot: /error/i,
});
Soft Assertions
import { expect, softExpect } from '@playwright/test';
test('soft assertions', async ({ page }) => {
await softExpect(page).toHaveTitle('Dashboard');
await softExpect(page.locator('.user')).toHaveText('John');
// Won't fail test if these fail
});
API Testing
Making Requests
import { test } from '@playwright/test';
test('API request', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John',
email: '[email protected]',
},
});
expect(response.ok()).toBeTruthy();
expect(response.status()).to);
Be(201 const data = await response.json();
expect(data.name).toBe('John');
});
Mocking API
test('with mocked API', async ({ page }) => {
await page.route('/api/user', async route => {
await route.fulfill({
status: 200,
body: JSON.stringify({ name: 'Mocked User' }),
});
});
await page.goto('/profile');
await expect(page.locator('.user-name')).toHaveText('Mocked User');
});
Authentication
Login Once
import { test as base } from '@playwright/test';
const test = base.extend({
authenticatedPage: async ({ page }, use }) {
await page.goto('/login');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'password');
await page.click('[type="submit"]');
await use(page);
},
});
test('dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
// Already logged in!
});
Storage State
// login.spec.ts
test('login and save state', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'password');
await page.click('[type="submit"]');
await page.context().storageState({ path: 'storageState.json' });
});
// other-tests.spec.ts
test.use({ storageState: 'storageState.json' });
test('dashboard', async ({ page }) => {
await page.goto('/dashboard'); // Already logged in
});
Page Object Pattern
// pages/Dashboard.ts
export class DashboardPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/dashboard');
}
get userName() {
return this.page.locator('.user-name');
}
async logout() {
await this.page.click('#logout-btn');
}
}
// tests/dashboard.spec.ts
test('dashboard shows user', async ({ page }) => {
const dashboard = new DashboardPage(page);
await dashboard.goto();
await expect(dashboard.userName).toHaveText('John Doe');
});
CI Integration
GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
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/
Configuration
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',
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'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Best Practices
1. Use Test Isolation
// โ
Good - each test is independent
test('create user', async ({ page }) => {
await createUser({ name: 'John' });
await expect(page.locator('.user')).toHaveText('John');
});
test('delete user', async ({ page }) => {
await deleteUser(1);
await expect(page.locator('.user')).not.toBeVisible();
});
// โ Bad - tests depend on each other
let userId: number;
test('create user', async ({ page }) => {
userId = await createUser({ name: 'John' });
});
test('delete user', async ({ page }) => {
await deleteUser(userId); // Depends on previous test!
});
2. Avoid Sleeps
// โ Bad - arbitrary wait
await page.waitForTimeout(2000);
// โ
Good - wait for condition
await expect(page.locator('.loaded')).toBeVisible();
3. Use Locators Wisely
// โ
Good - specific locators
await page.getByRole('button', { name: 'Submit' }).click();
// โ Bad - fragile locators
await page.click('body > div > div:nth-child(2) > button');
Conclusion
Playwright is excellent for:
- Cross-browser testing
- API testing
- Mobile testing
- CI/CD integration
- Modern web apps
Start with basic tests, then add API mocking and CI integration.
Comments