📚 5 min read
Playwright Guide
Playwright is a powerful testing framework by Microsoft that enables reliable end-to-end testing for modern web apps. It supports multiple browser engines including Chromium, Firefox, and WebKit.
Key Features
- Cross-browser support
- Auto-wait capabilities
- Network interception
- Mobile device emulation
- Test parallelization
- Visual comparisons
- API testing support
- Codegen tool
Getting Started
bash
# Install Playwright
npm init playwright@latest
Basic Test Structure
typescript
import { test, expect } from '@playwright/test';
test.describe('authentication flows', () => {
test('successful login', 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')).toContainText('Welcome back');
});
});
Common Actions
typescript
// Navigation
await page.goto('https://example.com');
await page.goBack();
await page.reload();
// Interactions
await page.click('button');
await page.fill('input', 'text');
await page.selectOption('select', 'option1');
// Assertions
await expect(page).toHaveTitle(/My Website/);
await expect(page.locator('.count')).toHaveText('5');
await expect(page.locator('button')).toBeEnabled();
// Network
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ users: [] }),
});
});
// Screenshots
await page.screenshot({ path: 'screenshot.png' });
Test Configuration
typescript
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
timeout: 30000,
retries: 2,
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
{
name: 'firefox',
use: { browserName: 'firefox' },
},
{
name: 'webkit',
use: { browserName: 'webkit' },
},
],
};
export default config;
Advanced Features
API Testing
typescript
test('API endpoints', async ({ request }) => {
// GET request
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
expect(await response.json()).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'John' })])
);
// POST request with body
const createResponse = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
},
});
expect(createResponse.status()).toBe(201);
});
Network Interception
typescript
test('mock API calls', async ({ page }) => {
// Mock response
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
// Mock error response
await page.route('/api/error', (route) =>
route.fulfill({
status: 500,
body: 'Server Error',
})
);
// Modify request
await page.route('/api/data', async (route) => {
const request = route.request();
const postData = request.postData();
await route.continue({
postData: postData?.replace('old', 'new'),
});
});
});
Visual Comparison
typescript
test('visual regression', async ({ page }) => {
await page.goto('/dashboard');
// Full page screenshot
expect(await page.screenshot()).toMatchSnapshot('dashboard.png');
// Element screenshot
const logo = page.locator('.logo');
expect(await logo.screenshot()).toMatchSnapshot('logo.png');
// With options
expect(
await page.screenshot({
fullPage: true,
mask: [page.locator('.dynamic-content')],
})
).toMatchSnapshot('masked-page.png');
});
Testing Patterns
Page Objects
typescript
// models/LoginPage.ts
class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('input[name="email"]', email);
await this.page.fill('input[name="password"]', password);
await this.page.click('button[type="submit"]');
}
async getErrorMessage() {
return this.page.textContent('.error-message');
}
}
// tests/login.spec.ts
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
Component Testing
typescript
test('button component', async ({ mount }) => {
const component = await mount(
<Button onClick={() => console.log('clicked')}>
Click me
</Button>
)
await expect(component).toContainText('Click me')
await component.click()
await expect(component).toHaveClass(/active/)
})
Mobile Testing
Device Emulation
typescript
test('mobile viewport', async ({ browser }) => {
const pixel5 = playwright.devices['Pixel 5'];
const context = await browser.newContext({
...pixel5,
locale: 'en-US',
geolocation: { longitude: 12.492507, latitude: 41.889938 },
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('/mobile');
await expect(page.locator('.mobile-menu')).toBeVisible();
});
Touch Interactions
typescript
test('touch gestures', async ({ page }) => {
// Tap
await page.tap('.button');
// Double tap
await page.dblclick('.zoom-area');
// Swipe
await page.locator('.slider').dragTo(page.locator('.target'), {
sourcePosition: { x: 0, y: 0 },
targetPosition: { x: 100, y: 0 },
});
});
Performance Testing
Metrics Collection
typescript
test('performance metrics', async ({ page }) => {
// Enable performance monitoring
await page.coverage.startJSCoverage();
const startTime = Date.now();
await page.goto('/');
// Get metrics
const metrics = await page.metrics();
const timing = await page.evaluate(() =>
JSON.stringify(window.performance.timing)
);
const jsCoverage = await page.coverage.stopJSCoverage();
// Assertions
expect(Date.now() - startTime).toBeLessThan(3000);
expect(metrics.TaskDuration).toBeLessThan(100);
expect(jsCoverage[0].unusedBytes).toBeLessThan(1024);
});
Resource Monitoring
typescript
test('resource loading', async ({ page }) => {
const [request] = await Promise.all([
page.waitForRequest('**/*.js'),
page.goto('/'),
]);
const responses = await Promise.all([
page.waitForResponse('**/*.css'),
page.click('.load-more'),
]);
expect(request.resourceType()).toBe('script');
expect(responses[0].status()).toBe(200);
});
Debugging
Trace Viewer
typescript
test('record trace', async ({ page }) => {
// Start tracing
await context.tracing.start({
screenshots: true,
snapshots: true,
});
await page.goto('/');
await page.click('.button');
// Stop and save trace
await context.tracing.stop({
path: 'trace.zip',
});
});
Debug Mode
typescript
test('debug test', async ({ page }) => {
// Launch debugger
await page.pause();
// Console output
page.on('console', (msg) => console.log(msg.text()));
// Screenshot on failure
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({ path: `failure-${testInfo.title}.png` });
}
});
});
Best Practices
1. Selectors
typescript
// ❌ Avoid
page.click('.submit-button');
page.fill('#email', 'user@example.com');
// ✅ Prefer
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByTestId('submit-form');
2. Waiting
typescript
// ❌ Avoid
await page.waitForTimeout(1000);
// ✅ Prefer
await expect(page.getByText('Loading')).toBeHidden();
await expect(page.getByRole('alert')).toBeVisible();
await page.waitForResponse('**/api/data');
3. Assertions
typescript
// State assertions
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('textbox')).toHaveValue('text');
await expect(page.getByRole('heading')).toContainText('Welcome');
// Multiple assertions
await expect(async () => {
const count = await page.getByTestId('item').count();
expect(count).toBeGreaterThan(0);
expect(count).toBeLessThan(10);
}).toPass();
4. Error Handling
typescript
test.beforeEach(async ({ page }) => {
page.on('pageerror', (exception) => {
console.error(`Page error: ${exception.message}`);
});
page.on('requestfailed', (request) => {
console.error(`Failed request: ${request.url()}`);
});
});