← All skills

Playwright Skill

Hot
E2e testingJavaScriptTypeScriptPythonJavaC#

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill playwright-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Playwright — Advanced Implementation Playbook

§1 — Production Configuration

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/specs',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'results.json' }],
    ['junit', { outputFile: 'results.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] }, dependencies: ['setup'] },
    { name: 'webkit', use: { ...devices['Desktop Safari'] }, dependencies: ['setup'] },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] }, dependencies: ['setup'] },
    { name: 'mobile-safari', use: { ...devices['iPhone 13'] }, dependencies: ['setup'] },
  ],
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

§2 — Auth Fixture Reuse

// 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('password123');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: authFile });
});
// tests/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';

type Fixtures = {
  authenticatedPage: Page;
  adminPage: Page;
};

export const test = base.extend<Fixtures>({
  authenticatedPage: async ({ browser }, use) => {
    const ctx = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
    const page = await ctx.newPage();
    await use(page);
    await ctx.close();
  },
  adminPage: async ({ browser }, use) => {
    const ctx = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
    const page = await ctx.newPage();
    await use(page);
    await ctx.close();
  },
});

export { expect } from '@playwright/test';

§3 — Page Object Model

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

export abstract class BasePage {
  constructor(protected page: Page) {}

  protected async navigate(path: string) {
    await this.page.goto(path);
    await this.page.waitForLoadState('domcontentloaded');
  }

  protected getByTestId(id: string): Locator {
    return this.page.getByTestId(id);
  }

  async waitForToast(text: string) {
    await expect(this.page.getByRole('alert')).toContainText(text);
  }

  async takeScreenshot(name: string) {
    await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true });
  }
}

// tests/pages/login.page.ts
import { BasePage } from './base.page';
import { DashboardPage } from './dashboard.page';

export class LoginPage extends BasePage {
  private emailInput = this.page.getByLabel('Email');
  private passwordInput = this.page.getByLabel('Password');
  private submitButton = this.page.getByRole('button', { name: 'Sign In' });
  private errorMessage = this.page.getByRole('alert');

  async open() {
    await this.navigate('/login');
    return this;
  }

  async loginAs(email: string, password: string): Promise<DashboardPage> {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
    await this.page.waitForURL('/dashboard');
    return new DashboardPage(this.page);
  }

  async loginExpectingError(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
    return this;
  }

  async getErrorText(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }
}

// Usage in test:
test('login with valid credentials', async ({ page }) => {
  const loginPage = await new LoginPage(page).open();
  const dashboard = await loginPage.loginAs('user@test.com', 'password');
  await expect(page.getByRole('heading', { level: 1 })).toHaveText('Dashboard');
});

§4 — Advanced Network Interception

// Mock API response
test('handles API failure gracefully', async ({ page }) => {
  await page.route('**/api/products', route =>
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' }),
    })
  );
  await page.goto('/products');
  await expect(page.getByText('Something went wrong')).toBeVisible();
});

// Modify live response
test('injects test data into real response', async ({ page }) => {
  await page.route('**/api/products', async route => {
    const response = await route.fetch();
    const json = await response.json();
    json.push({ id: 999, name: 'Test Product', price: 0.01 });
    await route.fulfill({ response, json });
  });
  await page.goto('/products');
  await expect(page.getByText('Test Product')).toBeVisible();
});

// Wait for specific API response
test('waits for data load', async ({ page }) => {
  const responsePromise = page.waitForResponse(
    resp => resp.url().includes('/api/dashboard') && resp.status() === 200
  );
  await page.goto('/dashboard');
  const response = await responsePromise;
  const data = await response.json();
  expect(data.items.length).toBeGreaterThan(0);
});

// Block resources for speed
test('fast page load without images', async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => route.abort());
  await page.goto('/heavy-page');
});

// HAR recording & replay
test('replay from HAR file', async ({ page }) => {
  await page.routeFromHAR('tests/fixtures/api.har', { notFound: 'fallback' });
  await page.goto('/products');
});

§5 — Visual Regression Testing

test('homepage visual regression', async ({ page }) => {
  await page.goto('/');
  await page.waitForLoadState('networkidle');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01,
    animations: 'disabled',
    mask: [page.locator('.timestamp'), page.locator('.ad-banner')],
  });
});

