beginner30 minutes9 min readFEATURED

Getting Started with Playwright for End-to-End Testing

Learn how to set up and use Playwright for reliable end-to-end testing of web applications

Prerequisites:

  • Basic JavaScript knowledge
  • Node.js installed
9 min read
30 minutes

End-to-end testing is crucial for ensuring your web applications work correctly from the user's perspective. Playwright, developed by Microsoft, has quickly become one of the most popular tools for browser automation and testing. This tutorial will get you up and running with Playwright in no time.

What is Playwright?

Playwright is a Node.js library that provides a high-level API to control Chromium, Firefox, and WebKit browsers. It's designed for end-to-end testing, but it's also great for web scraping, automation, and performance testing.

Key Features

  • Cross-browser support: Test on Chromium, Firefox, and WebKit
  • Headless and headed modes: Run tests with or without a visible browser
  • Mobile testing: Emulate mobile devices and touch interactions
  • Network interception: Mock API responses and test offline scenarios
  • Parallel execution: Run tests in parallel for faster feedback
  • Auto-wait: Automatically waits for elements to be ready
  • Screenshots and videos: Capture visual evidence of test runs

Prerequisites

Before we start, make sure you have:

  • Node.js 16 or higher installed
  • Basic understanding of JavaScript/TypeScript
  • A text editor (VS Code recommended)
  • A web application to test (we'll use a demo app)

Installation and Setup

Step 1: Create a New Project

mkdir playwright-tutorial
cd playwright-tutorial
npm init -y

Step 2: Install Playwright

# Install Playwright with browsers
npm init playwright@latest

# Or install manually
npm install --save-dev @playwright/test
npx playwright install

The npm init playwright@latest command will:

  • Install Playwright and its dependencies
  • Download browser binaries
  • Create example test files
  • Set up configuration files

Step 3: Project Structure

After installation, your project should look like this:

playwright-tutorial/
├── tests/
│   └── example.spec.ts
├── playwright.config.ts
├── package.json
└── node_modules/

Step 4: Configuration

The playwright.config.ts file contains your test configuration:

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',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run start',
    port: 3000,
  },
});

Writing Your First Test

Let's create a simple test that navigates to a website and verifies its content.

Step 1: Create a Basic Test

Create tests/first-test.spec.ts:

import { test, expect } from '@playwright/test';

test('homepage has correct title', async ({ page }) => {
  // Navigate to the page
  await page.goto('https://playwright.dev/');
  
  // Verify the title
  await expect(page).toHaveTitle(/Playwright/);
  
  // Check for a specific element
  await expect(page.locator('h1')).toContainText('Playwright');
});

Step 2: Run Your Test

# Run all tests
npx playwright test

# Run a specific test file
npx playwright test first-test.spec.ts

# Run tests in headed mode (visible browser)
npx playwright test --headed

# Run tests in a specific browser
npx playwright test --project=firefox

Core Playwright Concepts

Page Object

The page object represents a web page in the browser:

test('page interactions', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Get page title
  const title = await page.title();
  
  // Take a screenshot
  await page.screenshot({ path: 'example.png' });
  
  // Get page URL
  const url = page.url();
});

Locators

Locators are Playwright's way of finding elements on the page:

test('working with locators', async ({ page }) => {
  await page.goto('https://example.com');
  
  // By text content
  const heading = page.locator('text=Welcome');
  
  // By CSS selector
  const button = page.locator('button.submit');
  
  // By data attribute
  const form = page.locator('[data-testid="login-form"]');
  
  // By role
  const link = page.getByRole('link', { name: 'About' });
  
  // By placeholder
  const input = page.getByPlaceholder('Enter your email');
});

Actions

Playwright provides methods to interact with elements:

test('user interactions', async ({ page }) => {
  await page.goto('https://example.com/login');
  
  // Fill input fields
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('password123');
  
  // Click buttons
  await page.getByRole('button', { name: 'Login' }).click();
  
  // Select from dropdown
  await page.selectOption('select#country', 'us');
  
  // Check/uncheck checkboxes
  await page.getByLabel('Remember me').check();
  
  // Upload files
  await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
});

