fix(web): align vitest setup with storybook docs

Quentin Gliech and Claude Opus 4.6 (1M context) created

- Restore setupFiles in storybook project (addon detects it, skips
  auto-provisioning, but it's still needed per docs)
- Add storybookScript for watch mode DX (story links in failures)
- Switch snapshot tests from render() to run() which goes through
  the full Storybook lifecycle (loaders, decorators, play functions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/.storybook/vitest.setup.ts                           |  5 
webui2/src/components/ui/__snapshots__/button.test.tsx.snap | 96 ++++--
webui2/src/components/ui/button.test.tsx                    |  7 
webui2/vitest.config.ts                                     |  2 
4 files changed, 63 insertions(+), 47 deletions(-)

Detailed changes

webui2/.storybook/vitest.setup.ts 🔗

@@ -3,9 +3,8 @@ import { beforeAll } from "vitest";
 
 import * as previewAnnotations from "./preview";
 
-// Apply Storybook decorators/parameters from preview.ts to portable stories.
-// Note: the @storybook/addon-vitest project handles this automatically;
-// this setup file is only used by the snapshot test project.
+// Apply Storybook decorators/parameters from preview.ts to portable stories
+// used by the snapshot test project.
 const annotations = setProjectAnnotations([previewAnnotations]);
 
 beforeAll(annotations.beforeAll);

webui2/src/components/ui/__snapshots__/button.test.tsx.snap 🔗

@@ -1,65 +1,81 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`Button/Default matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-9 px-4 py-2"
->
-  Button
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-9 px-4 py-2"
+  >
+    Button
+  </button>
+</div>
 `;
 
 exports[`Button/Destructive matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 h-9 px-4 py-2"
->
-  Delete
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 h-9 px-4 py-2"
+  >
+    Delete
+  </button>
+</div>
 `;
 
 exports[`Button/Ghost matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
->
-  Ghost
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
+  >
+    Ghost
+  </button>
+</div>
 `;
 
 exports[`Button/Large matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-10 rounded-md px-8"
->
-  Large
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-10 rounded-md px-8"
+  >
+    Large
+  </button>
+</div>
 `;
 
 exports[`Button/Link matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-primary underline-offset-4 hover:underline h-9 px-4 py-2"
->
-  Link
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-primary underline-offset-4 hover:underline h-9 px-4 py-2"
+  >
+    Link
+  </button>
+</div>
 `;
 
 exports[`Button/Outline matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
->
-  Outline
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
+  >
+    Outline
+  </button>
+</div>
 `;
 
 exports[`Button/Secondary matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 h-9 px-4 py-2"
->
-  Secondary
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 h-9 px-4 py-2"
+  >
+    Secondary
+  </button>
+</div>
 `;
 
 exports[`Button/Small matches snapshot 1`] = `
-<button
-  class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
->
-  Small
-</button>
+<div>
+  <button
+    class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
+  >
+    Small
+  </button>
+</div>
 `;

webui2/src/components/ui/button.test.tsx 🔗

@@ -1,5 +1,4 @@
 import { composeStories } from "@storybook/react-vite";
-import { render } from "@testing-library/react";
 import { expect, test } from "vitest";
 
 import * as stories from "./button.stories";
@@ -7,8 +6,8 @@ import * as stories from "./button.stories";
 const composed = composeStories(stories);
 
 for (const [name, Story] of Object.entries(composed)) {
-  test(`Button/${name} matches snapshot`, () => {
-    const { container } = render(<Story />);
-    expect(container.firstChild).toMatchSnapshot();
+  test(`Button/${name} matches snapshot`, async () => {
+    await Story.run();
+    expect(document.body.firstChild).toMatchSnapshot();
   });
 }

webui2/vitest.config.ts 🔗

@@ -19,6 +19,7 @@ export default mergeConfig(
           plugins: [
             storybookTest({
               configDir: path.join(dirname, ".storybook"),
+              storybookScript: "pnpm storybook --no-open",
             }),
           ],
           test: {
@@ -29,6 +30,7 @@ export default mergeConfig(
               headless: true,
               instances: [{ browser: "chromium" }],
             },
+            setupFiles: ["./.storybook/vitest.setup.ts"],
           },
         },
         // Snapshot tests (happy-dom, fast)