# Storybook Vitest

> Svelte CSF (@storybook/addon-svelte-csf) + @storybook/addon-vitest: .stories.svelte as Vitest browser tests with play functions and tags. Use when wiring or debugging Svelte Storybook tests in Vitest/CI, not for React/Vue/CSF3 .ts story files.

- **Type:** Skill
- **Install:** `agentstack add skill-fubits1-svelte-skills-storybook-vitest`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [fubits1](https://agentstack.voostack.com/s/fubits1)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [fubits1](https://github.com/fubits1)
- **Source:** https://github.com/fubits1/svelte-skills/tree/main/plugins/svelte-5/skills/storybook-vitest

## Install

```sh
agentstack add skill-fubits1-svelte-skills-storybook-vitest
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Storybook Vitest + Svelte CSF

Stories are **`.stories.svelte`** with **`defineMeta`** and **``** from `@storybook/addon-svelte-csf`. Official reference: [Vitest addon](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon), use it for **plugin, browser mode, CLI, tags, debugging, CI**; ignore non-Svelte story file examples there. [Manual setup](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#manual-setup-advanced) · [Example configuration files](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#example-configuration-files) · [Options](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#options)

## How it works

- Plugin **`storybookTest`** turns stories into Vitest tests via [portable stories (Vitest)](https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest). **No Storybook server** required for the test run.
- Each ``: smoke render; a **`play`** function runs as the interaction test ([interaction testing](https://storybook.js.org/docs/writing-tests/interaction-testing), [asserting with expect](https://storybook.js.org/docs/writing-tests/interaction-testing#asserting-with-expect)).
- Console errors fail the test. MSW in **`.storybook/preview`** applies when configured (v2+ if you use MSW).
- [Test coverage](https://storybook.js.org/docs/writing-tests/test-coverage): see Storybook’s coverage doc; **`vitest --coverage --project …`** only covers projects you pass (many pipelines omit `storybook` on purpose).
- With the addon, **snapshot tests** are **not** supported (unlike the [test runner](https://storybook.js.org/docs/writing-tests/integrations/test-runner); [comparison](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#comparison-to-the-test-runner)).

## `.stories.svelte` + `play`

Use **`storybook/test`** (`expect`, `within`, `userEvent`, `waitFor`) in `play`. Scope queries with **`canvasElement`** to `within(canvasElement)`.

**EVERY story MUST have meaningful interaction tests.** A smoke
render (``) proves the component mounts
without crashing, nothing else. For every story that involves
user-interactive elements (inputs, autocompletes, buttons, forms):

1. **Interact**: click, type, select, submit
2. **Assert the result**: verify the selected value, the changed
   text, the callback data, the visual state change
3. **Never silently skip**: no `if (items.length > 1)` guards.
   If the dropdown should have options, ASSERT it does.

A play function that opens a dropdown but never selects a value
is not a test. A play function that selects a value but never
checks WHICH value was selected is not a test. Example: stories
that "tested" autocomplete interaction by opening
a dropdown and checking "something was selected", this proved
nothing and missed a real bug (onChange returning objects instead
of strings).

**Tags:** default `storybookTest({ tags: { include: ['test'], … } })` ([API](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#tags)), only tagged stories run. Add other [tags](https://storybook.js.org/docs/writing-stories/tags) to **`include`** when needed (e.g. **`autodocs`** if those stories should run under Vitest). Set tags on **`defineMeta`** / stories or adjust `include` / `exclude` / `skip` (exclude wins if the same tag is both included and excluded).

```svelte

  import { defineMeta } from "@storybook/addon-svelte-csf";
  import { expect, within, userEvent } from "storybook/test";
  import MyComponent from "./MyComponent.svelte";

  const { Story } = defineMeta({
    title: "MyComponent",
    component: MyComponent,
    tags: ["test"],
  });

 {
    const canvas = within(canvasElement);
    const button = canvas.getByRole("button");
    await userEvent.click(button);
    await expect(canvas.getByText("Clicked")).toBeVisible();
  }}
