TL;DR
- Playwright is Microsoft’s browser automation framework with auto-wait and built-in assertions
- Supports Chromium, Firefox, and WebKit with a single API
- TypeScript-first with excellent IDE support and code generation
- Parallel execution out of the box — runs tests faster than Selenium or Cypress
- Trace viewer and video recording for debugging failed tests
Best for: Teams wanting modern tooling, TypeScript support, and fast parallel execution Skip if: Need Safari on real devices or have large existing Selenium infrastructure Read time: 15 minutes
Your Selenium tests run for 45 minutes. Your Cypress tests can’t run in parallel without paying for their cloud. Your testers spend hours debugging flaky waits.
Playwright solves these problems. Auto-wait eliminates timing issues. Parallel execution is free and built-in. The trace viewer shows exactly what happened when a test failed.
What is Playwright?
Playwright is an open-source browser automation framework from Microsoft. It controls Chromium, Firefox, and WebKit through a unified API.
Key features:
- Auto-wait — waits for elements to be actionable before interacting
- Web-first assertions — built-in retry logic for assertions
- Parallel execution — runs tests across multiple workers by default
- Trace viewer — time-travel debugging for failed tests
- Codegen — generates tests by recording browser actions
Installation and Setup
Create New Project
# Create new Playwright project
npm init playwright@latest
# Answer prompts:
# - TypeScript or JavaScript? → TypeScript
# - Where to put tests? → tests
# - Add GitHub Actions? → Yes
# - Install browsers? → Yes
Project Structure
my-project/
├── tests/
│ └── example.spec.ts
├── playwright.config.ts
├── package.json
└── .github/
└── workflows/
└── playwright.yml
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 ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['list']
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
],
});
Writing Your First Test
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test('user can login with valid credentials', async ({ page }) => {
// Navigate
await page.goto('/login');
// Fill form
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
// Submit
await page.getByRole('button', { name: 'Sign In' }).click();
// Assert
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('wrong@example.com');
await page.getByLabel('Password').fill('wrongpass');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
await expect(page).toHaveURL('/login');
});
Running Tests
# Run all tests
npx playwright test
# Run specific file
npx playwright test tests/login.spec.ts
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific browser
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
# UI mode (interactive)
npx playwright test --ui
Locators: Finding Elements
Playwright recommends user-facing locators over CSS/XPath selectors.
Recommended Locators
// By role (accessibility-based)
page.getByRole('button', { name: 'Submit' })
page.getByRole('link', { name: 'Home' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('checkbox', { name: 'Remember me' })
// By label (form inputs)
page.getByLabel('Email')
page.getByLabel('Password')
// By placeholder
page.getByPlaceholder('Search...')
// By text
page.getByText('Welcome')
page.getByText('Welcome', { exact: true })
// By test ID (when other options fail)
page.getByTestId('submit-button')
CSS and XPath (When Needed)
// CSS selector
page.locator('button.primary')
page.locator('[data-testid="submit"]')
page.locator('#login-form input[type="email"]')
// XPath
page.locator('xpath=//button[contains(text(), "Submit")]')
// Chaining locators
page.locator('.card').filter({ hasText: 'Premium' }).getByRole('button')
Locator Best Practices
| Priority | Locator | Example |
|---|---|---|
| 1 | Role | getByRole('button', { name: 'Submit' }) |
| 2 | Label | getByLabel('Email') |
| 3 | Placeholder | getByPlaceholder('Search') |
| 4 | Text | getByText('Welcome') |
| 5 | Test ID | getByTestId('submit-btn') |
| 6 | CSS | locator('.btn-primary') |
Assertions
Playwright’s assertions auto-retry until timeout.
Common Assertions
import { test, expect } from '@playwright/test';
test('assertions examples', async ({ page }) => {
await page.goto('/dashboard');
// Visibility
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByText('Loading')).toBeHidden();
// Text content
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByRole('heading')).toContainText('Dash');
// Attributes
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toBeDisabled();
await expect(page.getByRole('checkbox')).toBeChecked();
// URL and title
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('My App - Dashboard');
// Count
await expect(page.getByRole('listitem')).toHaveCount(5);
// Value
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
});
Soft Assertions
test('soft assertions continue after failure', async ({ page }) => {
await page.goto('/profile');
// Soft assertions don't stop the test
await expect.soft(page.getByText('Name')).toBeVisible();
await expect.soft(page.getByText('Email')).toBeVisible();
await expect.soft(page.getByText('Phone')).toBeVisible();
// Test continues even if some soft assertions fail
await page.getByRole('button', { name: 'Edit' }).click();
});
Page Object Model
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: 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' });
this.errorMessage = page.getByRole('alert');
}
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();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login', () => {
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
test('invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('wrong@example.com', 'wrong');
await loginPage.expectError('Invalid credentials');
});
});
API Testing
Playwright includes built-in API testing capabilities.
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
test('GET request', async ({ request }) => {
const response = await request.get('/api/users/1');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.email).toBe('user@example.com');
});
test('POST request', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
});
test('authenticated request', async ({ request }) => {
const response = await request.get('/api/profile', {
headers: {
'Authorization': 'Bearer token123'
}
});
expect(response.ok()).toBeTruthy();
});
});
Network Interception
test('mock API response', async ({ page }) => {
// Mock the API
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock User', email: 'mock@example.com' }
])
});
});
await page.goto('/users');
await expect(page.getByText('Mock User')).toBeVisible();
});
test('intercept and modify response', async ({ page }) => {
await page.route('/api/products', async (route) => {
const response = await route.fetch();
const json = await response.json();
// Modify response
json.products = json.products.map(p => ({
...p,
price: p.price * 0.9 // 10% discount
}));
await route.fulfill({ response, json });
});
await page.goto('/products');
});
test('block requests', async ({ page }) => {
// Block analytics
await page.route('**/*google-analytics*', route => route.abort());
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
await page.goto('/');
});
Debugging
Trace Viewer
# Enable traces
npx playwright test --trace on
# View traces
npx playwright show-trace trace.zip
Debug Mode
# Step through test
npx playwright test --debug
# Pause at specific point
await page.pause();
Screenshots and Video
// playwright.config.ts
export default defineConfig({
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
});
CI/CD Integration
# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
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 tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
AI-Assisted Playwright Development
AI tools integrate well with Playwright’s readable API.
What AI does well:
- Generating tests from user stories or requirements
- Converting Selenium/Cypress tests to Playwright
- Writing Page Object classes from HTML structure
- Creating assertions for complex data validation
- Explaining Playwright API methods and patterns
What still needs humans:
- Test strategy and coverage decisions
- Debugging visual or timing-related failures
- Choosing between locator strategies
- Performance optimization for large test suites
Useful prompt:
I have a checkout flow with these steps:
1. Add item to cart
2. Click checkout
3. Fill shipping address
4. Select payment method
5. Confirm order
6. Verify order confirmation page
Generate Playwright TypeScript tests with:
- Page Object Model
- Proper assertions at each step
- Error case for invalid payment
FAQ
Is Playwright better than Selenium?
Playwright offers several advantages: auto-wait eliminates most timing issues, execution is faster due to browser protocol communication, and the API is more modern. Selenium has broader browser support (including older versions) and a larger community with more learning resources. For new projects, Playwright is usually the better choice. For existing Selenium projects with large test suites, migration cost may not be worth it.
Is Playwright free to use?
Yes, completely. Playwright is open-source under Apache 2.0 license. Unlike Cypress, there are no paid tiers or enterprise features. Parallel execution, trace viewer, video recording — all free. The only cost is your own CI infrastructure.
Can Playwright test mobile apps?
Playwright tests mobile web browsers through device emulation — it simulates iPhone, Android, and tablet viewports. For native mobile apps (iOS/Android apps from app stores), you need Appium or platform-specific tools like XCUITest or Espresso.
What languages does Playwright support?
Playwright officially supports TypeScript, JavaScript, Python, Java, and C#. TypeScript/JavaScript have the most features (component testing, API testing) and best documentation. Python is excellent for teams already using pytest. Java and C# are good for enterprise environments.
When to Choose Playwright
Choose Playwright when:
- Starting a new test automation project
- Team uses TypeScript/JavaScript
- Need fast parallel execution
- Want modern debugging tools (trace viewer)
- Testing across Chromium, Firefox, WebKit
Consider alternatives when:
- Need real Safari on macOS (Selenium + Safari)
- Large existing Selenium infrastructure
- Team prefers Python/Java-first tooling
- Need component testing in React/Vue (Cypress has better DX)
Official Resources
See Also
- Playwright Comprehensive Guide - Advanced patterns and best practices
- Cypress vs Playwright - Detailed comparison for choosing the right tool
- Selenium Tutorial - When Selenium is still the right choice
- TypeScript Testing - TypeScript patterns for test automation
