TechLead

End-to-End Testing with Playwright

Master Playwright for modern E2E testing with auto-waiting, parallel execution, and CI/CD integration for reliable browser tests

Why Playwright?

Playwright is a modern end-to-end testing framework developed by Microsoft. It supports Chromium, Firefox, and WebKit with a single API, features auto-waiting, and provides powerful tools for reliable testing. Unlike Cypress, Playwright can handle multiple tabs, browser contexts, and cross-origin scenarios natively.

🎭 Playwright Advantages:

Auto-waiting eliminates flaky tests, multi-browser support ensures cross-browser compatibility, and built-in test generation speeds up authoring.

Setting Up Playwright

# Install Playwright
npm init playwright@latest

# This creates:
# playwright.config.ts - Configuration file
# tests/ - Test directory
# tests-examples/ - Example tests

# Run tests
npx playwright test

# Run with UI mode
npx playwright test --ui

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

# Show report
npx playwright show-report
// 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', use: { ...devices['iPhone 13'] } },
  ],

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

Writing Tests with Locators

Playwright uses locators that auto-wait for elements to be actionable before performing actions.

// tests/login.spec.ts
import { test, expect } from '@playwright/test';

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

  test('should login with valid credentials', async ({ page }) => {
    // Playwright auto-waits for elements
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('password123');
    await page.getByRole('button', { name: 'Sign In' }).click();

    // Assertions also auto-wait
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.getByLabel('Email').fill('wrong@example.com');
    await page.getByLabel('Password').fill('wrongpass');
    await page.getByRole('button', { name: 'Sign In' }).click();

    await expect(page.getByText('Invalid credentials')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('should validate required fields', async ({ page }) => {
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page.getByText('Email is required')).toBeVisible();
  });
});

Page Object Model

Organize test logic into reusable page objects for maintainability.

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

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: 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' });
    this.errorMessage = page.getByRole('alert');
  }

  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();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }
}

// tests/login-pom.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('login with POM', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password123');
  await expect(page).toHaveURL('/dashboard');
});

Screenshots, Videos & Tracing

// Capture screenshots programmatically
test('visual check', async ({ page }) => {
  await page.goto('/products');

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

  // Element screenshot
  const hero = page.locator('.hero-section');
  await hero.screenshot({ path: 'screenshots/hero.png' });

  // Visual comparison (snapshot testing)
  await expect(page).toHaveScreenshot('products-page.png', {
    maxDiffPixelRatio: 0.01,
  });
});

// CI/CD GitHub Actions workflow
// .github/workflows/e2e.yml
// name: E2E Tests
// on: [push, pull_request]
// jobs:
//   test:
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-node@v4
//       - run: npm ci
//       - run: npx playwright install --with-deps
//       - run: npx playwright test
//       - uses: actions/upload-artifact@v4
//         if: failure()
//         with:
//           name: playwright-report
//           path: playwright-report/

🎭 Playwright vs Cypress:

Playwright Strengths:

  • + Multi-browser (Chromium, Firefox, WebKit)
  • + Multi-tab & multi-origin support
  • + Faster parallel execution
  • + Better auto-waiting

Cypress Strengths:

  • + Simpler API for beginners
  • + Time-travel debugging
  • + Better DX for component testing
  • + Larger ecosystem of plugins

Key Takeaways

  • Use role-based and label-based locators for resilient selectors
  • Leverage auto-waiting instead of manual waits or sleeps
  • Use Page Object Model for complex test suites
  • Configure screenshots, videos, and traces for debugging CI failures
  • Run tests in parallel across multiple browsers for fast feedback

Continue Learning