JavaScript 7 min read

Playwright: Modern End-to-End Testing Guide

Master Playwright for browser automation and testing. Learn selectors, assertions, API testing, visual testing, and build reliable E2E test suites.

MR

Moshiour Rahman

Advertisement

What is Playwright?

Playwright is a modern end-to-end testing framework by Microsoft. It supports all major browsers, provides powerful automation APIs, and enables reliable, fast testing.

Playwright Features

FeatureDescription
Cross-browserChrome, Firefox, Safari
Auto-waitReliable element detection
NetworkIntercept and mock
MobileDevice emulation
VisualScreenshot comparison

Getting Started

Installation

# Install Playwright
npm init playwright@latest

# Or add to existing project
npm install -D @playwright/test
npx playwright install

Configuration

// 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 12'] },
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Writing Tests

Basic Test Structure

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

test.describe('Homepage', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('should display welcome message', async ({ page }) => {
    await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
  });

  test('should navigate to about page', async ({ page }) => {
    await page.click('text=About');
    await expect(page).toHaveURL('/about');
  });
});

Selectors

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

test('selector examples', async ({ page }) => {
  await page.goto('/');

  // Role-based selectors (recommended)
  await page.getByRole('button', { name: 'Submit' }).click();
  await page.getByRole('link', { name: 'Home' }).click();
  await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
  await page.getByRole('checkbox', { name: 'Accept terms' }).check();

  // Text selectors
  await page.getByText('Welcome').click();
  await page.getByText(/hello/i).click();  // Regex

  // Label and placeholder
  await page.getByLabel('Username').fill('john');
  await page.getByPlaceholder('Enter email').fill('john@example.com');

  // Test ID
  await page.getByTestId('submit-button').click();

  // CSS selectors
  await page.locator('.my-class').click();
  await page.locator('#my-id').click();
  await page.locator('[data-test="value"]').click();

  // XPath
  await page.locator('xpath=//button').click();

  // Chaining
  await page.locator('.card').filter({ hasText: 'Product' }).click();
  await page.locator('.list').locator('.item').first().click();
});

Assertions

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

test('assertion examples', async ({ page }) => {
  await page.goto('/');

  // Visibility
  await expect(page.getByText('Hello')).toBeVisible();
  await expect(page.getByText('Hidden')).toBeHidden();

  // Text content
  await expect(page.locator('h1')).toHaveText('Welcome');
  await expect(page.locator('h1')).toContainText('Welc');

  // Attributes
  await expect(page.locator('input')).toHaveAttribute('type', 'email');
  await expect(page.locator('input')).toHaveValue('test@example.com');

  // Count
  await expect(page.locator('.item')).toHaveCount(5);

  // URL and title
  await expect(page).toHaveURL('/dashboard');
  await expect(page).toHaveTitle('Dashboard');

  // CSS
  await expect(page.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');

  // State
  await expect(page.getByRole('button')).toBeEnabled();
  await expect(page.getByRole('button')).toBeDisabled();
  await expect(page.getByRole('checkbox')).toBeChecked();

  // Negation
  await expect(page.getByText('Error')).not.toBeVisible();
});

Form Interactions

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

test('form interactions', async ({ page }) => {
  await page.goto('/form');

  // Text input
  await page.getByLabel('Name').fill('John Doe');
  await page.getByLabel('Name').clear();
  await page.getByLabel('Name').type('Jane Doe', { delay: 100 });

  // Select dropdown
  await page.getByLabel('Country').selectOption('US');
  await page.getByLabel('Country').selectOption({ label: 'United States' });

  // Multi-select
  await page.getByLabel('Skills').selectOption(['javascript', 'python']);

  // Checkbox and radio
  await page.getByRole('checkbox', { name: 'Newsletter' }).check();
  await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck();
  await page.getByRole('radio', { name: 'Male' }).check();

  // File upload
  await page.getByLabel('Upload').setInputFiles('path/to/file.pdf');
  await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf']);

  // Submit
  await page.getByRole('button', { name: 'Submit' }).click();

  // Verify success
  await expect(page.getByText('Form submitted')).toBeVisible();
});
import { test, expect } from '@playwright/test';

test('navigation and waits', async ({ page }) => {
  // Navigate
  await page.goto('https://example.com');
  await page.goto('/dashboard', { waitUntil: 'networkidle' });

  // Click with navigation
  await Promise.all([
    page.waitForNavigation(),
    page.click('text=Next Page')
  ]);

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

  // Wait for response
  const response = await page.waitForResponse('**/api/users');
  const data = await response.json();

  // Wait for request
  const request = await page.waitForRequest('**/api/submit');

  // Wait for function
  await page.waitForFunction(() => {
    return document.querySelector('.count')?.textContent === '10';
  });

  // Wait for timeout (avoid if possible)
  await page.waitForTimeout(1000);

  // Wait for load state
  await page.waitForLoadState('domcontentloaded');
  await page.waitForLoadState('networkidle');
});

API Testing

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

test.describe('API Tests', () => {
  test('GET request', async ({ request }) => {
    const response = await request.get('/api/users');

    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);

    const users = await response.json();
    expect(users).toHaveLength(10);
  });

  test('POST request', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    });

    expect(response.status()).toBe(201);

    const user = await response.json();
    expect(user.name).toBe('John Doe');
  });

  test('with headers', async ({ request }) => {
    const response = await request.get('/api/protected', {
      headers: {
        Authorization: 'Bearer token123'
      }
    });

    expect(response.ok()).toBeTruthy();
  });
});