/>
```

**Test name in output:** use **`name`** on `` ([custom name FAQ](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#how-do-i-customize-a-test-name)).

## Gotchas (FAQ)

- **CLI / Vitest vs Storybook Interactions panel** can disagree: different environments ([docs](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#what-happens-when-there-are-different-test-results-in-multiple-environments)).
- **Vitest internal errors:** widget + console; [Vitest common errors](https://vitest.dev/guide/common-errors.html).
- **Non-default `public` dir:** set [`publicDir`](https://vitejs.dev/config/shared-options.html#publicdir) ([FAQ](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#how-do-i-ensure-my-tests-can-find-assets-in-the-public-directory)).
- **`Vitest failed to find the current suite`:** caused by `optimizeDeps` reload mid-test (look for `✨ new dependencies optimized:` in output). Fix: add the newly-discovered deps to **`optimizeDeps.include`** on the **storybook project config** itself. Common culprits: `msw-storybook-addon`, `svelte-tippy`, `@storybook/addon-svelte-csf`, `@storybook/addon-docs`. ([FAQ](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#how-do-i-fix-the-error-vitest-failed-to-find-the-current-suite-error)). **Any fix for this error is UNVERIFIED until proven by the flake-hygiene protocol below.** A single green run tells you nothing: this suite produces different counts between invocations on the same code. Do not recommend a fix until the flake-hygiene protocol has validated it.
- **Single-run bias:** this addon is PARTICULARLY prone to producing inconsistent counts between invocations on the same checkout. A "Test Files N passed (N)" line on one run does NOT prove the suite is healthy. Never claim "fixed" or "green" based on a single run on this suite. If the user shows a failing screenshot and your run goes green, the FIRST move is to acknowledge you cannot reproduce their failure and ask for their log or reproduction conditions, not to re-run hoping for another green. See `frontend:validate` flake rules and `frontend:vitest` flake-hygiene section.
- **Do not enshrine unverified approaches as "better fixes" in this skill.** If you encounter or propose a new approach to a storybook/vitest problem, it must go through the full flake-hygiene protocol on a real failure before being written down as a recommendation. Declaring an approach "worked" from a single background run while the user reproduces 31 failures on the same code is not verification. The approach may or may not be correct, but unverified does not go in the skill.
- **CI:** dynamic import / iframe: `test.isolate: false` and/or `--shard=i/n` ([FAQ](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#why-do-my-tests-fail-in-ci-with-failed-to-fetch-dynamically-imported-module-or-cannot-connect-to-the-iframe), [sharding](https://vitest.dev/guide/improving-performance.html#sharding)).
- **Isolation:** if **`vite.config` defines `test`**, it **merges** into configs that extend that Vite file and can break Storybook tests: **move `test` to `vitest.config`** ([FAQ](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#how-do-i-isolate-storybook-tests-from-others)). **`mergeConfig(viteConfig, defineConfig({ test: … }))` in `vitest.config.ts` is the documented Vitest 4 pattern** as long as **Vite does not own `test`**.
- **Playwright (WebGL / maps / Canvas):** optional `browser.provider: playwright({ launchOptions: { args: … } })` per [Vitest browser](https://vitest.dev/config/#browser-playwright): not in Storybook’s doc, but common for headless Chromium.

## `asChild`, decorators, and context

Two problems interact here:

1. **Decorator `setContext` doesn’t propagate** to the story component in vitest headless mode (works in browser UI). Components needing Svelte context must get it from an `asChild` wrapper.

2. **Never combine `asChild` with a decorator.** `DecoratorHandler` renders `` inside the decorator: this instantiates the component WITHOUT the props passed manually in the `asChild` content. The component renders twice (once broken via decorator, once correct via `asChild`), and the broken render crashes with e.g. `Cannot read properties of undefined`.

**Fix:** put both context and visual wrapping (CardWrapper etc.) inside the `asChild` wrapper component, never as a decorator:

```svelte

  import { setContext } from "svelte";
  import { writable } from "svelte/store";
  import CardWrapper from "$lib/storybook-util/CardWrapper.svelte";
  setContext("myContext", writable(null));

```

```svelte

  const { Story } = defineMeta({
    title: "MyComponent",
    component: MyComponent,
    tags: ["autodocs"],
  });

  
    
  