Assertions

Use assertions to verify expected behavior:

test('assertions example', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Page assertions
  await expect(page).toHaveTitle('Example Domain');
  await expect(page).toHaveURL(/example.com/);
  
  // Element assertions
  const heading = page.locator('h1');
  await expect(heading).toBeVisible();
  await expect(heading).toContainText('Example');
  await expect(heading).toHaveClass('main-heading');
  
  // Form assertions
  const input = page.getByLabel('Email');
  await expect(input).toBeEmpty();
  await expect(input).toBeEnabled();
  
  // Count assertions
  const items = page.locator('.item');
  await expect(items).toHaveCount(5);
});

Practical Example: Testing a Todo App

Let's create a comprehensive test for a todo application:

import { test, expect } from '@playwright/test';

test.describe('Todo App', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://todomvc.com/examples/react/');
  });

  test('should add a new todo', async ({ page }) => {
    // Add a new todo
    const newTodo = page.getByPlaceholder('What needs to be done?');
    await newTodo.fill('Buy groceries');
    await newTodo.press('Enter');
    
    // Verify the todo appears in the list
    await expect(page.locator('.todo-list li')).toHaveCount(1);
    await expect(page.locator('.todo-list li')).toContainText('Buy groceries');
  });

  test('should mark todo as completed', async ({ page }) => {
    // Add a todo
    await page.getByPlaceholder('What needs to be done?').fill('Complete tutorial');
    await page.getByPlaceholder('What needs to be done?').press('Enter');
    
    // Mark as completed
    await page.locator('.todo-list li .toggle').click();
    
    // Verify it's marked as completed
    await expect(page.locator('.todo-list li')).toHaveClass(/completed/);
  });

  test('should filter todos', async ({ page }) => {
    // Add multiple todos
    const newTodo = page.getByPlaceholder('What needs to be done?');
    
    await newTodo.fill('Active todo');
    await newTodo.press('Enter');
    
    await newTodo.fill('Completed todo');
    await newTodo.press('Enter');
    
    // Complete one todo
    await page.locator('.todo-list li').first().locator('.toggle').click();
    
    // Filter by active
    await page.getByRole('link', { name: 'Active' }).click();
    await expect(page.locator('.todo-list li')).toHaveCount(1);
    await expect(page.locator('.todo-list li')).toContainText('Completed todo');
    
    // Filter by completed
    await page.getByRole('link', { name: 'Completed' }).click();
    await expect(page.locator('.todo-list li')).toHaveCount(1);
    await expect(page.locator('.todo-list li')).toContainText('Active todo');
  });
});

Advanced Features

Network Interception

Mock API responses for more reliable tests:

test('should handle API errors', async ({ page }) => {
  // Mock a failed API response
  await page.route('**/api/todos', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Server error' })
    });
  });
  
  await page.goto('/todos');
  
  // Verify error handling
  await expect(page.locator('.error-message')).toBeVisible();
});

Mobile Testing

Test your app on mobile devices:

import { test, expect, devices } from '@playwright/test';

test.use({
  ...devices['iPhone 13'],
});

test('mobile navigation', async ({ page }) => {
  await page.goto('/');
  
  // Open mobile menu
  await page.getByRole('button', { name: 'Menu' }).click();
  
  // Verify mobile-specific behavior
  await expect(page.locator('.mobile-menu')).toBeVisible();
});

Visual Testing

Compare screenshots to detect visual regressions:

test('visual regression test', async ({ page }) => {
  await page.goto('/dashboard');
  
  // Take and compare screenshot
  await expect(page).toHaveScreenshot('dashboard.png');
  
  // Screenshot a specific element
  await expect(page.locator('.chart')).toHaveScreenshot('chart.png');
});

