E2E Test Runner
SkillWeb & browsingLets your agent run, debug, and write Playwright end-to-end browser tests for this project.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the E2E Test Runner skill
About this capability
Run, debug, and manage Playwright e2e tests. Use when running e2e tests, debugging test failures, writing new specs, or investigating test infrastructure issues.
What this skill tells your AI
The instructions your AI receives, as published by sendou-ink/sendou.ink in .claude/skills/e2e/SKILL.md and read by ahel’s review.
Architecture overview
- Tests live in
e2e/*.spec.ts, config inplaywright.config.ts - Page objects live in
e2e/pages/<feature>/— every spec uses them; conventions indocs/dev/e2e-page-objects.md, gotchas indocs/dev/e2e-page-objects-migration.md - Global setup (
e2e/global-setup.ts) builds the app (skipped when no build input changed since the last e2e build — tracked via.e2e-build-marker), creates/migrates per-worker databases (viascripts/ensure-test-db.ts: pending migrations are applied, drifted databases are rebuilt), and starts one server per worker - Port calculation:
E2E_BASE_PORT = PORT (from .env) + 500. Worker N uses port base+N, except ports on the WHATWG fetch bad port list (e.g. 6679) are skipped — seee2eWorkerPortine2e/helpers/playwright.ts - Worker count:
E2E_WORKERSenv, defaulting tomin(8, max(4, cores - 2)) - Worker databases:
db-test-e2e-<N>.sqlite3in the project root; every test starts from a wiped database holding only the admin (Sendou) and N-ZAP users, and builds its own data with thefactoriesfixture - SeaweedFS (S3-compatible storage) is started via Docker Compose if not already running
Pre-flight checks (run before every test execution)
Before running tests, check for these common issues:
-
Port conflicts — Check if anything is already listening on the e2e ports (base port + worker index):
lsof -i :4501-4508 2>/dev/nullIf ports are occupied by leftover e2e servers, kill them. If occupied by something else, warn the user.
-
Docker running — SeaweedFS requires Docker. Check with
docker infoif there are storage-related failures.
Stale worker databases (db-test-e2e-*.sqlite3) are handled automatically: global setup applies pending migrations and rebuilds databases whose migration history has drifted.
Running tests
Run all tests
pnpm run test:e2e
Run a specific test file
pnpm exec playwright test e2e/<name>.spec.ts
Batch multiple files into one invocation — every invocation pays global setup.
Flaky detection (repeats each test 10 times, stops on first failure)
pnpm run test:e2e:flaky-detect
Force a rebuild of the app
E2E_FORCE_BUILD=true pnpm run test:e2e
Global setup reuses the previous build when nothing under app/, public/, the lockfile, or the vite/react-router configs changed. Use this override if you suspect a stale build (e.g. after changing env-dependent build behavior).
Debugging failures
Follow this funnel when tests fail:
Step 1: Read the error output
- Look for the actual assertion or timeout that failed
- Check if it's an infrastructure error (server didn't start, migration failed) vs. a test logic error
Step 2: Check infrastructure issues
Common infrastructure errors and fixes:
- "table already exists" → Should not happen anymore (global setup rebuilds drifted worker DBs); if it does,
rm -f db-test-e2e-*.sqlite3and investigatescripts/ensure-test-db.ts - "Server on port X did not start within timeout" → Port conflict or app build error. Check ports with
lsof -i :<port>and check for build errors - "SeaweedFS failed to start" → Docker not running or compose issue. Check
docker info - "Test ended with database writes the server never saw" → A factory call was not followed by a helper that talks to the server; add a
navigate/impersonateafter the writes
Step 3: Reduce to single debug worker
If the error is unclear, re-run with debug output and a single worker to see server logs:
E2E_DEBUG=true E2E_WORKERS=1 pnpm exec playwright test e2e/<failing-test>.spec.ts
This shows stdout/stderr from the test server, which is hidden by default.
Step 4: Examine trace artifacts
Playwright is configured with trace: { mode: "retain-on-failure", snapshots: false } — failure traces carry screenshots, network and action logs but no DOM snapshots (recording those cost ~17% of total suite time). After a failure, check test-results/<test-folder>/error-context.md for the page's accessibility snapshot at failure time, or view the trace:
pnpm exec playwright show-trace test-results/<test-folder>/trace.zip
When a failure needs full DOM snapshots to understand, re-run just that test with --trace retain-on-failure (the CLI flag records complete traces).
Re-render races
Live events run on an in-process event bus per worker server (SSE, see app/features/events), so cross-worker crosstalk cannot cause flakes. Google Fonts are also blocked at the context level so font swaps never reflow the page mid-test. Re-renders from the test's own action revalidations can still swallow a React Aria press (press start registers, press end never fires — no POST); waitForPOSTResponse retries for this, so route flows through it rather than adding sleeps.
Other known sources, each handled once in the helpers or page objects:
- YouTube is stubbed at the context level (a fake
iframe_api, everything else aborted): the real player loads from the internet at its own pace and, arriving mid-test, closed an open select on the VoD form - dnd-kit stops every click in the document for 50ms after a drop; drag helpers end with
waitForDropToSettleso the next click lands - A tab or link clicked before hydration (after a raw
page.reload(), say) loads the target as a new document and the click after it is lost; reload through the page object'sreload()which waits for hydration - Popovers close on navigation in a passive effect, so for a frame the old panel and the new page both show the same names; scope locators to
mainwhere a name can appear in both - React Router drops a fetcher's redirect when a navigation (a revalidation included) started after the submission; broadcast revalidations are jittered up to 1.5s after a live event, so one could land mid-submission and the action silently did nothing.
holdRevalidationsDuring(used byuseActionSubmitandSendouForm) defers them until the submission settles
Test pattern reference
Every test builds its own data with factories and drives the UI through page objects:
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { expect, impersonate, test } from "./helpers/playwright";
import { BuildsPage } from "./pages/builds/builds-page";
test.describe("Feature", () => {
test("does something", async ({ page, factories }) => {
await factories.BuildFactory.create({ ownerId: NZAP_TEST_ID });
await impersonate(page, NZAP_TEST_ID);
const builds = new BuildsPage(page);
await builds.goto();
// ... interact via page object methods, assert in the spec ...
});
});
Key rules:
- The database starts each test holding only the admin and N-ZAP; the
factoriesworker fixture (seee2e/helpers/factories.tsfor the registry) creates everything else - Locators live in page objects under
e2e/pages/— specs contain no rawgetByTestId/getByRolecalls; seedocs/dev/e2e-page-objects.md - Use
navigate()instead ofpage.goto()— it waits for hydration (page objects'goto()methods wrap it) - Use
submit()instead of clicking submit buttons directly — it waits for the POST response - Use
impersonate(page, userId?)to authenticate. Default is admin (ADMIN_ID); prefer N-ZAP (NZAP_TEST_ID) when the flow doesn't need admin rights - Avoid
page.waitForTimeout— use assertions orwaitForpatterns instead (the one exception iswaitForDropToSettle, dnd-kit's post-drop window has nothing observable to wait on) - Import
testfrom./helpers/playwright(not from@playwright/test) — it includes worker port fixtures and the database reset - Factory writes must be followed by a helper that talks to the server (
navigate,impersonate,submit) or the test fails with "writes the server never saw"
Environment variables
| Variable | Purpose | Default |
|---|---|---|
E2E_WORKERS | Number of parallel workers | min(8, max(4, cores − 2)) |
E2E_DEBUG | Show server stdout/stderr when "true" | unset |
E2E_FORCE_BUILD | Rebuild the app even when inputs look unchanged | unset |
PORT | Base port for dev server (e2e adds 500) | 5173 |
Signals
- GitHub stars
- 295
- Forks
- 169
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
e2e-sendou-ink- Source
- github.com/sendou-ink/sendou.ink