SuperAPI AI Directory

playwright-test

Testing & quality

View repository

Updated 10-15 21:38

About this skill

Playwright Test (@playwright/test 1.63)

Config

Two projects, one dev server. Mobile uses a device descriptor.

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

export default defineConfig({ testDir: 'e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? [['github'], ['html']] : 'list', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry' }, expect: { toHaveScreenshot: { maxDiffPixels: 100, animations: 'disabled' } }, projects: [ { name: 'desktop', use: { ...devices['Desktop Chrome'] } }, { name: 'mobile', use: { ...devices['Pixel 7'] } }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120_000, stderr: 'pipe', }, });


`webServer.url` must answer 2xx to 4xx before tests start. `port` is deprecated, use `url`. The first `next dev` compile is slow, so keep the timeout at 120 s or more.

## Specs

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

test.describe('home', () => {
  test('renders the hero heading', async ({ page }) => {
    await page.goto('/');
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
  });
});
  • Prefer getByRole, getByLabel, getByText over CSS selectors.
  • Use web-first assertions (await expect(locator)...). They retry. expect(await locator.isVisible()) does not.
  • Never waitForTimeout. Wait on a locator state or a response.
  • Scope by project with test.skip(({ isMobile }) => isMobile, 'desktop only'). Skip by platform with test.skip(process.platform !== 'linux', 'snapshots are Linux only').

Snapshots

test('home matches', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home.png', { fullPage: true });
});
  • First run writes the baseline and fails. Review it, then rerun.
  • Update with npx playwright test --update-snapshots.
  • Files get the browser, project and platform suffix, for example home-desktop-linux.png. Rendering differs by OS, fonts and GPU, so a baseline made on macOS fails on Linux.
  • Options used here: maxDiffPixels, animations: 'disabled', mask, maskColor, stylePath, fullPage. Set shared defaults under expect.toHaveScreenshot in the config.

Linux-only baselines

Generate and update baselines only in the CI image, or the matching Playwright Docker image locally. Commit only *-linux.png. Gate the visual specs so other platforms skip instead of failing:

test.skip(process.platform !== 'linux', 'visual baselines are Linux only');

Clock and time

Freeze dates before navigation so relative dates and "year" copy stay stable.

test.beforeEach(async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-01-15T12:00:00Z'));
});
  • setFixedTime freezes Date.now() and new Date() while timers keep running.
  • install() fakes timers and rAF. Then runFor(ms) fires callbacks, fastForward(ms) jumps, pauseAt(date) freezes, resume() restores flow.
  • page.clock applies to the whole browser context.

Reduced motion

test.use({ reducedMotion: 'reduce' });
// or inside a test, before goto
await page.emulateMedia({ reducedMotion: 'reduce' });

Use it for two things. Assert the reduced path (no transform animation, content visible). Stabilize screenshots of pages that gate animation on prefers-reduced-motion.

Masking

await expect(page).toHaveScreenshot({
  mask: [page.locator('canvas'), page.getByTestId('live-clock')],
  maskColor: '#ff00ff',
});

Mask anything non-deterministic: canvas, video, avatars, counters, embeds. Masked areas paint a solid box in both baseline and actual.

WebGL canvas stability

Pixel-diffing a WebGL scene is flaky because of GPU, driver and frame timing. In order of preference:

  1. Mask the canvas and assert on the DOM around it. Separately assert the canvas exists and has nonzero size.
  2. Add a query flag or reducedMotion branch in the app that renders a static frame, then screenshot after the ready signal.
  3. Software GL (untested, confirm on your CI before relying on it): use: { launchOptions: { args: ['--use-gl=angle', '--use-angle=swiftshader'] } }. Output stays Linux only.

Wait for an explicit ready marker (data-ready attribute set after the first frame), not a timeout. Fix the viewport via project use, and keep deviceScaleFactor constant.

CI notes

  • retries: 2, workers: 1 (or a small number), forbidOnly: true, trace: 'on-first-retry'.
  • Install browsers with npx playwright install --with-deps chromium.
  • Upload playwright-report/ and test-results/ on failure. Diff images live in test-results.
  • Run against next build && next start in CI when dev compile time causes timeouts. Local runs can keep next dev via reuseExistingServer.
  • Retries hide flaky screenshots. Fix the nondeterminism first.

Author: Ender Puentes (with Claude Code) Source: https://playwright.dev/docs/intro