Best Practices

1. Use Page Object Model

Organize your tests using the Page Object pattern:

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}
  
  async navigate() {
    await this.page.goto('/login');
  }
  
  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Login' }).click();
  }
  
  async getErrorMessage() {
    return this.page.locator('.error-message').textContent();
  }
}

// test file
test('login with invalid credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  
  await loginPage.navigate();
  await loginPage.login('invalid@email.com', 'wrongpassword');
  
  const error = await loginPage.getErrorMessage();
  expect(error).toContain('Invalid credentials');
});

2. Use Data-Testid Attributes

Add test-specific attributes to make tests more reliable:

<!-- In your HTML -->
<button data-testid="submit-button">Submit</button>
// In your test
await page.getByTestId('submit-button').click();

3. Wait Strategies

Use appropriate waiting strategies:

// Wait for element to be visible
await page.waitForSelector('.loading-spinner', { state: 'hidden' });

// Wait for network request
await page.waitForResponse('**/api/data');

// Wait for function to return true
await page.waitForFunction(() => window.dataLoaded === true);

4. Test Data Management

Use fixtures for test data:

// tests/fixtures.ts
export const testUsers = {
  admin: { email: 'admin@test.com', password: 'admin123' },
  user: { email: 'user@test.com', password: 'user123' }
};

// In your test
import { testUsers } from './fixtures';

test('admin can access admin panel', async ({ page }) => {
  await loginPage.login(testUsers.admin.email, testUsers.admin.password);
  await expect(page.locator('.admin-panel')).toBeVisible();
});

Debugging Tests

Running Tests in Debug Mode

# Debug a specific test
npx playwright test --debug first-test.spec.ts

# Use Playwright Inspector
npx playwright test --ui

Adding Debug Information

test('debug example', async ({ page }) => {
  await page.goto('/');
  
  // Pause test execution
  await page.pause();
  
  // Add console logs
  console.log('Current URL:', page.url());
  
  // Take screenshot for debugging
  await page.screenshot({ path: 'debug.png' });
});

Running Tests in CI/CD

GitHub Actions Example

# .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@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/
        retention-days: 30

Next Steps

Now that you've learned the basics of Playwright, here are some areas to explore further:

  1. Advanced Selectors: Learn about CSS selectors, XPath, and text selectors
  2. API Testing: Use Playwright's API testing capabilities
  3. Performance Testing: Measure and test web performance
  4. Accessibility Testing: Integrate accessibility testing into your suite
  5. Custom Fixtures: Create reusable test fixtures and helpers
  6. Parallel Testing: Scale your tests with parallel execution
  7. Integration: Integrate with CI/CD pipelines and reporting tools

Troubleshooting Common Issues

Tests are Flaky

  • Use page.waitForLoadState() to ensure page is fully loaded
  • Add explicit waits for dynamic content
  • Use page.waitForSelector() instead of page.locator().click()

Elements Not Found

  • Check if element is in an iframe: page.frameLocator('iframe').locator('button')
  • Verify element timing: use { timeout: 10000 } for slow elements
  • Use more specific selectors or data-testid attributes

Slow Tests

  • Run tests in parallel: npx playwright test --workers=4
  • Use page.route() to mock slow API calls
  • Optimize selectors and reduce unnecessary waits

Conclusion

Playwright is a powerful tool for end-to-end testing that makes browser automation reliable and efficient. With its cross-browser support, auto-waiting capabilities, and rich API, it's an excellent choice for testing modern web applications.

The key to successful E2E testing with Playwright is to:

  • Start with simple tests and gradually add complexity
  • Use reliable selectors and waiting strategies
  • Organize tests with Page Object Model
  • Mock external dependencies when possible
  • Run tests in CI/CD for continuous feedback

With these fundamentals, you're ready to build a robust test suite that will catch bugs before they reach your users.


Ready to implement Playwright in your project? Get in touch to discuss testing strategies specific to your application and team needs.