Mocking Network

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

test('mock API response', async ({ page }) => {
  // Mock response
  await page.route('**/api/users', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Mock User' }
      ])
    });
  });

  await page.goto('/users');
  await expect(page.getByText('Mock User')).toBeVisible();
});

test('modify response', async ({ page }) => {
  await page.route('**/api/users', async route => {
    const response = await route.fetch();
    const json = await response.json();

    // Modify response
    json.push({ id: 999, name: 'Added User' });

    await route.fulfill({
      response,
      body: JSON.stringify(json)
    });
  });

  await page.goto('/users');
});

test('block requests', async ({ page }) => {
  // Block images
  await page.route('**/*.{png,jpg,jpeg}', route => route.abort());

  // Block analytics
  await page.route('**/analytics/**', route => route.abort());

  await page.goto('/');
});

Visual Testing

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

test('visual comparison', async ({ page }) => {
  await page.goto('/');

  // Full page screenshot
  await expect(page).toHaveScreenshot('homepage.png');

  // Element screenshot
  await expect(page.locator('.hero')).toHaveScreenshot('hero.png');

  // With options
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixels: 100,
    threshold: 0.2,
    animations: 'disabled'
  });
});

test('take screenshot', async ({ page }) => {
  await page.goto('/');

  // Save screenshot
  await page.screenshot({ path: 'screenshot.png' });

  // Full page
  await page.screenshot({ path: 'full.png', fullPage: true });

  // Specific element
  await page.locator('.card').screenshot({ path: 'card.png' });
});

Authentication

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByText('Dashboard')).toBeVisible();

  // Save auth state
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: {
        storageState: 'playwright/.auth/user.json',
      },
    },
  ],
});

Page Object Model

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.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', 'password');

  await expect(page).toHaveURL('/dashboard');
});

Running Tests

# Run all tests
npx playwright test

# Run specific file
npx playwright test login.spec.ts

# Run specific test
npx playwright test -g "should login"

# Run in headed mode
npx playwright test --headed

# Run specific browser
npx playwright test --project=chromium

# Debug mode
npx playwright test --debug

# Show report
npx playwright show-report

# Update snapshots
npx playwright test --update-snapshots

Summary

FeatureUsage
SelectorsgetByRole(), getByText()
Assertionsexpect() with matchers
Navigationgoto(), waitFor*()
API Testingrequest.get(), request.post()
Mockingpage.route()
VisualtoHaveScreenshot()
AuthstorageState

Playwright provides reliable, cross-browser end-to-end testing for modern web applications.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.