When a Story Becomes a Test
A Storybook story already describes a component in a known state, so it can do more than provide a convenient preview. This chapter examines what changes when that artifact becomes a browser-based component test: which part checks rendering, which part simulates behavior, how the Vitest addon turns CSF into test calls, and when portable stories or the legacy test runner are a better fit. The useful judgment is not “stories replace tests,” but whether the story captures a question that an automated run can answer reliably.
Render first, then exercise behavior
The simplest automated question is whether a story can render successfully. A Button story with a label and a visual state may not need any additional interaction code to expose a broken import, missing provider, invalid render result, or runtime exception. Storybook describes this as a render test or smoke test: the test passes when the story renders and fails when rendering produces an error.
This is valuable because a story collection can cover many static states without requiring a separate test file for each one. A disabled, long-label, loading, or empty state can be checked simply by being rendered. That coverage is not exhaustive. It says that the component reached the declared state without failing; it does not say that a user can complete a meaningful task from that state. The next question is behavior.
An interaction test starts from the story’s rendered state and uses a play function to describe a user action and its expected result. A form story might begin with empty fields, find the submit button, click it, and assert that validation appears. Another story might provide valid input, submit, and assert that a success callback was called. The initial args and context make the situation reproducible; the play function supplies the transition that a static render cannot observe.
Storybook’s interaction APIs line up with this sequence. The play function receives a canvas representing the story under test. Queries from Testing Library find elements in that canvas, preferably by accessible role or label so the test resembles how a person uses the UI. A userEvent utility simulates clicks, typing, tabbing, keyboard input, and selections. The expect utility checks the resulting DOM or a spy. The test is therefore not a list of tools; it is a path from an initial state through an observable user action to an assertion.
The choice of query is part of the test’s meaning. Finding a button by its accessible role checks that the control is exposed as a button to assistive technology as well as finding a node with a particular class. A test ID can still be useful when no user-facing query fits, but the documented guidance treats it as a last resort. The story test can thus provide a small accessibility signal through the way it interacts, even before a dedicated accessibility check is added.
The repository’s component-test stories show several forms of behavior: grouped steps for readability, spies created as story args, waits for asynchronous output, loaders that delay or supply data, and interaction sequences that use keyboard input. These examples also show why lifecycle matters. If a play function changes state or a loader establishes a fixture, the next story must start clean. Storybook’s beforeEach and returned cleanup functions exist so one run does not silently become the input to another.
mount introduces another boundary.