scaffold-tests
Set up Vitest for pracht routes, loaders, and middleware — browser mode with `vitest-browser-preact` or JSDOM with `@testing-library/preact` — wiring `vitest.config.ts`, `LoaderArgs` via `@pracht/test`, and runnable test files. Use for "scaffold tests", "set up Vitest", "add unit
Install
npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/scaffold-tests
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
git clone https://github.com/JoviDeCroock/pracht.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jovidecroock/pracht collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Pracht Scaffold Tests
Generate Vitest tests aligned with pracht's testing recipe (https://pracht.resynapse.dev/docs/recipes/testing). Loaders and API handlers are plain async functions — they test directly with no framework bootstrap. Component tests need a renderer; the user picks the flavor.
Step 1: Pick the rendering strategy
Use AskUserQuestion to choose between:
- Browser mode —
vitestwith@vitest/browserandvitest-browser-preact.- Pros: real browser, real events, fewer hydration false positives, screenshots, works for SPA-mode interaction tests.
- Cons: slower, heavier setup, requires a browser binary on CI.
- JSDOM —
vitestwith@testing-library/preact.- Pros: fast, lightweight, runs anywhere.
- Cons: JSDOM lacks layout, certain DOM APIs; brittle for complex UIs.
If the project already has one configured, default to it and confirm.
Step 2: Install dependencies
Detect the package manager from the lockfile.
Common (both modes):
pnpm add -D vitest @types/node @pracht/test
@pracht/test provides the typed args factories (createLoaderArgs,
createApiArgs, createMiddlewareArgs, createApiMiddlewareArgs), the
runMiddleware() chain runner, submitForm(), and the
readJson()/readRedirect() response readers used in the templates below.
Component-test extras — @preact/preset-vite must be an explicit dev
dependency: the configs in Step 3 import it, and a transitive-only copy
fails under pnpm's strict node_modules.
Browser mode:
pnpm add -D @vitest/browser playwright vitest-browser-preact @preact/preset-vite
JSDOM mode:
pnpm add -D jsdom @testing-library/preact @testing-library/jest-dom @preact/preset-vite
If the project only needs loader/middleware tests (no component rendering), skip the extras entirely and use the plugin-less config in Step 3.
Step 3: Wire vitest.config.ts
Browser mode:
import { defineConfig } from "vitest/config";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
test: {
browser: {
enabled: true,
provider: "playwright",
instances: [{ browser: "chromium" }],
},
},
});
JSDOM mode:
import { defineConfig } from "vitest/config";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
test: {
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
},
});
test/setup.ts (JSDOM only):
import "@testing-library/jest-dom/vitest";
Loader/middleware-only mode (no components) — matches the minimal config
in recipes-testing.md; no preset, no extra deps:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Exclude E2E tests (run those with Playwright)
exclude: ["e2e/**", "node_modules/**"],
},
});
If vitest.config.ts already exists, merge — never clobber.
Step 4: Generate the 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.
This skill covers loaders, middleware, and components. For API handlers
(src/api/**), delegate to /pracht-test-api — it owns handler enumeration and
per-method test generation; don't duplicate a weaker version here.
Use pracht inspect routes --json to find targets. Ask the user which subset
to scaffold, or pass paths via $ARGUMENTS.
Loader test template
import { describe, it, expect } from "vitest";
import { createLoaderArgs } from "@pracht/test";
import { loader } from "./<route-file>";
describe("<route> loader", () => {
it("returns the expected shape", async () => {
const data = await loader(createLoaderArgs({ url: "/<path>" }));
expect(data).toBeDefined();
});
});
Pass params, headers, a partial context, or a JSON-encoding body as
needed; the factory defaults everything else and exposes controller (the
AbortController behind args.signal) for cancellation tests.
Middleware test template
import { describe, it, expect } from "vitest";
import { createMiddlewareArgs, readRedirect, runMiddleware } from "@pracht/test";
import { middleware } from "./<middleware-file>";
describe("<name> middleware", () => {
it("redirects unauthenticated requests", async () => {
const response = await runMiddleware(middleware, createMiddlewareArgs({ url: "/dashboard" }));
expect(readRedirect(response).location).toMatch(/^\/login/);
});
it("calls through when authenticated", async () => {
const response = await runMiddleware(
middleware,
createMiddlewareArgs({ url: "/dashboard", headers: { cookie: "session=valid" } }),
async () => new Response("handler ran"),
);
expect(await response.text()).toBe("handler ran");
});
});
runMiddleware() accepts an array to test a chain in manifest order,
including context mutations flowing downstream and returned-response
short-circuits. A thrown Response resolves by default, matching page/API
outer normalization. For raw capability-chain behavior, pass
undefined, { thrownResponse: "reject" } after the args/final-handler slots;
prefer createCapabilityTestHost() so the test asserts the real typed
internal_error envelope. For middleware attached through defineApp({ api }),
use createApiMiddlewareArgs() so route has the API metadata shape.
Component test template (browser mode)
import { describe, it, expect } from "vitest";
import { render } from "vitest-browser-preact";
import { Component } from "./<route-file>";
describe("<route> component", () => {
it("renders the heading", async () => {
const screen = render(<Component data={{ /* mock loader data */ }} params={{}} />);
await expect.element(screen.getByRole("heading")).toBeVisible();
});
});
Component test template (JSDOM)
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/preact";
import { Component } from "./<route-file>";
describe("<route> component", () => {
it("renders the heading", () => {
render(<Component data={{ /* mock loader data */ }} params={{}} />);
expect(screen.getByRole("heading")).toBeInTheDocument();
});
});
Step 5: Wire package.json
Add (or merge) scripts:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
If pnpm test already exists, do not overwrite.
Step 6: Verify
pnpm test
pracht verify --json
If anything fails on first run, report the failure and the fix. Do not commit broken scaffolding.
Rules
- Ask the rendering-strategy question once per project; persist by
inspecting
vitest.config.tson subsequent runs. - Only test exports that exist — read the route file before generating.
- Use
@pracht/test's factories forBaseRouteArgs/LoaderArgsconstruction instead of hand-building args objects. - For routes with
getStaticPaths, scaffold a separate test that calls it. - Generated tests should pass on first run with a placeholder assertion; the user fills in real expectations.
$ARGUMENTS
Files (pracht)
-
SKILL.md 7.6 KB
--- name: scaffold-tests version: 1.2.1 description: | Set up Vitest for pracht routes, loaders, and middleware — browser mode with `vitest-browser-preact` or JSDOM with `@testing-library/preact` — wiring `vitest.config.ts`, `LoaderArgs` via `@pracht/test`, and runnable test files. Use for "scaffold tests", "set up Vitest", "add unit tests", "test this loader", "test this route". allowed-tools: - Bash - Read - Write - Edit - Grep - Glob - AskUserQuestion --- # Pracht Scaffold Tests Generate Vitest tests aligned with pracht's testing recipe (<https://pracht.resynapse.dev/docs/recipes/testing>). Loaders and API handlers are plain async functions — they test directly with no framework bootstrap. Component tests need a renderer; the user picks the flavor. ## Step 1: Pick the rendering strategy Use `AskUserQuestion` to choose between: 1. **Browser mode** — `vitest` with `@vitest/browser` and `vitest-browser-preact`. - Pros: real browser, real events, fewer hydration false positives, screenshots, works for SPA-mode interaction tests. - Cons: slower, heavier setup, requires a browser binary on CI. 2. **JSDOM** — `vitest` with `@testing-library/preact`. - Pros: fast, lightweight, runs anywhere. - Cons: JSDOM lacks layout, certain DOM APIs; brittle for complex UIs. If the project already has one configured, default to it and confirm. ## Step 2: Install dependencies Detect the package manager from the lockfile. Common (both modes): ```bash pnpm add -D vitest @types/node @pracht/test ``` `@pracht/test` provides the typed args factories (`createLoaderArgs`, `createApiArgs`, `createMiddlewareArgs`, `createApiMiddlewareArgs`), the `runMiddleware()` chain runner, `submitForm()`, and the `readJson()`/`readRedirect()` response readers used in the templates below. Component-test extras — `@preact/preset-vite` must be an explicit dev dependency: the configs in Step 3 import it, and a transitive-only copy fails under pnpm's strict `node_modules`. **Browser mode**: ```bash pnpm add -D @vitest/browser playwright vitest-browser-preact @preact/preset-vite ``` **JSDOM mode**: ```bash pnpm add -D jsdom @testing-library/preact @testing-library/jest-dom @preact/preset-vite ``` If the project only needs loader/middleware tests (no component rendering), skip the extras entirely and use the plugin-less config in Step 3. ## Step 3: Wire `vitest.config.ts` **Browser mode**: ```ts import { defineConfig } from "vitest/config"; import preact from "@preact/preset-vite"; export default defineConfig({ plugins: [preact()], test: { browser: { enabled: true, provider: "playwright", instances: [{ browser: "chromium" }], }, }, }); ``` **JSDOM mode**: ```ts import { defineConfig } from "vitest/config"; import preact from "@preact/preset-vite"; export default defineConfig({ plugins: [preact()], test: { environment: "jsdom", setupFiles: ["./test/setup.ts"], }, }); ``` `test/setup.ts` (JSDOM only): ```ts import "@testing-library/jest-dom/vitest"; ``` **Loader/middleware-only mode** (no components) — matches the minimal config in `recipes-testing.md`; no preset, no extra deps: ```ts import { defineConfig } from "vitest/config"; export default defineConfig({ test: { // Exclude E2E tests (run those with Playwright) exclude: ["e2e/**", "node_modules/**"], }, }); ``` If `vitest.config.ts` already exists, merge — never clobber. ## Step 4: Generate the 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. This skill covers **loaders, middleware, and components**. For API handlers (`src/api/**`), delegate to `/pracht-test-api` — it owns handler enumeration and per-method test generation; don't duplicate a weaker version here. Use `pracht inspect routes --json` to find targets. Ask the user which subset to scaffold, or pass paths via `$ARGUMENTS`. ### Loader test template ```ts import { describe, it, expect } from "vitest"; import { createLoaderArgs } from "@pracht/test"; import { loader } from "./<route-file>"; describe("<route> loader", () => { it("returns the expected shape", async () => { const data = await loader(createLoaderArgs({ url: "/<path>" })); expect(data).toBeDefined(); }); }); ``` Pass `params`, `headers`, a partial `context`, or a JSON-encoding `body` as needed; the factory defaults everything else and exposes `controller` (the `AbortController` behind `args.signal`) for cancellation tests. ### Middleware test template ```ts import { describe, it, expect } from "vitest"; import { createMiddlewareArgs, readRedirect, runMiddleware } from "@pracht/test"; import { middleware } from "./<middleware-file>"; describe("<name> middleware", () => { it("redirects unauthenticated requests", async () => { const response = await runMiddleware(middleware, createMiddlewareArgs({ url: "/dashboard" })); expect(readRedirect(response).location).toMatch(/^\/login/); }); it("calls through when authenticated", async () => { const response = await runMiddleware( middleware, createMiddlewareArgs({ url: "/dashboard", headers: { cookie: "session=valid" } }), async () => new Response("handler ran"), ); expect(await response.text()).toBe("handler ran"); }); }); ``` `runMiddleware()` accepts an array to test a chain in manifest order, including `context` mutations flowing downstream and returned-response short-circuits. A thrown `Response` resolves by default, matching page/API outer normalization. For raw capability-chain behavior, pass `undefined, { thrownResponse: "reject" }` after the args/final-handler slots; prefer `createCapabilityTestHost()` so the test asserts the real typed `internal_error` envelope. For middleware attached through `defineApp({ api })`, use `createApiMiddlewareArgs()` so `route` has the API metadata shape. ### Component test template (browser mode) ```tsx import { describe, it, expect } from "vitest"; import { render } from "vitest-browser-preact"; import { Component } from "./<route-file>"; describe("<route> component", () => { it("renders the heading", async () => { const screen = render(<Component data={{ /* mock loader data */ }} params={{}} />); await expect.element(screen.getByRole("heading")).toBeVisible(); }); }); ``` ### Component test template (JSDOM) ```tsx import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/preact"; import { Component } from "./<route-file>"; describe("<route> component", () => { it("renders the heading", () => { render(<Component data={{ /* mock loader data */ }} params={{}} />); expect(screen.getByRole("heading")).toBeInTheDocument(); }); }); ``` ## Step 5: Wire `package.json` Add (or merge) scripts: ```json { "scripts": { "test": "vitest run", "test:watch": "vitest" } } ``` If `pnpm test` already exists, do not overwrite. ## Step 6: Verify ```bash pnpm test pracht verify --json ``` If anything fails on first run, report the failure and the fix. Do not commit broken scaffolding. ## Rules 1. Ask the rendering-strategy question once per project; persist by inspecting `vitest.config.ts` on subsequent runs. 2. Only test exports that exist — read the route file before generating. 3. Use `@pracht/test`'s factories for `BaseRouteArgs`/`LoaderArgs` construction instead of hand-building args objects. 4. For routes with `getStaticPaths`, scaffold a separate test that calls it. 5. Generated tests should pass on first run with a placeholder assertion; the user fills in real expectations. $ARGUMENTS
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.