test('component visual test', async ({ page }) => {
  await page.goto('/components');
  const card = page.locator('.product-card').first();
  await expect(card).toHaveScreenshot('product-card.png', { threshold: 0.2 });
});

// Update baselines: npx playwright test --update-snapshots

§6 — File Upload & Download

// File upload
test('upload profile photo', async ({ page }) => {
  await page.goto('/settings/profile');
  const fileChooserPromise = page.waitForEvent('filechooser');
  await page.getByRole('button', { name: 'Upload Photo' }).click();
  const fileChooser = await fileChooserPromise;
  await fileChooser.setFiles('tests/fixtures/avatar.png');
  await expect(page.getByAltText('Profile Photo')).toBeVisible();
});

// Multiple files
test('upload multiple documents', async ({ page }) => {
  await page.setInputFiles('input[type="file"]', [
    'tests/fixtures/doc1.pdf',
    'tests/fixtures/doc2.pdf',
  ]);
});

// File download
test('download report', async ({ page }) => {
  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'Download Report' }).click();
  const download = await downloadPromise;
  expect(download.suggestedFilename()).toBe('report.pdf');
  await download.saveAs(`downloads/${download.suggestedFilename()}`);
});

§7 — Multi-Tab, Popup & Dialog Handling

// Handle new tab/popup
test('external link opens new tab', async ({ page, context }) => {
  const pagePromise = context.waitForEvent('page');
  await page.getByRole('link', { name: 'External Link' }).click();
  const newPage = await pagePromise;
  await newPage.waitForLoadState();
  expect(newPage.url()).toContain('external-site.com');
  await newPage.close();
});

// Handle dialog (alert/confirm/prompt)
test('confirm delete action', async ({ page }) => {
  page.on('dialog', dialog => {
    expect(dialog.message()).toContain('Are you sure?');
    dialog.accept();
  });
  await page.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByText('Deleted successfully')).toBeVisible();
});

// Handle prompt dialog
test('rename item via prompt', async ({ page }) => {
  page.on('dialog', dialog => dialog.accept('New Name'));
  await page.getByRole('button', { name: 'Rename' }).click();
});

§8 — Geolocation, Permissions & Device Emulation

test('location-based search', async ({ browser }) => {
  const context = await browser.newContext({
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
    permissions: ['geolocation'],
  });
  const page = await context.newPage();
  await page.goto('/stores/nearby');
  await expect(page.getByText('New York')).toBeVisible();
  await context.close();
});

// Timezone & locale
test('displays correct timezone', async ({ browser }) => {
  const context = await browser.newContext({
    timezoneId: 'Asia/Tokyo',
    locale: 'ja-JP',
  });
  const page = await context.newPage();
  await page.goto('/settings');
  await expect(page.getByText('日本時間')).toBeVisible();
  await context.close();
});

// Color scheme
test('dark mode renders correctly', async ({ browser }) => {
  const context = await browser.newContext({ colorScheme: 'dark' });
  const page = await context.newPage();
  await page.goto('/');
  await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(0, 0, 0)');
  await context.close();
});

§9 — Custom Test Fixtures

import { test as base, APIRequestContext } from '@playwright/test';

// Database seeding fixture
type MyFixtures = {
  seedDB: void;
  apiContext: APIRequestContext;
};

export const test = base.extend<MyFixtures>({
  seedDB: [async ({}, use) => {
    await fetch('http://localhost:3001/api/test/seed', { method: 'POST' });
    await use();
    await fetch('http://localhost:3001/api/test/cleanup', { method: 'POST' });
  }, { auto: true }],

  apiContext: async ({ playwright }, use) => {
    const ctx = await playwright.request.newContext({
      baseURL: 'http://localhost:3000/api',
      extraHTTPHeaders: { Authorization: 'Bearer test-token' },
    });
    await use(ctx);
    await ctx.dispose();
  },
});

§10 — API Testing with Playwright

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

