Files
magnus919_agent-skills/playwright/references/01-e2e-authoring.md
T
Magnus HedemarkGitHubfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
ac1beb117d feat(skill): add Playwright skill (E2E testing + scraping + headless browsing) (#264)
Add ONE tool skill for Playwright: SKILL.md covering E2E test authoring,
selector robustness, network interception/mocking, parallel workers, CI
integration, scraping/headless patterns, accessibility snapshot checks, and
headed debugging; scripts/pwrun (agent-first smoke harness with --json,
fixture-tested); templates/ test-suite scaffold; eight dated references; a
schema-valid evals/evals.json (6 cases); a human-facing README; reverse
routing from qa-methodology and frontend-engineering; top-level README index
entry; and regenerated catalogs (llms.txt, marketplace, codex).

Closes #244.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 17:59:38 -04:00

3.5 KiB

E2E Test Authoring

Last Updated: 2026-08-03

How to write Playwright E2E tests that are readable, stable, and fast to maintain. Authoring decisions are execution-side; what to test and at what level is QA strategy owned by qa-methodology (its qa-methodology/references/test-strategy.md).

Anatomy of a spec

One spec file per user journey, described in prose, located by behavior:

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

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

  test('completes a purchase with a saved card', async ({ page }) => {
    await page.getByRole('button', { name: 'Checkout' }).click();
    await page.getByRole('button', { name: 'Place order' }).click();
    await expect(page.getByRole('heading', { level: 1 })).toHaveText('Order confirmed');
  });
});

Rules of thumb:

  • One journey per test. A test that verifies two unrelated flows fails for two unrelated reasons and gets rewritten as two tests anyway.
  • Setup in beforeEach / fixtures, not in the test body. Keep the test body readable as a spec of the behavior.
  • Describe what the user does, not what the DOM does: "places an order", not "clicks the button with class .btn-primary".

Web-first assertions (no sleeps)

Playwright assertions retry until a timeout:

await expect(page.getByText('Saved')).toBeVisible();
await expect(input).toHaveValue('50');
await expect(page).toHaveURL(/\/orders\/\d+/);
  • Never await page.waitForTimeout(2000) to "fix" a race — it slows the suite and hides the real timing bug.
  • For genuinely async conditions use expect.poll() or expect(...).toPass() instead of arbitrary sleeps.

Fixtures

Shared setup lives in a fixture file and is composed per test:

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

export const test = base.extend<{ signedInPage: Page }>({
  signedInPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('qa@example.com');
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await use(page);
  },
});

Starting the app: webServer

Declare the app lifecycle in the config so Playwright starts, waits for, and tears down the server per run (see templates/playwright.config.ts):

  • webServer.command — the dev/preview command.
  • webServer.url — a readiness URL; Playwright polls it before running tests.
  • reuseExistingServer: !process.env.CI — reuse a dev server locally, always start fresh on CI.

Prefer webServer over asking the agent to start the app manually; the config makes the run reproducible in CI too.

The page-object model (at the size where it pays)

Group locators and actions for a screen into a class when a spec grows beyond ~15 lines or the same flow is asserted from several specs:

export class CartPage {
  constructor(private readonly page: Page) {}
  async open() { await this.page.goto('/cart'); }
  async applyPromo(code: string) { await this.page.getByLabel('Promo code').fill(code); }
  get checkoutButton() { return this.page.getByRole('button', { name: 'Checkout' }); }
}

Do not add a POM layer preemptively — one spec that reads as prose beats a POM with one user.

  • Locator choice and flaky-selector repair: 02-selectors.md.
  • Mocking external HTTP so tests stay hermetic: 03-network-interception-and-mocking.md.
  • Snapshot-style accessibility assertions: 07-accessibility-and-debugging.md.