Skip to content

Interaction testing

View as Markdown

StoryShelf can run your Storybook play functions before each screenshot. When enabled, a failing play blocks the build (failed) so you can drop a separate storybook test / vitest CI job. Flaky stories can be marked non-blocking.

Interaction tests are off by default; with them off, a screenshot still waits for Storybook to report the story rendered (see below).

  1. Open your project → Settings → Tests
  2. Check Enable interaction tests (play)
  3. Set Play timeout (ms) (1000–30000, default 10000)
  4. Save

Only that project’s builds will execute play. Global viewports (ShelfConfig.viewports) stay the default; per-story viewports remain deferred.

play is standard Storybook CSF:

src/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";
import { Button } from "./Button";
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: "primary" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button"));
await expect(canvas.getByText("Clicked")).toBeVisible();
},
};

The runner does page.goto(iframe.html?id=...&viewMode=story) → waitForSelector(#storybook-root, attached) → wait for Storybook’s render-finished event → if enabled page.evaluate(executePlay) with playTimeoutMs → screenshot({animations: disabled}). No code change in .storybook/main.ts is required.

The runner subscribes to the preview’s channel before the story renders and waits for Storybook’s own signal (storyRendered, or the error event of a story that fails to render) instead of a fixed sleep. After the signal it lets web fonts load and two frames paint (at most 250 ms), then waits delay if you set one. A story with no delay therefore no longer waits a fixed 500 ms, and a slow story is waited for until it is done.

If the page never sends the signal (a Storybook without the channel, or a story that never finishes rendering), the runner falls back to the earlier behaviour: after 5 seconds (the play timeout, when plays run and it is longer) it waits delay ?? 500 ms and takes the screenshot, and the log notes that the story did not signal. A story that throws while rendering or in play is still photographed; one log line names the render phase and Storybook’s message.

StoryShelf reads parameters from the Storybook build. Both chromatic and storyshelf keys are supported; storyshelf wins on conflict so existing Chromatic users work unchanged.

ParameterWhereEffect
disableSnapshotparameters: { storyshelf: { disableSnapshot: true } } or chromatic:{disableSnapshot:true}Skip capture + play entirely — not counted, no failed
flakyTestparameters: { storyshelf: { flakyTest: true } } or chromatic:{flakyTest:true}Failure is non-blocking: build stays reviewing/approved, UI shows warning, GitHub status stays success with a warning comment
delayparameters: { storyshelf: { delay: 300 } }Extra wait (ms) after Storybook reports the story rendered, before the screenshot (no extra wait when unset; 500 only as the fallback)
diffThresholdparameters: { storyshelf: { diffThreshold: 0.2 } }Per-story pixelThreshold override (0–1)
pauseAnimationAtEndparameters: { storyshelf: { pauseAnimationAtEnd: true } }Pause CSS animations before screenshot; otherwise animations:"disabled"

Tags are story-level and case-insensitive. Whole-story non-blocking:

// via tags (works even when index.json has no parameters)
export const Flaky: Story = {
tags: ["flaky-test"],
play: async () => {
throw new Error("flake");
},
};
// via parameters (both keys work)
export const FlakyParam: Story = {
parameters: { storyshelf: { flakyTest: true } },
play: async () => {
throw new Error("flake");
},
};
export const ChromaticFlaky: Story = {
parameters: { chromatic: { flakyTest: true } },
play: async ({ canvasElement }) => {
/* ... */
},
};
export const Hidden: Story = {
parameters: { storyshelf: { disableSnapshot: true } },
// or tags: ["skip"] — also disables via isDisabledStory
};

disableSnapshot just disables; there is no reviewer waive in the build review UI (Approve/Reject only) — mark flaky-test/flakyTest in code or fix the test.

Storybook 10’s CSF-Next story.test("name", async ({ canvas, userEvent }) => { ... }) adds a test to a story. Write it as a statement of its own, not an exported call (Storybook’s indexer does not see the latter):

Primary.test("renders its label", async ({ canvas }) => {
await expect(canvas.getByRole("button")).toHaveTextContent("Button");
});

Each test is an index entry with subtype: "test". StoryShelf never screenshots them, and by default ignores them. Under Settings → Tests check Run Storybook tests (.test()) (runTests, default off, also on the project API) and each test runs once per build, in the same browser as the project’s primary target: the runner opens the test’s own iframe URL, Storybook renders the story and runs the test body, and the runner reads pass or fail from Storybook’s events (a body that throws, a story that fails to render, or no finish within the play timeout fails the test).

  • A failing test fails the build, exactly like a failing play; a test of a flaky-test story does not, and the tests of a story marked skip or disableSnapshot are not run.
  • The build page shows a Storybook tests card: failures first with their message, passes behind a disclosure. The same results are in the testResults JSON of the build on the API.
  • Tests take no screenshot, write no baseline and add no build status. Storybook 8 and 9 have no test entries, so the setting does nothing there. Tests run on every build, also with affected capture.

For the background on the preview page this reads from, see How Storybook works.

Storybook 8’s index.json (v5) omits parameters; StoryShelf prefers stories.json (which has them) then falls back to runtime page.evaluate(() => __STORYBOOK_PREVIEW__.extract()[id].parameters) for SB 8 without buildStoriesJson.

You do not need to edit .storybook/main.ts. When storyshelf upload builds Storybook for you, it sets STORYBOOK_BUILD_STORIES_JSON=true automatically. If you build outside upload (storybook build -o storybook-static then upload without rebuild), the runtime fallback still covers it. For a persistent file, you may add:

.storybook/main.ts
export default {
// ...
features: { buildStoriesJson: true },
};
  • Blocking play failure (!flaky) → build.status='failed', GitHub status failure, PR blocked.
  • Flaky failure (flaky-test/flakyTest) → build.status stays reviewing/approved (visual diff decides), GitHub status success with a warning comment ⚠️ flaky story failed — not blocking, UI shows a warning banner/badge.
  • Disabled → not counted in snapshotCount, no baseline update.

Retry a flaky capture with Retry on the build page (POST /api/v1/projects/:slug/builds/:id/retry).

See Project settings → Tests for the toggle, ADR 0017 for the full decision, and Configuration for buildStoriesJson details.