{"slug":"scaffold-tests","title":"scaffold-tests","summary":"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","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:26.834863Z","repo":{"url":"https://github.com/JoviDeCroock/pracht","stars":99,"forks":5,"license":"MIT","updatedAt":"2026-10-19T16:43:31Z"},"bodyHtml":"<hr>\n<p>name: scaffold-tests\nversion: 1.2.1\ndescription: |\nSet up Vitest for pracht routes, loaders, and middleware — browser mode with\n<code>vitest-browser-preact</code> or JSDOM with <code>@testing-library/preact</code> — wiring\n<code>vitest.config.ts</code>, <code>LoaderArgs</code> via <code>@pracht/test</code>, and runnable test files.\nUse for \"scaffold tests\", \"set up Vitest\", \"add unit tests\", \"test this loader\",\n\"test this route\".\nallowed-tools:</p>\n<ul>\n<li>Bash</li>\n<li>Read</li>\n<li>Write</li>\n<li>Edit</li>\n<li>Grep</li>\n<li>Glob</li>\n<li>AskUserQuestion</li>\n</ul>\n<hr>\n<h1>Pracht Scaffold Tests</h1>\n<p>Generate Vitest tests aligned with pracht's testing recipe\n(<a href=\"https://pracht.resynapse.dev/docs/recipes/testing\">https://pracht.resynapse.dev/docs/recipes/testing</a>). Loaders and API handlers\nare plain async functions — they test directly with no framework bootstrap.\nComponent tests need a renderer; the user picks the flavor.</p>\n<h2>Step 1: Pick the rendering strategy</h2>\n<p>Use <code>AskUserQuestion</code> to choose between:</p>\n<ol>\n<li><strong>Browser mode</strong> — <code>vitest</code> with <code>@vitest/browser</code> and\n<code>vitest-browser-preact</code>.\n<ul>\n<li>Pros: real browser, real events, fewer hydration false positives,\nscreenshots, works for SPA-mode interaction tests.</li>\n<li>Cons: slower, heavier setup, requires a browser binary on CI.</li>\n</ul>\n</li>\n<li><strong>JSDOM</strong> — <code>vitest</code> with <code>@testing-library/preact</code>.\n<ul>\n<li>Pros: fast, lightweight, runs anywhere.</li>\n<li>Cons: JSDOM lacks layout, certain DOM APIs; brittle for complex UIs.</li>\n</ul>\n</li>\n</ol>\n<p>If the project already has one configured, default to it and confirm.</p>\n<h2>Step 2: Install dependencies</h2>\n<p>Detect the package manager from the lockfile.</p>\n<p>Common (both modes):</p>\n<pre><code>pnpm add -D vitest @types/node @pracht/test\n</code></pre>\n<p><code>@pracht/test</code> provides the typed args factories (<code>createLoaderArgs</code>,\n<code>createApiArgs</code>, <code>createMiddlewareArgs</code>, <code>createApiMiddlewareArgs</code>), the\n<code>runMiddleware()</code> chain runner, <code>submitForm()</code>, and the\n<code>readJson()</code>/<code>readRedirect()</code> response readers used in the templates below.</p>\n<p>Component-test extras — <code>@preact/preset-vite</code> must be an explicit dev\ndependency: the configs in Step 3 import it, and a transitive-only copy\nfails under pnpm's strict <code>node_modules</code>.</p>\n<p><strong>Browser mode</strong>:</p>\n<pre><code>pnpm add -D @vitest/browser playwright vitest-browser-preact @preact/preset-vite\n</code></pre>\n<p><strong>JSDOM mode</strong>:</p>\n<pre><code>pnpm add -D jsdom @testing-library/preact @testing-library/jest-dom @preact/preset-vite\n</code></pre>\n<p>If the project only needs loader/middleware tests (no component rendering),\nskip the extras entirely and use the plugin-less config in Step 3.</p>\n<h2>Step 3: Wire <code>vitest.config.ts</code></h2>\n<p><strong>Browser mode</strong>:</p>\n<pre><code>import { defineConfig } from \"vitest/config\";\nimport preact from \"@preact/preset-vite\";\n\nexport default defineConfig({\n  plugins: [preact()],\n  test: {\n    browser: {\n      enabled: true,\n      provider: \"playwright\",\n      instances: [{ browser: \"chromium\" }],\n    },\n  },\n});\n</code></pre>\n<p><strong>JSDOM mode</strong>:</p>\n<pre><code>import { defineConfig } from \"vitest/config\";\nimport preact from \"@preact/preset-vite\";\n\nexport default defineConfig({\n  plugins: [preact()],\n  test: {\n    environment: \"jsdom\",\n    setupFiles: [\"./test/setup.ts\"],\n  },\n});\n</code></pre>\n<p><code>test/setup.ts</code> (JSDOM only):</p>\n<pre><code>import \"@testing-library/jest-dom/vitest\";\n</code></pre>\n<p><strong>Loader/middleware-only mode</strong> (no components) — matches the minimal config\nin <code>recipes-testing.md</code>; no preset, no extra deps:</p>\n<pre><code>import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    // Exclude E2E tests (run those with Playwright)\n    exclude: [\"e2e/**\", \"node_modules/**\"],\n  },\n});\n</code></pre>\n<p>If <code>vitest.config.ts</code> already exists, merge — never clobber.</p>\n<h2>Step 4: Generate the tests</h2>\n<p>MCP: when the pracht MCP server is registered (<code>pracht dev-mcp</code>), prefer its\n<code>inspect_routes</code>/<code>inspect_api</code>/<code>inspect_build</code>/<code>doctor</code>/<code>verify</code>/<code>generate_*</code>\ntools over shelling out. <code>pracht inspect</code> needs the pracht plugin in the vite\nconfig.</p>\n<p>This skill covers <strong>loaders, middleware, and components</strong>. For API handlers\n(<code>src/api/**</code>), delegate to <code>/pracht-test-api</code> — it owns handler enumeration and\nper-method test generation; don't duplicate a weaker version here.</p>\n<p>Use <code>pracht inspect routes --json</code> to find targets. Ask the user which subset\nto scaffold, or pass paths via <code>$ARGUMENTS</code>.</p>\n<h3>Loader test template</h3>\n<pre><code>import { describe, it, expect } from \"vitest\";\nimport { createLoaderArgs } from \"@pracht/test\";\nimport { loader } from \"./&lt;route-file&gt;\";\n\ndescribe(\"&lt;route&gt; loader\", () =&gt; {\n  it(\"returns the expected shape\", async () =&gt; {\n    const data = await loader(createLoaderArgs({ url: \"/&lt;path&gt;\" }));\n    expect(data).toBeDefined();\n  });\n});\n</code></pre>\n<p>Pass <code>params</code>, <code>headers</code>, a partial <code>context</code>, or a JSON-encoding <code>body</code> as\nneeded; the factory defaults everything else and exposes <code>controller</code> (the\n<code>AbortController</code> behind <code>args.signal</code>) for cancellation tests.</p>\n<h3>Middleware test template</h3>\n<pre><code>import { describe, it, expect } from \"vitest\";\nimport { createMiddlewareArgs, readRedirect, runMiddleware } from \"@pracht/test\";\nimport { middleware } from \"./&lt;middleware-file&gt;\";\n\ndescribe(\"&lt;name&gt; middleware\", () =&gt; {\n  it(\"redirects unauthenticated requests\", async () =&gt; {\n    const response = await runMiddleware(middleware, createMiddlewareArgs({ url: \"/dashboard\" }));\n    expect(readRedirect(response).location).toMatch(/^\\/login/);\n  });\n\n  it(\"calls through when authenticated\", async () =&gt; {\n    const response = await runMiddleware(\n      middleware,\n      createMiddlewareArgs({ url: \"/dashboard\", headers: { cookie: \"session=valid\" } }),\n      async () =&gt; new Response(\"handler ran\"),\n    );\n    expect(await response.text()).toBe(\"handler ran\");\n  });\n});\n</code></pre>\n<p><code>runMiddleware()</code> accepts an array to test a chain in manifest order,\nincluding <code>context</code> mutations flowing downstream and returned-response\nshort-circuits. A thrown <code>Response</code> resolves by default, matching page/API\nouter normalization. For raw capability-chain behavior, pass\n<code>undefined, { thrownResponse: \"reject\" }</code> after the args/final-handler slots;\nprefer <code>createCapabilityTestHost()</code> so the test asserts the real typed\n<code>internal_error</code> envelope. For middleware attached through <code>defineApp({ api })</code>,\nuse <code>createApiMiddlewareArgs()</code> so <code>route</code> has the API metadata shape.</p>\n<h3>Component test template (browser mode)</h3>\n<pre><code>import { describe, it, expect } from \"vitest\";\nimport { render } from \"vitest-browser-preact\";\nimport { Component } from \"./&lt;route-file&gt;\";\n\ndescribe(\"&lt;route&gt; component\", () =&gt; {\n  it(\"renders the heading\", async () =&gt; {\n    const screen = render(&lt;Component data={{ /* mock loader data */ }} params={{}} /&gt;);\n    await expect.element(screen.getByRole(\"heading\")).toBeVisible();\n  });\n});\n</code></pre>\n<h3>Component test template (JSDOM)</h3>\n<pre><code>import { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/preact\";\nimport { Component } from \"./&lt;route-file&gt;\";\n\ndescribe(\"&lt;route&gt; component\", () =&gt; {\n  it(\"renders the heading\", () =&gt; {\n    render(&lt;Component data={{ /* mock loader data */ }} params={{}} /&gt;);\n    expect(screen.getByRole(\"heading\")).toBeInTheDocument();\n  });\n});\n</code></pre>\n<h2>Step 5: Wire <code>package.json</code></h2>\n<p>Add (or merge) scripts:</p>\n<pre><code>{\n  \"scripts\": {\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\"\n  }\n}\n</code></pre>\n<p>If <code>pnpm test</code> already exists, do not overwrite.</p>\n<h2>Step 6: Verify</h2>\n<pre><code>pnpm test\npracht verify --json\n</code></pre>\n<p>If anything fails on first run, report the failure and the fix. Do not commit\nbroken scaffolding.</p>\n<h2>Rules</h2>\n<ol>\n<li>Ask the rendering-strategy question once per project; persist by\ninspecting <code>vitest.config.ts</code> on subsequent runs.</li>\n<li>Only test exports that exist — read the route file before generating.</li>\n<li>Use <code>@pracht/test</code>'s factories for <code>BaseRouteArgs</code>/<code>LoaderArgs</code>\nconstruction instead of hand-building args objects.</li>\n<li>For routes with <code>getStaticPaths</code>, scaffold a separate test that calls it.</li>\n<li>Generated tests should pass on first run with a placeholder assertion;\nthe user fills in real expectations.</li>\n</ol>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":7833,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:18:14.111178Z","sha256":"E2474772C824558C5A1A2C601369B5DE1DF2E1DDEA9ADC112DDD6C265964618B","sizeBytes":3233},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/scaffold-tests","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"9F38B6DB0FF5CF813E1D03BD3E4939694198E215634E9D1BE17B072F5246795D","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:20:00.547821Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/scaffold-tests"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart"},{"target":"git","command":"git clone https://github.com/JoviDeCroock/pracht.git"}]}