Visual tests and snapshot tests are crucial in frontend development. Storybook's Test-Runner uses components' stories as test cases to ensure accurate functionality. We utilize the Test-Runner to capture visual and DOM changes based on the component's state. This document introduces how to use Visual Tests and Snapshot Tests within the Play Function of Storybook Test-Runner.
Issue
While the Storybook documentation touched on Visual Tests (Image Snapshot) and Snapshot Tests (HTML Snapshot), it did not account for the fact that the postVisit
point occurs after the completion of the operation, making we can't test the intermediate states.
Solution
You can find code here: https://github.com/flyinglimao/storybook-test-runner-example
In the preVisit
phase, we add a Snapshot function to the Window object. (Some declaration for TS may be required.)
// .storybook/test-runner.ts
import { type TestRunnerConfig } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const config: TestRunnerConfig = {
setup() {
expect.extend({ toMatchImageSnapshot });
},
async preVisit(page) {
if (await page.evaluate(() => !("takeSnapshot" in window))) {
await page.exposeBinding("takeSnapshot", async ({ page }) => {
const elementHandler = await page.$("#storybook-root");
const innerHTML = await elementHandler?.innerHTML();
expect(innerHTML).toMatchSnapshot();
});
}
if (await page.evaluate(() => !("takeScreenshot" in window))) {
await page.exposeBinding("takeScreenshot", async ({ page }) => {
const image = await page.locator("#storybook-root").screenshot();
expect(image).toMatchImageSnapshot();
});
}
},
};
export default config;
Then, call the Snapshot function in the Play function.
// .storybook/button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, within } from "@storybook/test";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Button",
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
onClick: fn(),
},
play: async () => {
await window.takeSnapshot?.();
await window.takeScreenshot?.();
// Can be called multiple times
},
};
Reasons for Not Adding in Prepare
In the Prepare phase, expect
is not yet injected, making it inaccessible.
// .storybook/test-runner.ts
...
const config: TestRunnerConfig = {
async prepare({ page, browserContext, testRunnerConfig }) {
...
// default prepare
...
// expose takeSnapshot
await page.exposeBinding("takeSnapshot", async ({ page }) => {
const elementHandler = await page.$("#storybook-root");
const innerHTML = await elementHandler?.innerHTML();
expect(innerHTML).toMatchSnapshot();
});
},
...
The expect
function is available from setup
. However, setup
is a function without arguments.
Conclusion
When setting up the testing environment, it was essential to capture not only the final state of the component but also intermediate states. In the quest to test the intermediate states of components, this method was discovered.
Top comments (0)