From svelte-5
Wires Svelte CSF Storybook stories (.stories.svelte) as Vitest browser tests via addon-svelte-csf and addon-vitest. Configures vitest.config with test.projects, play on <Story>, tags, storybookUrl, MSW preview for wiring/debugging in Vitest/CI.
npx claudepluginhub fubits1/svelte-skills --plugin svelte-5This skill uses the workspace's default tool permissions.
Stories are **`.stories.svelte`** with **`defineMeta`** and **`<Story>`** 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...
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
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
storybookTest turns stories into Vitest tests via portable stories (Vitest). No Storybook server required for the test run.<Story>: smoke render; a play function runs as the interaction test (interaction testing, asserting with expect)..storybook/preview applies when configured (v2+ if you use MSW).vitest --coverage --project … only covers projects you pass (many pipelines omit storybook on purpose)..stories.svelte + playUse storybook/test (expect, within, userEvent, waitFor) in play. Scope queries with canvasElement → 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):
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 "vitest/browser";
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).
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. Common culprits: 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 I have not personally validated with the protocol.frontend:validate flake rules and frontend:vitest flake-hygiene section.test.isolate: false and/or --shard=i/n (FAQ, sharding).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.browser.provider: playwright({ launchOptions: { args: … } }) per Vitest browser — not in Storybook’s doc, but common for headless Chromium.asChild, decorators, and contextTwo 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>
frontend:vitest skill.svelte-5:testing-svelte skill.vitest.config.{ts,mts,js,mjs} at the package root (per monorepo package). Put storybookTest and test.projects here.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.vitest.config.* and the only Vitest config is vite.config → test: (or there is no Vitest file), ask before adding the addon whether to introduce vitest.config.ts and move test out of Vite.@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: the plugin auto-injects its own setup files (@storybook/addon-vitest/internal/setup-file and setup-file-with-project-annotations). You do not need a manual .storybook/vitest.setup.ts unless you have custom per-test setup beyond what preview.ts provides. The doc example shows setupFiles but the plugin handles this internally.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));
// 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",
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),
);
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": "bash scripts/test-story.sh"
}
}
--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.
| 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) |