Playwright - End-to-End Testing Framework
Modern end-to-end testing framework for web applications with cross-browser support for Chromium, Firefox, and WebKit using a single unified API.
- Step 1
Overview
Playwright is a framework for Web Testing and Automation developed by Microsoft. It enables reliable end-to-end testing for modern web apps across Chromium, Firefox, and WebKit with a single API. Playwright supports multiple programming languages (TypeScript, JavaScript, Python, Java, .NET), provides auto-wait capabilities, powerful debugging tools, and parallel test execution. It's designed to handle modern web features including SPAs, mobile emulation, network interception, and accessibility testing.
- Step 2
Technology Stack
Playwright's architecture and core technologies.
Language: TypeScript License: Apache License 2.0 Stars: ~90K Owner: microsoft Repo: https://github.com/microsoft/playwright Website: https://playwright.dev Created: November 2019 Core Technologies: - TypeScript 6.0+ - Primary language - Node.js (>=18) - Runtime environment - Chromium-BiDi - Chrome DevTools Protocol integration - WebSocket (ws) - Browser communication - Vite - Build tool for Playwright UI - React 19 - Trace viewer and UI components - esbuild - Fast bundling - Babel - Code transformation - Debug - Logging utilities Key Features: - Cross-browser testing (Chromium, Firefox, WebKit) - Auto-wait and retry-ability - Network interception and mocking - Mobile device emulation - Parallel test execution - Built-in test runner (@playwright/test) - Trace viewer for debugging - Screenshots and videos - Accessibility testing - API testing capabilities - Step 3
Installation
Install Playwright and browser binaries using npm, yarn, or pnpm.
# Install with npm npm init playwright@latest # Or install manually npm install -D @playwright/test npx playwright install # Install specific browsers npx playwright install chromium npx playwright install firefox npx playwright install webkit # Install system dependencies (Linux) pnx playwright install-deps - Step 4
Project Setup
The init command creates a basic project structure with example tests and configuration.
# Initialize new Playwright project (interactive) npm init playwright@latest # Creates: # - playwright.config.ts (or .js) # - tests/ directory with example tests # - tests-examples/ directory # - .github/workflows/playwright.yml (CI config) - Step 5
Basic Configuration
Configure Playwright through playwright.config.ts for test execution, browsers, and reporting.
// 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', 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-chrome', use: { ...devices['Pixel 5'] } }, { name: 'mobile-safari', use: { ...devices['iPhone 13'] } } ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI } }) - Step 6
Writing Your First Test
Create test files in the tests directory with .spec.ts or .spec.js extension.
// tests/example.spec.ts import { test, expect } from '@playwright/test' test('homepage has title and login link', async ({ page }) => { await page.goto('https://playwright.dev/') // Expect a title to contain Playwright await expect(page).toHaveTitle(/Playwright/) // Create a locator const getStarted = page.getByRole('link', { name: 'Get started' }) // Expect link to be visible await expect(getStarted).toBeVisible() // Click the link await getStarted.click() // Expects page to have a heading await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible() }) - Step 7
Running Tests
Execute tests using the Playwright test runner.
# Run all tests npx playwright test # Run tests in headed mode (visible browser) npx playwright test --headed # Run specific test file npx playwright test tests/example.spec.ts # Run tests in specific browser npx playwright test --project=chromium # Run tests in debug mode npx playwright test --debug # Run tests with UI mode (recommended for development) npx playwright test --ui # Run specific test by title npx playwright test -g "homepage has title" # Show test report npx playwright show-report - Step 8
Locators (Finding Elements)
Playwright's locator strategy prioritizes user-facing attributes and auto-waits for elements.
import { test, expect } from '@playwright/test' test('locator examples', async ({ page }) => { await page.goto('https://example.com') // Recommended: Role-based (accessibility-first) await page.getByRole('button', { name: 'Submit' }).click() await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com') // By text content await page.getByText('Welcome').click() // By label (for form inputs) await page.getByLabel('Password').fill('secret') // By placeholder await page.getByPlaceholder('Search...').fill('query') // By test id (data-testid attribute) await page.getByTestId('submit-button').click() // By CSS or XPath (less recommended) await page.locator('css=.btn-primary').click() await page.locator('xpath=//button[text()="Submit"]').click() // Combining and filtering await page.getByRole('listitem').filter({ hasText: 'Active' }).click() await page.getByRole('heading').nth(2).click() } - Step 9
Assertions
Playwright provides auto-retrying assertions that wait until the condition is met.
import { test, expect } from '@playwright/test' test('assertion examples', async ({ page }) => { await page.goto('https://example.com') // Page assertions await expect(page).toHaveTitle(/Example/) await expect(page).toHaveURL(/example.com/) // Element visibility await expect(page.getByRole('button')).toBeVisible() await expect(page.getByText('Hidden')).toBeHidden() // Element state await expect(page.getByRole('button')).toBeEnabled() await expect(page.getByRole('button')).toBeDisabled() await expect(page.getByRole('checkbox')).toBeChecked() // Text content await expect(page.getByRole('heading')).toHaveText('Welcome') await expect(page.getByRole('heading')).toContainText('Wel') // Attributes await expect(page.getByRole('link')).toHaveAttribute('href', '/about') await expect(page.getByRole('img')).toHaveAttribute('alt', /logo/i) // Count await expect(page.getByRole('listitem')).toHaveCount(5) // Value (for inputs) await expect(page.getByLabel('Email')).toHaveValue('test@example.com') // Screenshots (visual regression) await expect(page).toHaveScreenshot('homepage.png') } - Step 10
Page Interactions
Common user interactions with automatic waiting and actionability checks.
import { test } from '@playwright/test' test('interaction examples', async ({ page }) => { await page.goto('https://example.com') // Click await page.getByRole('button', { name: 'Submit' }).click() // Double click await page.getByRole('button').dblclick() // Right click await page.getByRole('button').click({ button: 'right' }) // Fill input await page.getByLabel('Email').fill('user@example.com') // Type with delay (simulates human typing) await page.getByLabel('Search').type('playwright', { delay: 100 }) // Clear input await page.getByLabel('Email').clear() // Check/uncheck await page.getByLabel('Subscribe').check() await page.getByLabel('Subscribe').uncheck() // Select dropdown await page.getByLabel('Country').selectOption('us') await page.getByLabel('Country').selectOption({ label: 'United States' }) // Upload file await page.getByLabel('Upload').setInputFiles('path/to/file.pdf') // Hover await page.getByRole('button').hover() // Focus await page.getByLabel('Email').focus() // Press keys await page.keyboard.press('Enter') await page.keyboard.type('Hello World') } - Step 11
Navigation and Waiting
Navigate between pages and wait for specific conditions.
import { test } from '@playwright/test' test('navigation examples', async ({ page }) => { // Navigate await page.goto('https://example.com') await page.goto('https://example.com', { waitUntil: 'networkidle' }) // Wait for navigation await Promise.all([ page.waitForNavigation(), page.getByRole('link').click() ]) // Back/forward await page.goBack() await page.goForward() await page.reload() // Wait for selector await page.waitForSelector('.loaded') // Wait for load state await page.waitForLoadState('domcontentloaded') await page.waitForLoadState('networkidle') // Wait for URL await page.waitForURL('**/dashboard') // Wait for function await page.waitForFunction(() => document.title === 'Ready') // Wait for timeout (use sparingly) await page.waitForTimeout(1000) // Wait for element to be visible await page.waitForSelector('text=Welcome', { state: 'visible' }) } - Step 12
Test Fixtures and Hooks
Use fixtures for setup and teardown, and hooks for test lifecycle management.
import { test, expect } from '@playwright/test' // Before and after hooks test.beforeEach(async ({ page }) => { await page.goto('https://example.com') await page.getByRole('button', { name: 'Login' }).click() }) test.afterEach(async ({ page }) => { await page.getByRole('button', { name: 'Logout' }).click() }) test.beforeAll(async () => { console.log('Before all tests') }) test.afterAll(async () => { console.log('After all tests') }) // Test with fixtures test('user can view dashboard', async ({ page, context }) => { await expect(page).toHaveURL(/dashboard/) }) // Custom fixtures const test2 = test.extend<{ authenticatedPage: Page }>({ authenticatedPage: async ({ page }, use) => { await page.goto('https://example.com/login') await page.getByLabel('Username').fill('testuser') await page.getByLabel('Password').fill('password') await page.getByRole('button', { name: 'Login' }).click() await use(page) } }) - Step 13
Network Interception and Mocking
Intercept and mock network requests for testing.
import { test, expect } from '@playwright/test' test('network interception', async ({ page }) => { // Mock API response await page.route('**/api/users', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ id: 1, name: 'Test User' }]) }) }) // Abort images await page.route('**/*.{png,jpg,jpeg}', route => route.abort()) // Modify request await page.route('**/api/**', async route => { const headers = await route.request().allHeaders() await route.continue({ headers: { ...headers, 'X-Custom-Header': 'value' } }) }) // Wait for request const [request] = await Promise.all([ page.waitForRequest('**/api/data'), page.getByRole('button', { name: 'Load' }).click() ]) // Wait for response const [response] = await Promise.all([ page.waitForResponse('**/api/data'), page.getByRole('button', { name: 'Load' }).click() ]) expect(response.status()).toBe(200) const data = await response.json() expect(data).toHaveProperty('users') } - Step 14
Screenshots and Videos
Capture screenshots and videos for debugging and documentation.
import { test } from '@playwright/test' test('screenshot examples', async ({ page }) => { await page.goto('https://example.com') // Full page screenshot await page.screenshot({ path: 'screenshot.png', fullPage: true }) // Element screenshot await page.getByRole('heading').screenshot({ path: 'heading.png' }) // Screenshot to buffer const buffer = await page.screenshot() // Screenshot with options await page.screenshot({ path: 'screenshot.png', type: 'png', quality: 90, fullPage: true }) }) // Video configuration in playwright.config.ts // Videos are automatically recorded when configured: // use: { // video: 'on', // or 'retain-on-failure', 'on-first-retry' // videoSize: { width: 1280, height: 720 } // } - Step 15
Mobile Device Emulation
Test on emulated mobile devices with touch and geolocation support.
import { test, devices } from '@playwright/test' test('mobile emulation', async ({ page }) => { // Configure in playwright.config.ts: // { name: 'mobile', use: { ...devices['iPhone 13'] } } await page.goto('https://example.com') // Or use device descriptor directly const iPhone = devices['iPhone 13'] await page.setViewportSize(iPhone.viewport) // Touch actions await page.tap('button') // Set geolocation await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }) await page.context().grantPermissions(['geolocation']) // Landscape orientation await page.setViewportSize({ width: 844, height: 390 }) }) // Available devices: const deviceList = [ 'iPhone 13', 'iPhone 13 Pro', 'iPhone 13 Pro Max', 'Pixel 5', 'Galaxy S9+', 'iPad Pro', 'Desktop Chrome', 'Desktop Firefox', 'Desktop Safari' ] - Step 16
API Testing
Use Playwright's request context for API testing without a browser.
import { test, expect } from '@playwright/test' test('API testing', async ({ request }) => { // GET request const response = await request.get('https://api.example.com/users') expect(response.ok()).toBeTruthy() expect(response.status()).toBe(200) const users = await response.json() expect(users).toHaveLength(10) // POST request const newUser = await request.post('https://api.example.com/users', { data: { name: 'Test User', email: 'test@example.com' } }) expect(newUser.ok()).toBeTruthy() // With authentication const authenticatedRequest = await request.fetch('https://api.example.com/me', { headers: { 'Authorization': 'Bearer token123' } }) // Reuse authentication in browser tests const loginResponse = await request.post('https://example.com/api/login', { form: { username: 'user', password: 'pass' } }) const cookies = await loginResponse.headers()['set-cookie'] // Cookies are automatically shared with page context }) - Step 17
Parallel Testing
Configure parallel test execution for faster test runs.
// playwright.config.ts import { defineConfig } from '@playwright/test' export default defineConfig({ // Run tests in parallel fullyParallel: true, // Number of parallel workers workers: process.env.CI ? 2 : undefined, // undefined = CPU cores // Limit parallel tests per file maxFailures: process.env.CI ? 10 : undefined, // Retry failed tests retries: process.env.CI ? 2 : 0 }) // Run sequentially in a file import { test } from '@playwright/test' test.describe.configure({ mode: 'serial' }) test.describe('serial tests', () => { test('runs first', async ({ page }) => { }) test('runs second', async ({ page }) => { }) }) // Run tests in parallel (default) test.describe.configure({ mode: 'parallel' }) - Step 18
Debugging Tests
Multiple debugging options including UI mode, debug mode, and trace viewer.
# UI Mode (recommended for development) npx playwright test --ui # Debug mode (step through tests) npx playwright test --debug # Debug specific test npx playwright test tests/example.spec.ts:10 --debug # Headed mode (see browser) npx playwright test --headed # Slow motion (delay actions) npx playwright test --headed --slow-mo=1000 # Generate trace npx playwright test --trace on # View trace npx playwright show-trace trace.zip # Codegen (record actions) npx playwright codegen https://example.com # Inspector PWDEBUG=1 npx playwright test - Step 19
Trace Viewer
Powerful debugging tool that records test execution with DOM snapshots, network activity, and screenshots.
# Configure in playwright.config.ts: # use: { # trace: 'on-first-retry', // or 'on', 'retain-on-failure' # } # View trace from failed test npx playwright show-trace test-results/example-chromium/trace.zip # Trace includes: # - DOM snapshots at every action # - Screenshots # - Network activity # - Console logs # - Source code # - Test steps timeline - Step 20
CI/CD Integration
Configure Playwright for continuous integration environments.
# .github/workflows/playwright.yml 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@v4 - uses: actions/setup-node@v4 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@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 - Step 21
Authentication Storage
Save and reuse authentication state across tests for faster execution.
// auth.setup.ts import { test as setup } from '@playwright/test' const authFile = 'playwright/.auth/user.json' setup('authenticate', async ({ page }) => { await page.goto('https://example.com/login') await page.getByLabel('Username').fill('user@example.com') await page.getByLabel('Password').fill('password') await page.getByRole('button', { name: 'Sign in' }).click() await page.waitForURL('**/dashboard') // Save storage state await page.context().storageState({ path: authFile }) }) // playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: authFile }, dependencies: ['setup'] } ] }) - Step 22
Page Object Model
Organize tests using the Page Object Model pattern for maintainability.
// pages/LoginPage.ts import { Page, Locator } from '@playwright/test' export class LoginPage { readonly page: Page readonly usernameInput: Locator readonly passwordInput: Locator readonly submitButton: Locator constructor(page: Page) { this.page = page this.usernameInput = page.getByLabel('Username') this.passwordInput = page.getByLabel('Password') this.submitButton = page.getByRole('button', { name: 'Sign in' }) } async login(username: string, password: string) { await this.usernameInput.fill(username) await this.passwordInput.fill(password) await this.submitButton.click() } async goto() { await this.page.goto('https://example.com/login') } } // tests/login.spec.ts import { test, expect } from '@playwright/test' import { LoginPage } from '../pages/LoginPage' test('user can login', async ({ page }) => { const loginPage = new LoginPage(page) await loginPage.goto() await loginPage.login('user@example.com', 'password') await expect(page).toHaveURL(/dashboard/) }) - Step 23
Accessibility Testing
Test accessibility using Playwright's built-in accessibility features.
import { test, expect } from '@playwright/test' test('accessibility', async ({ page }) => { await page.goto('https://example.com') // Get accessibility tree const snapshot = await page.accessibility.snapshot() console.log(snapshot) // Test keyboard navigation await page.keyboard.press('Tab') const focused = await page.evaluate(() => document.activeElement?.tagName) expect(focused).toBe('BUTTON') // Check ARIA attributes await expect(page.getByRole('button')).toHaveAttribute('aria-label', 'Submit') // For comprehensive a11y testing, integrate @axe-core/playwright: // npm install -D @axe-core/playwright // import { injectAxe, checkA11y } from '@axe-core/playwright' // await injectAxe(page) // await checkA11y(page) } - Step 24
Test Reporters
Configure different reporters for test output and CI integration.
// playwright.config.ts import { defineConfig } from '@playwright/test' export default defineConfig({ reporter: [ ['html'], // HTML report (default) ['list'], // List reporter (console) ['json', { outputFile: 'results.json' }], ['junit', { outputFile: 'results.xml' }], ['github'], // GitHub Actions annotations ['dot'], // Minimal output ['line'] // One line per test ] }) // Custom reporter class MyReporter { onBegin(config, suite) { console.log(`Starting tests with ${suite.allTests().length} tests`) } onTestEnd(test, result) { console.log(`Finished ${test.title}: ${result.status}`) } onEnd(result) { console.log(`Finished the run: ${result.status}`) } } export default defineConfig({ reporter: './my-reporter.ts' }) - Step 25
Global Setup and Teardown
Run setup and teardown code once for all tests.
// global-setup.ts import { chromium, FullConfig } from '@playwright/test' async function globalSetup(config: FullConfig) { // Start a development server // Seed database // Setup test data console.log('Global setup') // Example: Create admin user const browser = await chromium.launch() const page = await browser.newPage() await page.goto('https://example.com/admin/setup') // ... setup logic await browser.close() } export default globalSetup // global-teardown.ts async function globalTeardown() { // Clean up test data // Stop servers console.log('Global teardown') } export default globalTeardown // playwright.config.ts export default defineConfig({ globalSetup: require.resolve('./global-setup'), globalTeardown: require.resolve('./global-teardown') }) - Step 26
Key Features Summary
Comprehensive overview of Playwright's capabilities.
1. Cross-browser: Chromium, Firefox, WebKit 2. Auto-wait: Smart waiting for elements 3. Trace Viewer: Time-travel debugging 4. Codegen: Record and generate tests 5. Parallel Execution: Fast test runs 6. Network Control: Intercept and mock 7. Mobile Emulation: Touch and geolocation 8. API Testing: Test APIs without browser 9. Screenshots & Videos: Visual documentation 10. Multiple Languages: TS, JS, Python, Java, .NET 11. UI Mode: Interactive test development 12. Accessibility: Built-in a11y testing 13. Retry & Timeout: Configurable resilience 14. Authentication: State storage and reuse 15. CI/CD Ready: GitHub Actions integration 16. Component Testing: Isolated component tests 17. Visual Comparisons: Screenshot assertions 18. Reporters: HTML, JSON, JUnit, custom - Step 27
Use Cases
Common scenarios where Playwright excels.
1. E2E Testing: Full user flow testing 2. Regression Testing: Prevent breaking changes 3. Cross-browser: Ensure compatibility 4. Mobile Web: Test responsive designs 5. Visual Testing: Screenshot comparisons 6. API Testing: Backend endpoint validation 7. Web Scraping: Automated data extraction 8. PDF Generation: Headless PDF creation 9. Accessibility: A11y compliance testing 10. Performance: Load time monitoring 11. Integration: Third-party service testing 12. Authentication: Login flow validation 13. Forms: Complex form testing 14. SPAs: Single-page app testing 15. PWAs: Progressive web app testing 16. Component: Isolated component tests - Step 28
Migration from Other Frameworks
Guidelines for migrating from Selenium, Puppeteer, or Cypress.
From Selenium: - Replace WebDriver with Page API - Auto-wait replaces explicit waits - Built-in test runner replaces Mocha/Jest - Locators use getBy* methods From Puppeteer: - Similar API, less manual waiting - Built-in test runner and assertions - Cross-browser support (not just Chromium) - Better TypeScript support From Cypress: - Similar auto-wait behavior - Native multi-tab/domain support - Parallel execution at file level - More flexible network control - Language choice (not just JS) Key differences: - No jQuery-style chaining - Page object model recommended - Context isolation per test - Browser-native events - Step 29
Best Practices
Recommendations for effective Playwright testing.
1. Use getByRole for accessibility-first locators 2. Prefer auto-wait over manual timeouts 3. Enable trace on first retry for debugging 4. Use Page Object Model for organization 5. Store authentication state to speed up tests 6. Run tests in parallel for faster CI 7. Use --ui mode for test development 8. Mock network for flaky external APIs 9. Use data-testid sparingly, prefer semantic selectors 10. Configure retries for CI environments 11. Screenshot/video on failure only 12. Use fixtures for common setup 13. Keep tests isolated and independent 14. Use strict locators (fail on multiple matches) 15. Test in multiple browsers 16. Use TypeScript for better IDE support - Step 30
Resources
Official documentation and community resources.
Documentation: https://playwright.dev GitHub: https://github.com/microsoft/playwright API Reference: https://playwright.dev/docs/api/class-playwright Discord: https://aka.ms/playwright/discord Stack Overflow: [playwright] tag YouTube: Playwright Channel Examples: https://github.com/microsoft/playwright/tree/main/examples Best Practices: https://playwright.dev/docs/best-practices Community Guides: https://playwright.dev/community/welcome Release Notes: https://github.com/microsoft/playwright/releases - Step 31
File Locations
Important files and directories in a Playwright project.
playwright.config.ts - Main configuration tests/ - Test files (*.spec.ts) tests-examples/ - Example tests playwright-report/ - HTML test reports test-results/ - Screenshots, videos, traces playwright/.auth/ - Stored authentication states .github/workflows/ - CI/CD configurations node_modules/.playwright/ - Browser binaries global-setup.ts - Global setup (optional) global-teardown.ts - Global teardown (optional) pages/ - Page objects (recommended)
Feature requests
Sign in to suggest features or vote on existing ones.
No feature requests yet.
Discussion
Sign in to join the discussion.
No comments yet.