DEV Community

flyinglimao
flyinglimao

Posted on

Visual Tests and Snapshot Tests in Storybook Test-Runner's Play Function

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;
Enter fullscreen mode Exit fullscreen mode

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
  },
};
Enter fullscreen mode Exit fullscreen mode

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();
    });
  },
  ...
Enter fullscreen mode Exit fullscreen mode

Error when expose function in prepare phase

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)