What Is Playwright?
Playwright is a modern browser automation framework created by Microsoft. It provides a single API to control Chromium, Firefox, and WebKit browsers. Released in 2020, it has rapidly become the most popular choice for new test automation projects.
Why Playwright?
| Feature | Playwright | Selenium |
|---|---|---|
| Auto-waiting | Built-in | Manual waits required |
| Multi-browser | Chromium, Firefox, WebKit | Requires separate drivers |
| Speed | Very fast | Moderate |
| Locators | Role-based, text, test-id | CSS, XPath, ID |
| Debugging | Trace Viewer, Inspector | Screenshots only |
| API testing | Built-in | Requires separate tool |
| Codegen | Built-in | Not available |
| Parallel execution | Native | Requires Grid |
| Languages | JS/TS, Python, Java, C# | All major languages |
Setting Up Playwright
JavaScript/TypeScript
# Create a new project
npm init playwright@latest
# This creates:
# playwright.config.ts — configuration
# tests/ — test directory
# package.json — with Playwright dependency
Python
pip install playwright
playwright install
Project Structure
project/
├── playwright.config.ts
├── tests/
│ ├── login.spec.ts
│ ├── checkout.spec.ts
│ └── search.spec.ts
├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── CheckoutPage.ts
├── fixtures/
│ └── test-data.json
└── package.json
Writing Your First Test
import { test, expect } from '@playwright/test';
test('user can login and see dashboard', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('[data-testid="email"]', 'admin@test.com');
await page.fill('[data-testid="password"]', 'secret123');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome')).toHaveText('Welcome, Admin');
});
test('invalid login shows error', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('[data-testid="email"]', 'wrong@test.com');
await page.fill('[data-testid="password"]', 'wrongpass');
await page.click('[data-testid="submit"]');
await expect(page.locator('.error')).toHaveText('Invalid credentials');
await expect(page).toHaveURL('/login');
});
Powerful Locators
Playwright provides multiple locator strategies beyond CSS and XPath:
Role-Based Locators (Recommended)
// By role — the most resilient locator strategy
await page.getByRole('button', { name: 'Sign In' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('admin@test.com');
await page.getByRole('link', { name: 'Dashboard' }).click();
await page.getByRole('heading', { name: 'Welcome' });
// By label
await page.getByLabel('Email').fill('admin@test.com');
await page.getByLabel('Password').fill('secret');
// By placeholder
await page.getByPlaceholder('Enter your email').fill('admin@test.com');
// By text
await page.getByText('Sign In').click();
await page.getByText('Welcome, Admin');
// By test ID (recommended for custom attributes)
await page.getByTestId('login-submit').click();
await page.getByTestId('email-input').fill('admin@test.com');
Locator Chaining and Filtering
// Chain locators to narrow down
await page.locator('.product-card').filter({ hasText: 'Wireless Mouse' })
.getByRole('button', { name: 'Add to Cart' }).click();
// Nth element
await page.locator('.product-card').nth(0).click();
await page.locator('.product-card').first().click();
await page.locator('.product-card').last().click();
// Has child
await page.locator('.card', { has: page.locator('.discount-badge') }).click();
Auto-Waiting
Playwright automatically waits for elements to be actionable:
// No manual waits needed — Playwright handles it
await page.click('#submit');
// Playwright waits for: element visible, stable, enabled, receiving events
await page.fill('#email', 'test@test.com');
// Playwright waits for: element visible, enabled, editable
await expect(page.locator('.result')).toHaveText('Success');
// Playwright retries until the assertion passes (within timeout)
Web-First Assertions
// These auto-retry until they pass or timeout
await expect(page).toHaveTitle('Dashboard');
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('.status')).toHaveText('Active');
await expect(page.locator('.item')).toHaveCount(5);
await expect(page.locator('#btn')).toBeVisible();
await expect(page.locator('#btn')).toBeEnabled();
await expect(page.locator('#input')).toHaveValue('hello');
await expect(page.locator('.badge')).toHaveCSS('color', 'rgb(0, 128, 0)');
Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'results.xml' }],
],
use: {
baseURL: 'https://app.example.com',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-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'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
],
});
Advanced Features
Trace Viewer
The Trace Viewer is Playwright’s killer debugging feature:
# Record traces on failure (configured above)
npx playwright test
# View the trace
npx playwright show-trace trace.zip
The trace shows: screenshots at each step, DOM snapshots, network requests, console logs, and source code.
Codegen — Record Tests
# Open browser and record actions as test code
npx playwright codegen https://app.example.com
Codegen opens a browser and records your interactions, generating Playwright test code in real-time.
API Testing (Built-in)
test('create user via API', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'Test User',
email: 'test@example.com',
},
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('Test User');
});
// Mix API and UI testing
test('admin creates user via API, verifies in UI', async ({ page, request }) => {
// Create via API (fast)
const response = await request.post('/api/users', {
data: { name: 'New User', email: 'new@test.com' }
});
const user = await response.json();
// Verify in UI
await page.goto(`/admin/users/${user.id}`);
await expect(page.locator('.user-name')).toHaveText('New User');
});
Network Interception
// Mock API responses
await page.route('/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
// Wait for specific network request
const [response] = await Promise.all([
page.waitForResponse('/api/checkout'),
page.click('#pay-button'),
]);
expect(response.status()).toBe(200);
Multiple Browser Contexts
test('two users interact in real-time', async ({ browser }) => {
const adminContext = await browser.newContext();
const userContext = await browser.newContext();
const adminPage = await adminContext.newPage();
const userPage = await userContext.newPage();
// Admin sends a message
await adminPage.goto('/admin/chat');
await adminPage.fill('#message', 'Hello user!');
await adminPage.click('#send');
// User sees the message
await userPage.goto('/chat');
await expect(userPage.locator('.message')).toHaveText('Hello user!');
await adminContext.close();
await userContext.close();
});
Page Object Model with Fixtures
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';
// test file
import { test, expect } from '../fixtures';
test('admin sees dashboard', async ({ loginPage, dashboardPage }) => {
await loginPage.goto();
await loginPage.login('admin@test.com', 'secret');
await expect(dashboardPage.welcomeMessage).toBeVisible();
});
Running Tests
# Run all tests
npx playwright test
# Run specific file
npx playwright test tests/login.spec.ts
# Run with specific browser
npx playwright test --project=firefox
# Run in headed mode (see the browser)
npx playwright test --headed
# Run with UI mode (interactive)
npx playwright test --ui
# Debug a specific test
npx playwright test --debug tests/login.spec.ts
# Generate HTML report
npx playwright show-report
Exercise: Build a Playwright Test Suite
Create a complete Playwright test suite for a web application:
- Initialize a Playwright project with TypeScript
- Configure 3 browser projects (Chrome, Firefox, WebKit)
- Create Page Objects for Login, Dashboard, and Settings
- Write 8 tests: login (valid/invalid), navigation, form submission, API + UI mix, network mock, multi-viewport, screenshot comparison
- Configure traces and screenshots on failure
- Run tests in parallel across all browsers
- Generate and review the HTML report
Key Takeaways
- Playwright’s auto-waiting eliminates most flaky tests caused by timing issues
- Role-based locators are the most resilient strategy for finding elements
- The Trace Viewer provides unmatched debugging capabilities
- Built-in API testing allows mixing fast API setup with UI verification
- Codegen records browser interactions and generates test code
- Native parallel execution across Chromium, Firefox, and WebKit
- Playwright fixtures integrate naturally with Page Object Model