e2e

Playwright E2E testing patterns. Use when working on files in tests/e2e/.

SKILL.md

GitHub

E2E Test Guidelines

Philosophy

  • Journey-style tests covering critical flows end-to-end
  • Strongly prefer adding test.step() to existing tests over creating new tests - keeps suite fast and avoids duplicating slow setup flows
  • Fix flaky tests immediately, regardless of when introduced

Commands

@playwright/test is the automated test runner:

run test e2e --retries=0 --reporter=line --max-failures=1 2>&1
run test e2e --retries=0 --workers=4 --repeat-each=5 --reporter=line 2>&1

Use MONEYFLOW_E2E_PORT=3107 run test e2e when another checkout owns port 3000. The managed server and independently created browser contexts share E2E_BASE_URL from tests/e2e/helpers/base-url.ts, so never hardcode an origin in a spec.

@playwright/cli is the separate manual/agent browser CLI. Use the repository-installed binary, not Playwright MCP, npx, an ad-hoc Node script, or a temporary test file:

run browser --help
run browser -s=<unique-session> open http://localhost:3000
run browser -s=<unique-session> snapshot
run browser -s=<unique-session> console error
run browser -s=<unique-session> requests
run browser -s=<unique-session> close
run browser -s=<unique-session> delete-data

Use a uniquely named, non-persistent session for each smoke test. Exercise the UI through CLI commands, verify persistence with reload, inspect console and failed network requests, then close the session, delete its data, and remove generated .playwright-cli/ artifacts. Manual CLI smoke coverage complements the automated suite; neither substitutes for the other.

NEVER use --debug, --ui, --headed, or show - they open a GUI and can block agents.

Selectors (priority order)

  1. getByRole()
  2. getByTestId()
  3. getByLabel()
  4. getByText(/regex/i)

Critical Rules

  • Assert behaviour, not text - text changes with copy edits/i18n
  • Independent tests - use beforeEach, don't depend on prior test state
  • No arbitrary waits - use toBeEnabled(), not waitForTimeout()

Timeouts

playwright.config.ts sets expect: { timeout: 15_000 }, so a bare assertion waits 15 s, not Playwright's 5 s default. An explicit per-assertion timeout still overrides it, so deliberately short probes such as toBeVisible({ timeout: 3_000 }) are unaffected.

The cost falls on assertions that are meant to fail slowly. 113 of the 115 toHaveCount(0) absence assertions carry no explicit timeout: a passing one still returns as soon as the condition holds, but a genuinely failing one now burns 15 s. Two failing bare assertions in one test exhaust the 30 s test budget and report a test timeout instead of the assertion's own error, which is a worse diagnostic. Give an absence assertion you expect to be contentious an explicit shorter timeout, or raise the test budget with test.setTimeout().

Helpers

Import from tests/e2e/helpers/:

  • createNewIdentity(page) - full new user flow
  • goToTransactions(page), goToTags(page), etc.

Create helpers for multi-step reused flows. Don't wrap single Playwright calls.

Pattern

test.describe("Feature", () => {
    test.beforeEach(async ({ page }) => {
        await createNewIdentity(page);
        await goToFeature(page);
    });

    test("should do thing", async ({ page }) => {
        // Arrange → Act → Assert
    });
});