test.describe('API Tests', () => {
  test('GET /api/users returns users', async ({ request }) => {
    const response = await request.get('/api/users');
    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    expect(body).toHaveLength(expect.any(Number));
    expect(body[0]).toHaveProperty('name');
  });

  test('POST /api/users creates user', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: { name: 'Test User', email: 'test@example.com' },
    });
    expect(response.status()).toBe(201);
    const user = await response.json();
    expect(user.name).toBe('Test User');
  });

  test('end-to-end: API + UI', async ({ page, request }) => {
    // Create via API
    const response = await request.post('/api/products', {
      data: { name: 'Playwright Widget', price: 29.99 },
    });
    const product = await response.json();

    // Verify in UI
    await page.goto(`/products/${product.id}`);
    await expect(page.getByRole('heading')).toHaveText('Playwright Widget');
  });
});

§11 — Accessibility Testing Integration

import AxeBuilder from '@axe-core/playwright';

test('page passes accessibility audit', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
    .exclude('.third-party-widget')
    .analyze();
  expect(results.violations).toEqual([]);
});

test('form has proper ARIA labels', async ({ page }) => {
  await page.goto('/contact');
  const results = await new AxeBuilder({ page })
    .include('#contact-form')
    .analyze();
  expect(results.violations).toEqual([]);
});

§12 — Parallel & Sharding

# Run with sharding (CI matrix)
npx playwright test --shard=1/4
npx playwright test --shard=2/4

# Merge shard reports
npx playwright merge-reports ./all-blob-reports --reporter html
# GitHub Actions with sharding
jobs:
  test:
    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ strategy.job-index }}
          path: blob-report/
  merge-reports:
    needs: test
    steps:
      - uses: actions/download-artifact@v4
        with: { pattern: blob-report-*, merge-multiple: true, path: all-blob-reports }
      - run: npx playwright merge-reports --reporter html ./all-blob-reports

§13 — CI/CD Integration

name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
        env:
          CI: true
          BASE_URL: http://localhost:3000
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

§14 — Debugging Toolkit

# Debug mode — step through tests
npx playwright test --debug

# UI mode — interactive test explorer
npx playwright test --ui

# Trace viewer
npx playwright test --trace on
npx playwright show-trace test-results/trace.zip

# Codegen — record tests from browser
npx playwright codegen https://example.com

# Show report
npx playwright show-report

# Run specific test
npx playwright test login.spec.ts --project=chromium

§15 — Debugging Quick-Reference

ProblemCauseFix
TimeoutError: locator.clickElement not found or not clickableCheck locator accuracy, use --debug to inspect
strict mode violationLocator matches multiple elementsAdd first(), nth(), or make locator more specific
Target closedPage/context closed during operationAwait navigation properly, check waitForURL
net::ERR_CONNECTION_REFUSEDwebServer not startedCheck webServer config, verify reuseExistingServer
Test passes locally, fails CITiming/rendering differencesUse waitForLoadState, add retries: 2 for CI
Flaky testRace conditionUse web-first assertions (expect(locator)), avoid raw page.$
Screenshot mismatchFont/rendering differencesUse maxDiffPixelRatio, mask dynamic elements
download event not firingMissing browser-level triggerUse waitForEvent('download') before click
filechooser never resolvesInput hidden or overlayedUse setInputFiles('input[type=file]', ...) directly
Slow parallel testsWorkers exceed CPU coresSet workers to CPU count, use sharding for CI

§16 — Best Practices Checklist

  • ✅ Use getByRole, getByLabel, getByText over CSS/XPath selectors
  • ✅ Use web-first assertions (expect(locator)) — they auto-retry
  • ✅ Use storageState to skip login in non-auth tests
  • ✅ Mock external APIs with page.route() for reliability
  • ✅ Enable trace on first retry: trace: 'on-first-retry'
  • ✅ Keep tests independent — no shared state between test files
  • ✅ Use baseURL in config, never hardcode URLs
  • ✅ Set fullyParallel: true for maximum speed
  • ✅ Use fixtures for shared setup/teardown, not beforeAll
  • ✅ Use Page Object Model for 3+ page interactions
  • ✅ Run npx playwright codegen to bootstrap tests quickly
  • ✅ Use sharding in CI for faster feedback
  • ✅ Set forbidOnly: !!process.env.CI to prevent .only in CI
  • ✅ Use test.slow() for known-slow tests instead of increasing global timeout
  • ✅ Structure: pages/, specs/, fixtures/, utils/
  • ✅ Use @axe-core/playwright for accessibility testing in pipeline
  • ✅ Store auth state in playwright/.auth/ with .gitignore