Agent Skills: Playwright Test (@playwright/test 1.63)

Author end-to-end specs and visual regression tests with @playwright/test 1.63. Use when writing or fixing playwright.config.ts, spec files, toHaveScreenshot snapshots, fixed clocks, reduced-motion checks, masking, WebGL canvas stability, or CI retries. Not for ad hoc browser automation (use playwright-cli).

UncategorizedID: enderpuentes/ai-agent-skills/playwright-test

Install this agent skill to your local

pnpm dlx add-skill https://github.com/EnderPuentes/ai-agent-skills/tree/HEAD/playwright-test

Skill Files

Browse the full folder contents for playwright-test.

Download Skill

Loading file tree…

playwright-test/SKILL.md

Skill Metadata

Name
playwright-test
Description
Author end-to-end specs and visual regression tests with @playwright/test 1.63. Use when writing or fixing playwright.config.ts, spec files, toHaveScreenshot snapshots, fixed clocks, reduced-motion checks, masking, WebGL canvas stability, or CI retries. Not for ad hoc browser automation (use playwright-cli).

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

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