| name | storybook-vitest |
| description | 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. |
| compatibility | Vitest 4.1+, MSW 2, msw-storybook-addon 3 (v2 uses the bare "msw-storybook-addon" specifier), Storybook for Svelte (Vite), @storybook/addon-svelte-csf. Ignore JSX/TSX/CSF3-TS story patterns here. |
| user-invocable | false |
Storybook Vitest + Svelte CSF
Stories are .stories.svelte with defineMeta and <Story> from @storybook/addon-svelte-csf. Official reference: Vitest addon, use it for plugin, browser mode, CLI, tags, debugging, CI; ignore non-Svelte story file examples there. Manual setup · Example configuration files · Options
How it works
- Plugin
storybookTest turns stories into Vitest tests via portable stories (Vitest). No Storybook server required for the test run.
- Each
<Story>: smoke render; a play function runs as the interaction test (interaction testing, asserting with expect).
- Console errors fail the test. MSW in
.storybook/preview applies when configured (v2+ if you use MSW).
- 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; comparison).
.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 (<Story name="Default" />) proves the component mounts
without crashing, nothing else. For every story that involves
user-interactive elements (inputs, autocompletes, buttons, forms):
- Interact: click, type, select, submit
- Assert the result: verify the selected value, the changed
text, the callback data, the visual state change
- 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), only tagged stories run. Add other 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).
<script module>
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"],
});
</script>
<Story
name="Default"
play={async ({ canvasElement }) => {
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 <Story> (custom name FAQ).
Gotchas (FAQ)
- CLI / Vitest vs Storybook Interactions panel can disagree: different environments (docs).
- Vitest internal errors: widget + console; Vitest common errors.
- Non-default
public dir: set publicDir (FAQ).
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, copying each specifier verbatim from your own import statements. Measured on Vite 8.1.5: an entry that cannot resolve aborts the run before any test executes (... is not exported under the conditions [...], Test Files no tests), while an entry that resolves but is not what you import passes silently and pre-bundles the wrong module, leaving the real one un-included. The first mistake is loud, the second is not. Common culprits: msw-storybook-addon/csf3 (addon v2: msw-storybook-addon), svelte-tippy, @storybook/addon-svelte-csf, @storybook/addon-docs. (FAQ). 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, 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). 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: not in Storybook’s doc, but common for headless Chromium.
asChild, decorators, and context
Two problems interact here:
-
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.
-
Never combine asChild with a decorator. DecoratorHandler renders <Component {...args} /> 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:
<!-- MyWrapper.svelte — provides context + visual wrap -->
<script>
import { setContext } from "svelte";
import { writable } from "svelte/store";
import CardWrapper from "$lib/storybook-util/CardWrapper.svelte";
setContext("myContext", writable(null));
</script>
<CardWrapper><slot /></CardWrapper>
<!-- stories file — NO decorators -->
<script module>
const { Story } = defineMeta({
title: "MyComponent",
component: MyComponent,
tags: ["autodocs"],
});
</script>
<Story name="Default" asChild>
<MyWrapper>
<MyComponent prop={value} />
</MyWrapper>
</Story>
Related
- Vitest/browser projects:
frontend:vitest skill.
- Svelte tests outside Storybook:
svelte-5:testing-svelte skill.
- MSW handlers, story-scoped mocks, addon v2→v3:
svelte-5:msw 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 for Storybook vs other tests when using Vitest ≥ 4.0 (manual setup).
- 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). 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). Playwright Chromium is required for default browser mode, install browsers if prompted (Playwright browsers).
Manual wiring follows 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). For CI, set the full URL of the published Storybook (including path prefix if hosted under a subpath) so output links work (CI, Testing in CI).
storybookScript behavior: in watch mode, the plugin starts Storybook via this script only if nothing is already available at storybookUrl (API).
Vitest 4 shape (aligned with Storybook doc; add sibling projects for node/browser/etc.):
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));
function resolveViteConfig(env: ConfigEnv): UserConfig {
return typeof vite === "function" ? vite(env) : vite;
}
const testConfig: UserConfig = {
test: {
projects: [
{
extends: true,
plugins: [
storybookTest({
configDir: path.join(dirname, ".storybook"),
storybookScript: "pnpm storybook --no-open",
}),
],
optimizeDeps: {
include: [
"msw-storybook-addon/csf3",
"svelte-tippy",
"@storybook/addon-svelte-csf",
"@storybook/addon-docs",
],
},
test: {
name: "storybook",
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{ browser: "chromium" }],
},
},
},
],
},
};
export default defineConfig((configEnv) =>
mergeConfig(resolveViteConfig(configEnv), testConfig),
);
CLI & scripts
vitest CLI, default watch; use vitest run in CI.
Docs example: vitest --project=storybook (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.
{
"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 can run/debug these tests from the editor.
Plugin options (quick)
| Option | Notes |
|---|
configDir | Storybook config directory (default .storybook) (API) |
tags | { include, exclude, skip }, defaults include: ['test'], exclude: [], skip: []; exclude wins for the same tag in both (API) |
storybookScript | Command to start Storybook; used in watch when storybookUrl is not already up (API) |
storybookUrl | Used for checks + failure links in output (API) |
disableAddonDocs | Default true, MDX mocked unless you need real MDX parsing in tests (API) |