e2e
Playwright E2E testing patterns. Use when working on files in tests/e2e/.
SKILL.md.claude/skills/e2e/SKILL.md
GitHubE2E 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)
getByRole()getByTestId()getByLabel()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(), notwaitForTimeout()
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 flowgoToTransactions(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
});
});