← Back to Blog

Playwright Automation Testing: Complete Guide for 2025

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

  1. Use Playwright Inspector:
PWDEBUG=1 npx playwright test
  1. Slow Down Execution:
export default defineConfig({
  use: {
    slowMo: 1000, // Slow down by 1 second
  },
});
  1. Pause Execution:
await page.pause(); // Opens inspector
  1. 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.