scaffold-e2e
Set up Playwright for a pracht app: install it, generate a `playwright.config.ts` that boots `pracht dev` (or the build via `pracht preview`), and emit a smoke test per manifest route asserting 200, h
SKILL.mdskills/scaffold-e2e/SKILL.md
GitHubPracht Scaffold E2E
Generate a Playwright suite that exercises the live app. Aligns with
recipes-testing.md.
Step 1: Decide which build the tests run against
Ask the user (default: dev):
pracht dev— fast, HMR, but can mask production-only bugs (different bundling, different SSG/ISG flow).- Production runtime —
pracht preview: purpose-built, it builds and serves the production output locally for both Node and Cloudflare targets (Vercel prints guidance instead). Usepracht preview --skip-buildto serve an existing build after an explicitpracht build. Prefer this over hand-rollingnode dist/server/server.js, which is Node-adapter-only and skips the build. - External URL — user provides
BASE_URL; tests do not boot the server.
Step 2: Install
pnpm add -D @playwright/test
pnpm exec playwright install chromium
Add firefox and webkit only if the user requests them.
Step 3: Write playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
const PORT = Number(process.env.PORT ?? 3000);
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? "github" : "list",
use: {
baseURL: process.env.BASE_URL ?? `http://localhost:${PORT}`,
trace: "on-first-retry",
screenshot: "only-on-failure",
},
webServer: process.env.BASE_URL
? undefined
: {
command: "pnpm dev", // or "pracht preview" for the production runtime — choose at scaffold time
url: `http://localhost:${PORT}`,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
projects: [
{ name: "chromium", use: devices["Desktop Chrome"] },
],
});
If playwright.config.ts already exists, merge — do not clobber.
Step 4: Generate per-route smoke tests
MCP: when the pracht MCP server is registered (pracht dev-mcp), prefer its
inspect_routes/inspect_api/inspect_build/doctor/verify/generate_*
tools over shelling out. pracht inspect needs the pracht plugin in the vite
config.
pracht inspect routes --json
For every route entry, generate one spec under e2e/smoke/. Skip routes with
dynamic segments unless the user provides example params (ask via
AskUserQuestion).
Per-route template:
import { test, expect } from "@playwright/test";
test.describe("smoke: <path>", () => {
test("loads with 200 and a title", async ({ page }) => {
const errors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(msg.text());
});
page.on("pageerror", (err) => errors.push(err.message));
const response = await page.goto("<path>");
expect(response?.status(), "HTTP status").toBeLessThan(400);
await expect(page).toHaveTitle(/.+/);
expect(errors, "no console errors").toEqual([]);
});
});
Step 5: Generate a navigation crawl
A single e2e/navigation.spec.ts that:
- Visits the home route.
- Waits for hydration with the documented readiness idiom before touching
anything — otherwise the crawl races hydration and clicks fall through to
full page loads:
await page.waitForFunction(() => (window as any).__PRACHT_ROUTER_READY__); - Collects every same-origin
<a href>. - Clicks each in turn, asserts no console errors, no 4xx/5xx.
This catches client-router intercept regressions and broken links.
Step 6: Generate a hydration check (optional)
If any route is render: "ssr" or "ssg"/"isg":
test("server HTML matches client render", async ({ page }) => {
const errors: string[] = [];
page.on("console", (m) => m.type() === "error" && errors.push(m.text()));
await page.goto("<path>");
await page.waitForLoadState("networkidle");
expect(errors.filter((e) => /Hydration|hydrat/i.test(e))).toEqual([]);
});
Step 7: Wire package.json
{
"scripts": {
"e2e": "playwright test",
"e2e:ui": "playwright test --ui"
}
}
If an e2e script already exists in the project, confirm before
overwriting.
Step 8: Run
pnpm e2e
pracht verify --json
If specs fail on first run, report. Common first-run failures:
- Port collision (already-running dev server).
- Missing browser binary (
playwright install chromium). - Strict CSP blocking inline test scripts (rare).
Rules
- Source of routes is
pracht inspect routes --json. Do not globsrc/routes/**. - Skip dynamic-segment routes unless example params are provided.
- Never overwrite an existing
playwright.config.tswithout diffing first. - If the app uses typed routes, run
pracht typegen --checkbefore generating specs and prefer importedhref()for test URLs in app-owned route fixtures. - Use
webServerto bootpracht devorpracht previewso CI works out-of-the-box. - Console-error capture is mandatory — silent JS errors are the most common pracht hydration regression.
- Wait for
window.__PRACHT_ROUTER_READY__before interacting with the page in any spec that relies on client-side navigation.
$ARGUMENTS