Ultimate Playwright Test Automation Guide
A comprehensive guide to Playwright — installation, setup, writing robust UI tests, async operations, and CI/CD pipeline integration.
Playwright has rapidly become the go-to framework for modern UI test automation. Built by Microsoft and released as open source, it supports Chromium, Firefox, and WebKit — all with a single API. If you're evaluating automation tools or looking to level up your existing Playwright skills, this guide covers everything from first install to production-grade CI/CD integration.
Why Playwright?
Before diving into code, it's worth understanding what makes Playwright stand out from alternatives like Selenium and Cypress.
Auto-waiting is Playwright's biggest quality-of-life feature. Instead of manually adding waitForElement() calls or arbitrary sleep() delays, Playwright automatically waits for elements to be actionable before interacting with them. This alone eliminates an entire class of flaky tests.
True cross-browser support means you can run the same test suite against Chromium, Firefox, and WebKit (Safari's engine) without any changes. Cypress only supports Chromium-based browsers natively. Selenium supports all browsers but requires per-browser driver management.
Network interception lets you mock API responses, block third-party scripts, and simulate network conditions directly in your tests — no proxy setup required.
Parallel execution is built in. Playwright runs tests in separate browser contexts (not separate processes), making parallel execution fast and isolated.
Installation and Setup
Start with Node.js 18+ and run:
BASH1npm init playwright@latest
The interactive setup will ask you to choose TypeScript or JavaScript, where to put your tests, and whether to add a GitHub Actions workflow. Choose TypeScript — the type safety pays dividends as your suite grows.
This creates:
playwright.config.ts ← Global configuration
tests/ ← Your test files
tests-examples/ ← Sample tests to reference
Your playwright.config.ts will look like this by default:
TYPESCRIPT1import { defineConfig, devices } from '@playwright/test'; 2 3export default defineConfig({ 4 testDir: './tests', 5 fullyParallel: true, 6 forbidOnly: !!process.env.CI, 7 retries: process.env.CI ? 2 : 0, 8 workers: process.env.CI ? 1 : undefined, 9 reporter: 'html', 10 use: { 11 baseURL: 'http://localhost:3000', 12 trace: 'on-first-retry', 13 }, 14 projects: [ 15 { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, 16 { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, 17 { name: 'webkit', use: { ...devices['Desktop Safari'] } }, 18 ], 19});
Writing Your First Test
Playwright uses a test and expect API that feels familiar if you've used Jest or Vitest.
TYPESCRIPT1import { test, expect } from '@playwright/test'; 2 3test('homepage has correct title', async ({ page }) => { 4 await page.goto('/'); 5 await expect(page).toHaveTitle(/InnovateBits/); 6}); 7 8test('navigation links work', async ({ page }) => { 9 await page.goto('/'); 10 await page.getByRole('link', { name: 'Blog' }).click(); 11 await expect(page).toHaveURL('/blog'); 12});
Locator Strategies
The recommended way to find elements in Playwright is through semantic locators — locators that reflect how users perceive the page rather than implementation details.
TYPESCRIPT1// ✅ Preferred — resilient to CSS/HTML changes 2page.getByRole('button', { name: 'Submit' }) 3page.getByLabel('Email address') 4page.getByPlaceholder('Enter your email') 5page.getByText('Welcome back') 6page.getByTestId('submit-btn') // data-testid attribute 7 8// ⚠️ Acceptable — but fragile if structure changes 9page.locator('.submit-button') 10page.locator('#email-input') 11 12// ❌ Avoid — XPath is brittle and hard to read 13page.locator('//div[@class="container"]/button[1]')
Page Object Model
For any test suite beyond a few files, the Page Object Model (POM) is essential. It encapsulates the selectors and actions for each page into a class, so when the UI changes you update one place instead of every test.
TYPESCRIPT1// pages/LoginPage.ts 2import { Page, Locator } from '@playwright/test'; 3 4export class LoginPage { 5 readonly page: Page; 6 readonly emailInput: Locator; 7 readonly passwordInput: Locator; 8 readonly submitButton: Locator; 9 readonly errorMessage: Locator; 10 11 constructor(page: Page) { 12 this.page = page; 13 this.emailInput = page.getByLabel('Email'); 14 this.passwordInput = page.getByLabel('Password'); 15 this.submitButton = page.getByRole('button', { name: 'Log in' }); 16 this.errorMessage = page.getByRole('alert'); 17 } 18 19 async goto() { 20 await this.page.goto('/login'); 21 } 22 23 async login(email: string, password: string) { 24 await this.emailInput.fill(email); 25 await this.passwordInput.fill(password); 26 await this.submitButton.click(); 27 } 28}
Using it in a test:
TYPESCRIPT1import { test, expect } from '@playwright/test'; 2import { LoginPage } from '../pages/LoginPage'; 3 4test('valid login redirects to dashboard', async ({ page }) => { 5 const loginPage = new LoginPage(page); 6 await loginPage.goto(); 7 await loginPage.login('user@example.com', 'password123'); 8 await expect(page).toHaveURL('/dashboard'); 9}); 10 11test('invalid password shows error', async ({ page }) => { 12 const loginPage = new LoginPage(page); 13 await loginPage.goto(); 14 await loginPage.login('user@example.com', 'wrongpassword'); 15 await expect(loginPage.errorMessage).toBeVisible(); 16});
Handling Async Operations
Most UI interactions are asynchronous — API calls, animations, redirects. Playwright handles the most common patterns automatically, but here are the ones to know explicitly.
Waiting for network requests
TYPESCRIPT1// Wait for a specific API call to complete 2const responsePromise = page.waitForResponse('**/api/users'); 3await page.getByRole('button', { name: 'Load Users' }).click(); 4const response = await responsePromise; 5expect(response.status()).toBe(200);
Intercepting and mocking APIs
TYPESCRIPT1test('shows empty state when no results', async ({ page }) => { 2 // Intercept the API and return mock data 3 await page.route('**/api/search*', async route => { 4 await route.fulfill({ 5 status: 200, 6 contentType: 'application/json', 7 body: JSON.stringify({ results: [], total: 0 }), 8 }); 9 }); 10 11 await page.goto('/search?q=something'); 12 await expect(page.getByText('No results found')).toBeVisible(); 13});
File uploads and downloads
TYPESCRIPT1// File upload 2await page.getByLabel('Upload file').setInputFiles('path/to/file.pdf'); 3 4// File download 5const downloadPromise = page.waitForEvent('download'); 6await page.getByRole('button', { name: 'Export CSV' }).click(); 7const download = await downloadPromise; 8await download.saveAs('./test-results/' + download.suggestedFilename());
Visual Testing
Playwright has built-in screenshot comparison. Add --update-snapshots the first time to create the baseline:
TYPESCRIPT1test('homepage matches snapshot', async ({ page }) => { 2 await page.goto('/'); 3 await expect(page).toHaveScreenshot('homepage.png', { 4 maxDiffPixels: 100, // Allow minor rendering differences 5 }); 6});
Running Tests
BASH1# Run all tests 2npx playwright test 3 4# Run a specific file 5npx playwright test tests/login.spec.ts 6 7# Run with UI mode (great for debugging) 8npx playwright test --ui 9 10# Run headed (see the browser) 11npx playwright test --headed 12 13# Run specific browser only 14npx playwright test --project=chromium 15 16# Debug a specific test 17npx playwright test --debug tests/login.spec.ts
CI/CD Integration
GitHub Actions
The npm init playwright@latest setup generates this workflow automatically. Here's a production-ready version:
YAML1name: Playwright Tests 2on: 3 push: 4 branches: [main, develop] 5 pull_request: 6 branches: [main] 7 8jobs: 9 test: 10 runs-on: ubuntu-latest 11 steps: 12 - uses: actions/checkout@v4 13 - uses: actions/setup-node@v4 14 with: 15 node-version: 20 16 cache: 'npm' 17 - name: Install dependencies 18 run: npm ci 19 - name: Install Playwright browsers 20 run: npx playwright install --with-deps 21 - name: Run Playwright tests 22 run: npx playwright test 23 env: 24 BASE_URL: ${{ secrets.STAGING_URL }} 25 - name: Upload test report 26 uses: actions/upload-artifact@v4 27 if: always() 28 with: 29 name: playwright-report 30 path: playwright-report/ 31 retention-days: 30
Jenkins
GROOVY1pipeline { 2 agent any 3 stages { 4 stage('Install') { 5 steps { 6 sh 'npm ci' 7 sh 'npx playwright install --with-deps' 8 } 9 } 10 stage('Test') { 11 steps { 12 sh 'npx playwright test --reporter=junit' 13 } 14 post { 15 always { 16 junit 'test-results/*.xml' 17 publishHTML([ 18 reportDir: 'playwright-report', 19 reportFiles: 'index.html', 20 reportName: 'Playwright Report' 21 ]) 22 } 23 } 24 } 25 } 26}
Best Practices
Keep tests independent. Each test should set up its own state and not depend on another test having run first. Use beforeEach hooks or API calls to seed data.
Use test.describe for grouping. Organise related tests into describe blocks. This also lets you apply shared beforeEach and afterEach hooks to a group.
Avoid page.waitForTimeout(). Fixed delays make tests slow and still flaky. Use proper waitFor assertions instead:
TYPESCRIPT1// ❌ Fragile — fails on slow machines, wastes time on fast ones 2await page.waitForTimeout(3000); 3 4// ✅ Reliable — waits for the actual condition 5await expect(page.getByText('Loading...')).toBeHidden();
Store credentials in environment variables. Never hardcode passwords in tests. Use .env files locally and CI secrets in pipelines.
Set a global baseURL. Configure baseURL in playwright.config.ts and use relative URLs in page.goto(). This makes it trivial to point your suite at staging, UAT, or production.
What's Next
With Playwright running in CI and a solid Page Object Model in place, the natural next steps are:
- API testing alongside UI tests using Playwright's built-in
requestcontext - AI-assisted test generation to accelerate writing new test cases — see our guide on AI-Powered Test Generation with Playwright
- Performance testing hooks to capture metrics like LCP and FCP during test runs
Playwright's official documentation is exceptionally good — bookmark the locator API and assertions reference pages in particular.
Share this article
Follow for more
Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!