[RFC] Post-Test Assertions Using afterEach in Storybook #29583
Replies: 1 comment 6 replies
-
You've made it clear why such a hook is useful and why cleanup should not be done immediately after tests. I have two concerns with the proposed API: the naming, and the ability to disable inherited assertions. On the latter, a11y tests are far from perfect and run into the occasional false positive. Large teams that are given mandatory test assertions by a well meaning dx engineer may run into irrelevant use cases. How does one disable such tests? I'm worried the simplest course of action here is to comment out my own test/story since it would be failing otherwise. On the naming, reusing a name that has a universal meaning to it to do something new, and, specifically, to NOT do what that name means is a hurdle to learning and transferring existing knowledge about testing. I'd rather, as a developer, receive an error message when using And so, if that name was made to error out, it would force your intended use case, which is newish, to find a unique name that best conveys its purpose. This could possibly be a name chosen jointly with vitest-browser, possibly one backed by user research (card sorting can work in this situation). I would also consider an API that lets me register and unregister assertions. Curious to see if folks would consider it overkill though. |
Beta Was this translation helpful? Give feedback.
-
RFC: Post-Test Assertions Using afterEach in Storybook
The proposed
afterEach
hook in Storybook is designed to support post-test assertions such as accessibility (a11y) checks, automatic snapshots, and other validation tasks that need to occur after each story'splay
function has executed.Why Use
afterEach
for Post-Test Assertions and Not for Cleanup?In many testing frameworks, the primary purpose of
afterEach
is for cleanup. However, in Storybook, we recommend performing cleanup in the return callback of thebeforeEach
hook. This approach offers a convenient way to co-locate the creation and cleanup of state providing a better developer experience (DX).Vitest also supports return a cleanup function in beforeEach:
https://vitest.dev/api/#beforeeach
Example of Cleanup in
beforeEach
Return CallbackAlternatively, you can perform cleanup at the very beginning of a globally registered
beforeEach
inpreview.ts
:This is often simpler, as you don’t have to think about resetting state at a story level.
Understanding Storybook's Approach to Cleanup
Storybook is a visual testing tool where you can execute tests in an interactive development environment. In command-line interface (CLI) testing frameworks like Jest or Vitest, tests are executed from beginning to end, and hooks such as
afterEach
andafterAll
are used to clean up state created during tests. For example:In an interactive testing tool like Storybook, it's preferable not to execute cleanup callbacks at the end of a test's interactive execution. This allows you to interact with the end state of the test, such as performing additional DOM interactions while the mocked server remains active. Cleanup callbacks are only called when switching stories. Making sure a new story execution, starts with a fresh state.
Note: Vitest's browser mode faces similar challenges and has forked testing library to ensure cleanup is not run at the end of a test:
https://github.com/vitest-dev/vitest-browser-react
To address this challenge more generally, we propose having two distinct stages at the end of the testing lifecycle:
afterEach
): For tasks like accessibility checks, snapshots, and other validations that should occur after the story'splay
function has completed.beforeEach
): For resetting or cleaning up state that was set up inbeforeEach
, ensuring that each story starts fresh.Migrating from
useEffect
Some addons have implemented post-test validations using Storybook's
useEffect
API (note: this is different from React'suseEffect
):However, promises in
useEffect
cannot be awaited, which poses challenges when executing tests—you can't determine if the test has fully ended. Consequently,useEffect
is currently not executed in our Vitest-based test runner (using portable stories). Additionally, theuseEffect
API can be clunky.Note that many use cases previously handled by
decorators
can now be achieved withbeforeEach
. Many scenarios involving adecorator
combined withuseEffect
can be better handled withafterEach
in the future.Usage Examples
Addon Usage of the
afterEach
HookAddons in Storybook can leverage the
afterEach
hook by defining it within a preview file, registering post-test assertions for each story run. This allows for seamless integration of accessibility checks, visual snapshots, and other custom assertions.For example, an addon might register an
afterEach
hook that performs accessibility tests and captures visual snapshots after each story execution:Just like
beforeEach
anddecorators
, an array of functions is also accepted:The items in the array are executed in reverse order. See order of execution below.
Story-Specific Uses for the
afterEach
HookWhile
afterEach
can be used in addons for universal checks (e.g., accessibility, snapshots), it can also be leveraged on a per-story file basis for unique post-test behaviors.Post-Interaction DOM Validation
Certain stories may require a specific DOM structure or state after the story interactions have completed. The
afterEach
hook can assert that elements are in the expected state, which is particularly useful for stories where UI conditions need to be validated for each story in the story file.Order of Execution
Storybook executes hooks and functions in a specific order across various levels, from global presets to story-specific configurations.
Execution Levels and Order
In Storybook, presets and hooks can be defined at different levels:
./storybook/preview.ts
or in an addon's preview file, such as./addons/preview.ts
. These preview files apply globally across all stories.Detailed Order of Execution
Given this structure, here is the specific order of execution for hooks and functions during a story run:
beforeAll
beforeEach
beforeEach
beforeEach
play
afterEach
afterEach
afterEach
beforeEach
return callbackbeforeEach
return callbackbeforeEach
return callbackbeforeAll
return callback (only happens when storybook reloads or is finished)beforeAll
,beforeEach
): These are executed from the outermost level (global presets) to the innermost level (individual story).play
Function: The story'splay
function is executed after allbeforeEach
hooks have run.afterEach
): These are executed after theplay
function, starting from the innermost level (individual story) out to the global presets.beforeEach
andbeforeAll
in reverse order.Beta Was this translation helpful? Give feedback.
All reactions