ホーム自己紹介ブログ
NO.181
DATE2025. 11. 29

StorybookのcomposeStoryとVitestのtoMatchScreenshotを組み合わせたVRT

Web フロントエンドの UI 開発では、私の中では Storybook は、ほぼ必須なツールです。
UI カタログとして使うだけでなく、 ビジュアルリグレッションテスト(VRT) にも活用したいと常々思っています。

VRT が必要になる場面としては、たとえば次のようなケースがあります。

  • 共通コンポーネントのデザイン修正後、依存コンポーネントの崩れを検知したい
  • デザイントークンやテーマ変更の影響範囲を確認したい
  • ライブラリアップデート時にデザイン差分を確認したい

Storybook の以下のドキュメントでは、Visual Tests は Chromatic の利用が紹介されています。

  • Visual tests | Storybook docs

ただ、さまざまな理由から Chromatic に依存したくないこともあります。

Vitest の Browser Mode と toMatchScreenshot

ここで役に立つのが、Vitest に追加された Browser Mode です。
ヘッドレスブラウザ(Chromium など)上での UI テストが可能になりました。

  • Browser Mode | Guide | Vitest

さらに、 toMatchScreenshot により、スクリーンショットを比較する VRT ができるようになりました。

  • Visual Regression Testing | Vitest

Storybook の composeStory と組み合わせる

Storybook には composeStory(および composeStories)というAPIがあり、
Storybook の Story オブジェクトをテストコード内で利用できます。

  • Stories in unit tests | Storybook docs

これを使うことで、

  • Storybook に定義した UI 状態(Story)
  • Vitest のスクリーンショット比較(toMatchScreenshot)

をそのままつなげて、Chromatic なしで Storybook の Story を VRT できるようになります。

実際に対応したコミット

実際にこの方法を試したコミットはこちらです。

  • https://github.com/silverbirder/fequest/commit/13a331b029a406781d500f1ce88a26cf924b3918

上記コミットには、Vitest 側の不具合修正を早めに取り込みたかったため、patch を当てています。

  • 参考: https://github.com/vitest-dev/vitest/issues/8853

使用した環境は次のとおりです。

  • @storybook/react-vite
  • vitest-browser-react

当初は @storybook/nextjs-vite で試していたものの、Next.js の next/navigation 周りの mock エラーに苦戦し、一旦断念しました。
再挑戦すれ解決できる可能性はありますが、今回は @storybook/react-vite を使用しています。

サンプルコンポーネント

// my-input.tsx
import { Input } from "<path to shadcn input component>";
import { ComponentProps } from "react";
 
type Props = ComponentProps<typeof Input>;
 
export const MyInput = ({ ...rest }: Props) => {
  return <Input {...rest} />;
};

Storybook 側のファイル

// my-input.stories.tsx
import type { Meta, StoryObj } from "@storybook/react-vite";
 
import { MyInput } from "./my-input";
 
const meta = {
  component: MyInput,
} satisfies Meta<typeof MyInput>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {};

Vitest の VRT テストコード

// my-input.spec.tsx
import { composeStories } from "@storybook/react-vite";
import { describe, expect, it } from "vitest";
import { render } from "vitest-browser-react";
 
import * as stories from "./my-input.stories";
 
const { Default } = composeStories(stories);
 
describe("MyInput", () => {
  it("matches Default story screenshot", async () => {
    const { getByTestId } = await render(
      <div data-testid="test">
        <Default />
      </div>,
    );
 
    await expect(getByTestId("test")).toMatchScreenshot();
  });
});

このテストを実行すると、Default Story のスクリーンショットが保存され、
次回以降はその差分が検知されるようになります。

これで Storybook の Story を全部 VRT できる

composeStories と toMatchScreenshot を組み合わせることで、

  • Storybook の Story をそのままテストに読み込む
  • Story 全パターンに対してスクリーンショット比較を行う
  • Chromatic なしで VRT ができる

