Playwright has rapidly become one of the most popular end-to-end testing frameworks, and for good reason. Developed by Microsoft, it offers powerful features, excellent reliability, and cross-browser support that makes it a top choice for modern web application testing.
Why Choose Playwright?
Playwright stands out from other testing frameworks with several key advantages:
- Multi-Browser Support: Test across Chromium, Firefox, and WebKit with a single API
- Auto-Waiting: Built-in intelligent waiting eliminates flaky tests
- Network Control: Intercept and modify network requests
- Mobile Emulation: Test mobile views without real devices
- Parallel Execution: Run tests in parallel across multiple browsers
- Powerful Debugging: Time-travel debugging and trace viewer
- TypeScript Support: First-class TypeScript support out of the box
Installation
Getting started with Playwright is straightforward:
Using npm:
npm init playwright@latest
This command will:
- Install Playwright
- Create a configuration file
- Set up example tests
- Install browser binaries
Using yarn:
yarn create playwright
Manual Installation:
npm install -D @playwright/test
npx playwright install
Project Structure
A typical Playwright project structure looks like this:
playwright-tests/
├── tests/
│ ├── example.spec.ts
│ ├── login.spec.ts
│ └── checkout.spec.ts
├── playwright.config.ts
├── package.json
└── .gitignore
Writing Your First Test
Let's create a simple test to verify a login flow:
import { test, expect } from '@playwright/test';
test('should successfully log in', async ({ page }) => {
// Navigate to the login page
await page.goto('https://example.com/login');
// Fill in credentials
await page.fill('[data-testid=email]', 'user@example.com');
await page.fill('[data-testid=password]', 'password123');
// Click the login button
await page.click('[data-testid=login-button]');
// Verify successful login
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('text=Welcome back')).toBeVisible();
});
Key Features
1. Auto-Waiting
Playwright automatically waits for elements to be actionable:
// Playwright waits for the element to be visible, enabled, and stable
await page.click('button.submit');
// Waits for navigation to complete
await page.goto('https://example.com');
2. Selectors
Playwright supports multiple selector strategies:
// CSS selector
await page.click('button.submit');
// Text selector
await page.click('text=Submit');
// Role selector (recommended for accessibility)
await page.click('role=button[name="Submit"]');
// Data test ID (best practice)
await page.click('[data-testid=submit-button]');
// XPath (when necessary)
await page.click('xpath=//button[@class="submit"]');
3. Network Interception
Intercept and modify network requests:
test('should handle API responses', async ({ page }) => {
// Intercept API calls
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ users: [] })
});
});
await page.goto('https://example.com/users');
// Test will use mocked response
});
4. Screenshots and Videos
Capture screenshots and record videos automatically:
test('should capture screenshot on failure', async ({ page }) => {
await page.goto('https://example.com');
// Take a screenshot
await page.screenshot({ path: 'screenshot.png' });
// Take screenshot of specific element
await page.locator('.header').screenshot({ path: 'header.png' });
});
Configure automatic screenshots in playwright.config.ts:
export default defineConfig({
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Best Practices
1. Use Page Object Model (POM)
Organize your tests using the Page Object Model pattern:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[data-testid=email]');
this.passwordInput = page.locator('[data-testid=password]');
this.loginButton = page.locator('[data-testid=login-button]');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('should login successfully', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL(/.*dashboard/);
});
2. Use Data Test IDs
Prefer data-testid attributes over CSS selectors:
<!-- Good -->
<button data-testid="submit-button">Submit</button>
<!-- Avoid -->
<button class="btn btn-primary">Submit</button>
// Good
await page.click('[data-testid=submit-button]');
// Avoid
await page.click('.btn.btn-primary');
3. Organize Tests with Fixtures
Use fixtures for reusable setup:
// fixtures.ts
import { test as base } from '@playwright/test';
type MyFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
// Login before each test
await page.goto('https://example.com/login');
await page.fill('[data-testid=email]', 'user@example.com');
await page.fill('[data-testid=password]', 'password123');
await page.click('[data-testid=login-button]');
await page.waitForURL(/.*dashboard/);
await use(page);
},
});
// Use in tests
test('should access dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
// Already logged in!
});
4. Parallel Execution
Run tests in parallel for faster execution:
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : 4, // Use 2 workers in CI, 4 locally
fullyParallel: true,
});
5. Handle Flaky Tests
Use retries and better waiting strategies:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry twice in CI
timeout: 30000, // 30 seconds timeout
});
Advanced Features
1. Multi-Browser Testing
Test across different browsers:
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
2. Mobile Emulation
Test mobile views:
import { devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
});
3. API Testing
Test APIs directly:
import { test, expect } from '@playwright/test';
test('should fetch user data', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.name).toBe('John Doe');
});
4. File Upload/Download
Handle file operations:
test('should upload file', async ({ page }) => {
await page.goto('https://example.com/upload');
// Upload file
await page.setInputFiles('[data-testid=file-input]', 'path/to/file.pdf');
await page.click('[data-testid=upload-button]');
await expect(page.locator('text=Upload successful')).toBeVisible();
});
test('should download file', async ({ page }) => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('[data-testid=download-button]')
]);
await download.saveAs('downloaded-file.pdf');
});
5. Trace Viewer
Debug tests with time-travel debugging:
// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry', // Record trace on retry
},
});
View traces:
npx playwright show-trace trace.zip
CI/CD Integration
GitHub Actions Example:
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@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- 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@v3
if: always()
with:
name: playwright-report
path: playwright-report/
Jenkins Example:
pipeline {
agent any
stages {
stage('Install') {
steps {
sh 'npm ci'
sh 'npx playwright install --with-deps'
}
}
stage('Test') {
steps {
sh 'npx playwright test'
}
}
stage('Report') {
steps {
publishHTML([
reportDir: 'playwright-report',
reportFiles: 'index.html',
reportName: 'Playwright Test Report'
])
}
}
}
}
Common Patterns
1. Waiting for API Calls
test('should wait for API response', async ({ page }) => {
await page.goto('https://example.com');
// Wait for API call to complete
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.status() === 200
);
// Now verify the UI updated
await expect(page.locator('.data-loaded')).toBeVisible();
});
2. Handling Multiple Tabs
test('should handle multiple tabs', async ({ context }) => {
const page1 = await context.newPage();
await page1.goto('https://example.com');
const page2 = await context.newPage();
await page2.goto('https://example.com/about');
// Work with both pages
await page1.click('button');
await page2.click('link');
});
3. Authentication State
Reuse authentication across tests:
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// Perform login
await page.goto('https://example.com/login');
await page.fill('[data-testid=email]', 'user@example.com');
await page.fill('[data-testid=password]', 'password123');
await page.click('[data-testid=login-button]');
// Save authentication state
await page.context().storageState({ path: 'auth.json' });
await browser.close();
}
export default globalSetup;
// Use in tests
export default defineConfig({
use: {
storageState: 'auth.json',
},
});
Debugging Tips
- Use Playwright Inspector:
PWDEBUG=1 npx playwright test
- Slow Down Execution:
export default defineConfig({
use: {
slowMo: 1000, // Slow down by 1 second
},
});
- Pause Execution:
await page.pause(); // Opens inspector
- Console Logs:
page.on('console', msg => console.log(msg.text()));
Performance Testing
Measure page load times and performance:
test('should load page quickly', async ({ page }) => {
const startTime = Date.now();
await page.goto('https://example.com');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // Should load in under 3 seconds
});
Comparison with Other Tools
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| Browser Support | Chromium, Firefox, WebKit | Chrome, Edge, Firefox | All browsers |
| API Testing | ✅ Built-in | ⚠️ Limited | ❌ No |
| Network Control | ✅ Full control | ⚠️ Limited | ❌ No |
| Mobile Emulation | ✅ Built-in | ⚠️ Limited | ⚠️ Limited |
| Parallel Execution | ✅ Excellent | ⚠️ Limited | ✅ Good |
| Debugging | ✅ Excellent | ✅ Good | ⚠️ Basic |
Conclusion
Playwright is an excellent choice for modern web application testing. Its powerful features, excellent reliability, and developer-friendly API make it ideal for teams looking to build robust test automation.
Key takeaways:
- Auto-waiting eliminates flaky tests
- Multi-browser support out of the box
- Network interception for testing edge cases
- Excellent debugging tools
- Great CI/CD integration
Ready to implement Playwright in your testing strategy? Contact AstericLabs to learn how we can help you set up a comprehensive Playwright testing framework for your application.