```

## Related

- Vitest/browser projects: `frontend:vitest` skill.
- Svelte tests outside Storybook: `svelte-5:testing-svelte` skill.
- Decorator rendering internals + `props_invalid_value` traps: `references/storybook-decorator.md`.

## Vitest config file

- Use a **dedicated `vitest.config.{ts,mts,js,mjs}`** at the package root (per monorepo package). Put **`storybookTest`** and **`test.projects`** here.
- Storybook docs recommend a **separate [test project](https://vitest.dev/guide/projects)** for Storybook vs other tests when using **Vitest ≥ 4.0** ([manual setup](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#manual-setup-advanced)).
- **Do not define `test` in `vite.config`** if that config is extended/merged for Vitest: the FAQ’s merge problem is **Vite’s `test` field**, not `mergeConfig` itself.
- **`extends: true`** on a project inherits the **merged** root Vitest config (matches [official example](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#example-configuration-files)). Alternative: root `defineConfig({ test: { projects: [{ extends: './vite.config.ts', … }] } })` with **no** `test` in Vite: same isolation goal.
- **Gate:** if there is **no** `vitest.config.*` and the only Vitest config is **`vite.config` to `test:`** (or there is no Vitest file), **ask** before adding the addon whether to introduce `vitest.config.ts` and move `test` out of Vite.

## Setup (`@storybook/addon-vitest`)

Prefer **`pnpm exec storybook add @storybook/addon-vitest`** ([automatic installation](https://storybook.js.org/docs/addons/install-addons#automatic-installation)). **Playwright Chromium** is required for default browser mode, install browsers if prompted ([Playwright browsers](https://playwright.dev/docs/browsers#install-browsers)).

**Manual** wiring follows [example configuration files](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#example-configuration-files). Vitest **4** example uses **`mergeConfig(viteConfig, defineConfig({ test: { projects: […] } }))`** and a Storybook project with **`extends: true`**.

- **`setupFiles`:** since **Storybook 10.3** the plugin applies preview annotations automatically — it always injects `@storybook/addon-vitest/internal/setup-file`, and injects `setup-file-with-project-annotations` **only when you have not supplied a `setProjectAnnotations` setup file**. So a manual `.storybook/vitest.setup.ts` is **not required** unless you have custom per-test setup beyond `preview.ts`; if you keep one that calls `setProjectAnnotations`, the plugin defers to it. Before 10.3 that file, referenced via `setupFiles`, was required — which is why the doc example still shows it.
- **`storybookScript`:** docs: _“This should match your **`package.json`** script to run Storybook”_ (e.g. **`pnpm storybook --no-open`**). You may **prefix** the same command with setup steps (i18n mocks, env) so watch-mode debugging matches dev.
- **`storybookUrl`:** default **`http://localhost:6006`**: must be **reachable** for failure links ([debugging](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#debugging)). For **CI**, set the **full URL** of the **published** Storybook (including **path prefix** if hosted under a subpath) so output links work ([CI](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#in-ci), [Testing in CI](https://storybook.js.org/docs/writing-tests/in-ci#21-debugging-test-failures-in-ci)).
- **`storybookScript` behavior:** in **watch** mode, the plugin starts Storybook via this script **only if** nothing is already available at **`storybookUrl`** ([API](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#storybookscript)).

**Vitest 4 shape (aligned with Storybook doc; add sibling `projects` for node/browser/etc.):**

```typescript
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig, mergeConfig } from "vitest/config";
import type { ConfigEnv, UserConfig } from "vite";
import { playwright } from "@vitest/browser-playwright";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import vite from "./vite.config";

const dirname = path.dirname(fileURLToPath(import.meta.url));

// If vite.config exports a function, resolve it:
function resolveViteConfig(env: ConfigEnv): UserConfig {
  return typeof vite === "function" ? vite(env) : vite;
}

const testConfig: UserConfig = {
  test: {
    projects: [
      // … other projects (node, browser) …
      {
        extends: true,
        plugins: [
          storybookTest({
            configDir: path.join(dirname, ".storybook"),
            storybookScript: "pnpm storybook --no-open",
          }),
        ],
        // Pre-bundle deps that trigger optimizeDeps reload mid-test
        // (causes "Vitest failed to find the current suite" error)
        optimizeDeps: {
          include: [
            "msw-storybook-addon",
            "svelte-tippy",
            "@storybook/addon-svelte-csf",
            "@storybook/addon-docs",
          ],
        },
        test: {
          name: "storybook",
          // no setupFiles: since Storybook 10.3 the plugin auto-applies preview annotations (see setupFiles note above)
          browser: {
            enabled: true,
            headless: true,
            provider: playwright({}),
            instances: [{ browser: "chromium" }],
          },
        },
      },
    ],
  },
};

// Function export handles vite.config that exports defineConfig(({ mode }) => …)
export default defineConfig((configEnv) =>
  mergeConfig(resolveViteConfig(configEnv), testConfig),
);
```

## CLI & scripts

[`vitest` CLI](https://vitest.dev/guide/cli.html), default **watch**; use **`vitest run`** in CI.

Docs example: **`vitest --project=storybook`** ([CLI](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#cli)). Many repos run **`--project storybook`** only from a **dedicated script** and keep **`test`** / **`validate`** on **node + browser**, that is a **pipeline choice**, not required by the addon.

```json
{
  "scripts": {
    "test": "vitest run --project node --project browser",
    "test-storybook": "vitest run --project storybook",
    "test:story": "node --experimental-strip-types scripts/bin/test-story.ts"
  }
}
```

**`--silent`:** MSW logs flood storybook test output. ALWAYS use
`--silent` when running storybook tests, or use `pnpm test:story`
which includes it. Without `--silent`, grep for errors is
impossible, the output is 90% MSW request/response bodies.

**Debugging failures:** When a storybook test fails, ALWAYS open
the story in the **Storybook browser UI** via Playwright FIRST.
The browser console shows the actual Svelte error with component
stack trace. vitest only shows "test failed" without details.
Decorators (CardWrapper etc.) swallow errors, the real error is
only visible in the browser console.

**Common Svelte 5 error:** `props_invalid_value`, happens when
`bind:prop={undefined}` targets a prop with `$bindable(fallback)`.
Fix: add explicit `null` default: `prop = $bindable(null)`.

[Vitest IDE](https://vitest.dev/guide/ide.html) can run/debug these tests from the editor.

## Plugin options (quick)

| Option             | Notes

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [fubits1](https://github.com/fubits1)
- **Source:** [fubits1/svelte-skills](https://github.com/fubits1/svelte-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-fubits1-svelte-skills-storybook-vitest
- Seller: https://agentstack.voostack.com/s/fubits1
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