という、シンプルな構成で VRT ができそうです。
Storybook と Vitest という、どちらもメジャーな OSS だけで実現できるため、外部依存を減らせる点も魅力的です。

追記

@storybook/nextjs-vite でもスクリーンショット VRT が動作することを確認できました。 ポイントは、以下で紹介されている vite-plugin-storybook-nextjs を導入する必要があったことです。

  • Portable stories in Vitest | Storybook docs

私の環境では monorepo を採用しているため、vite.config.ts に次のような設定を追加しました。

// vite.config.ts
import nextjs from "vite-plugin-storybook-nextjs";
 
export default defineConfig({
  plugins: [nextjs({dir: "../../apps/user"})],
});

テストの書き方ですが、上記の紹介した形式だと、以下のエラーが発生しました。

SB_FRAMEWORK_NEXTJS_0002 (NextjsRouterMocksNotAvailable): Tried to access router mocks from "next/router" but they were not created yet. You might be running code in an unsupported environment.

そこで、以下のようなテストの書き方に変更したら動作しました。

// my-input.spec.tsx
import { composeStories } from "@storybook/react-vite";
import { describe, expect, it } from "vitest";
import { render } from "vitest-browser-react";
 
import * as stories from "./my-input.stories";
 
const { Default } = composeStories(stories);
 
describe("MyInput", () => {
  it("matches Default story screenshot", async () => {
    await Default.run();
 
    await expect(document.body).toMatchScreenshot();
  });
});

修正したコミットは以下になります。

  • https://github.com/silverbirder/fequest/commit/d8ef4ddbb52ab78d954f1db2d95b727d6f46d81c
フロントエンド
テスト

-

読者になる

|

シェアする

|

silverbirders

silverbirder

Webソフトウェアエンジニア

ブログを応援する

この記事がよかったら、お布施という形で応援してもらえるとうれしいです。

おふせぼたん

※ ログイン不要で投稿できます。

※ 同じブラウザから投稿を削除できます。

0

読み込み中...

次の記事へ前の記事へ

関連する記事

タグ「フロントエンド」の記事

Webフロントエンドのコードレビューメモ

Webフロントエンドのコードレビューをしているときに考えていることについて書きます。 毎日1記事投稿、1記事30分という制約を課していますので、本記事は完璧ではありません。(言い訳) また希望的な考えもあるので、実践していないものもあります

2026年03月12日

フロントエンド
AIの書いたコードの手直しを減らすお作法

AI にコードを書かせた後、余計なコードを見つけて消す作業があります。 不毛なことなので、それらの作業を減らすためのお作法を紹介します。 未使用コードを消す 以下でも書きましたが、未使用コードの検査に knip を使うことが多いです。 ht

2026年02月25日

AI
フロントエンド
iframeの難しさ

最近、iframeを使っています。 クライアントサイドで埋め込む想定で、iframeを使おうとしています。 色々と苦労したことがあったので、書いて残しておこうと思います。 レスポンスヘッダー 前提として、ウェブアプリケーションをプロダクショ

2026年02月18日

フロントエンド
ブラウザ

タグ「テスト」の記事

CSSを、Vitestでテストしてみる

以下の記事で書いた CSSをテストする方法について、試してみました。 https://zenn.dev/silverbirder/articles/df6752b230f04c ソースコードは、以下に置いています。 https://gith

2026年02月10日

フロントエンド
テスト
CSS Layout Testing というテスト手法の提案

Web のフロントエンド実装において、次のようなミスによってデザイン崩れを起こしてしまったことはありませんか。 flex-shrink の指定を忘れて、要素が押しつぶされてしまった z-index の指定を間違えて、要素が意図せず前面(また

2026年01月10日

フロントエンド
テスト
単体テストを全通り書くんじゃない!

AIの進化によって、プロダクションコードに対するテストコードは、以前と比べて格段に書きやすくなったと感じています。 単体テストに関する基本的なお作法については、以前に以下の記事で整理しました。 興味があれば、参考として読んでもらえると嬉しい

2026年01月09日

テスト
← ブログ一覧へ