reg-suit + AWS S3 でビジュアルリグレッションテストを運用する


前回の記事

https://atsmile.net/2026/05/08/how-to-cap-storycap-testrun/

前回の記事では、Storybook + @storycap-testrun を使ってDesktop/Mobile のスクリーンショットを撮るところまでを紹介しました。

今回はその続きで、撮ったスクリーンショットを比較・レポート化する reg-suit と、比較結果の保存先として AWS S3 を組み合わせた実装を中心に紹介します。

あわせて、実際に運用してみてぶつかった フォント問題・アニメーション対応・Storyの選定 についても書いていきます。

reg-suit + AWS S3 の実装

reg-suit は、スクリーンショットの差分を検出してHTMLレポートを生成してくれるツールです。単体でも使えますが、比較結果をどこかに保存する仕組みが別途必要になります。

今回は保存先として AWS S3 を選びました。
ポートフォリオでEC2を使うきっかけになった実装ですね!

設定は regconfig.json に書いていきます。主なポイントは下記の3つです。

① キャプチャ画像の取得元を指定する

reg-simple-keygen-plugin でキーを生成し、reg-publish-s3-plugin でS3バケットへのアップロード先を指定します。

② S3バケットの用意

バケットはパブリックアクセスをブロックしつつ、reg-suitが読み書きできるIAMユーザーを作成してアクセスキーを発行します。CIで使う場合はGitHub ActionsのSecretsに登録しておきます。

③ GitHub Actionsとの連携

mainブランチへのプッシュをトリガーに、storycapでキャプチャ → reg-suitで比較 → S3にレポートをアップロード、という流れをワークフローに組み込みます。

実際に動かすと、差分があった箇所だけピックアップされたHTMLレポートがS3上に生成されます。「どこが変わったか」が一目でわかるのはかなり便利でした。

フォント問題にハマった話

実装が一通り動くようになってから、CIで撮ったスクリーンショットがローカルと微妙にズレていることに気づきました。

コードは何も変えていないのに、なぜか差分として検出されていました。

まぁ文字部分だけだったのでおおよその見当はつきましたが、原因を調べたところやはりフォントの違いでした。

ローカル環境にはシステムフォントやWebフォントがインストールされているのに対して、CI環境(GitHub ActionsのUbuntuランナー)にはそれらが入っていません。

フォールバックで別のフォントが適用された結果、テキストの描画が微妙に変わり、差分として検出されてしまっていました。

対応としては、CIのワークフローに日本語フォントのインストールステップを追加しました。

- name: Install fonts
  run: sudo apt-get install -y fonts-noto-cjk

これだけで差分が消えました。フォント問題はハマりやすいポイントなので、VRTを導入する際は最初から意識しておくと良いと思います。

アニメーション対応

@storycap-testrun でスクリーンショットを撮るとき、アニメーションが何も始まっていない状態でキャプチャされてしまう問題があり、これを解決するために色々と試しました。

最初に試したのはデコレーターでのアニメーション無効化です。
preview.tsx にスタイルを渡す方法を試しましたが、これではKeyVisualのアニメーションが止まりませんでした。

次に useInView をモックする方法も検討しましたが、今回のKeyVisualはフックでの実装だと発火しない可能性があり、CSSアニメーションのみで実装していたため、モックは効果がありませんでした。

最終的に .storybook/preview.css を作成し、Storybook内のみで全体にアニメーションを無効化するCSSを追加する方法に落ち着きました。

*,
*::before,
*::after {
  animation-duration: 0s !important;
  animation-delay: 0s !important;
  transition-duration: 0s !important;
  transition-delay: 0s !important;
}

ただしこれだけでは問題が残りました。

KeyVisualではフェードインの初期状態として Tailwind の opacity-0 クラスを使っており、アニメーション自体が発火しないことで opacity-0 のまま要素が非表示でキャプチャされてしまいます。そこで

preview.css にスコープを絞った上書きも追加しました。

[data-layout="KeyVisual"] .opacity-0 {
  opacity: 1 !important;
}

全体に適用すると他のコンポーネントへの影響が出るため、KeyVisual配下に限定しています。

なお、こんなときのために data-layout 管理で考えていました!
クラス名と異なるスタイルはあまり広げたくないので限定的にできるのがいいですね!

play関数でインタラクションをキャプチャ

VRTで「開いた状態のハンバーガーメニュー」もキャプチャしたいと思い、play関数を使う方法を試しました。

play関数はStorybookのStoryに定義できる非同期関数で、キャプチャ前にクリックなどの操作を再現できます。importは storybook/test のサブパスから行います。

import { userEvent, within, waitFor } from "storybook/test";

export const Mobile_Open: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole("button", { name: "メニューを開く" });
    await userEvent.click(button);
    await waitFor(() => {
      const btn = canvas.getByRole("button", { name: "メニューを閉じる" });
      if (!btn) throw new Error();
    });
  },
};

within でキャンバス内に操作スコープを絞り、userEvent.click でボタンをクリック、waitFor でメニューが開いた状態になるまで待機しています。

これで開いた状態のメニューをキャプチャできました。

ちなみにimportパスは @storybook/test ではなく storybook/test になっています。
Storybook v8以降のサブパスimportで、最初ここでなかなかインポートできずでハマりました。

VRT対象Storyの選定

Storybookはコンポーネントカタログとしても使っているので、ボタンやアイコンなど細かいUIパーツも含めてStoryを用意しています。

そのためVRTもすべてキャプチャされるため、すべてのStoryがVRT対象になっています。

VRT対象を絞ることも考えましたが、細かいパーツであっても意図しない変化を検知できることはメリットなので、まずは全Story対象のままで運用することにしました。

差分が増えてレビューコストが上がってきたタイミングで、重要度の低いStoryを除外していく方向に調整するつもりです。

VRTは導入して終わりではなく、運用しながら育てていくものだと感じていますので!

やってみて

reg-suit + AWS S3 の構築自体はドキュメント通りに進められましたが、フォント問題やアニメーション対応など、実際に動かしてみないとわからないところでハマることが多かったですね。

特にアニメーション対応は色々な方法を試した末に preview.css に行き着いたので、思ったより時間がかかりました。

一方でplay関数でインタラクションをキャプチャできたのは特に問題なくすっと出来ました。
「開いた状態のメニューも対象にできる」となると、VRTでカバーできる範囲がぐっと広がる感覚がありますね〜。

VRTを導入した目的はまさに「意図した変更かどうかを仕組みとして確認できるようにすること」だったので、CIで自動的に差分を検出できる状態になったのはありがたいことですです。

今後はDB化やコンテンツ追加など変更が増えていく予定なので、その際にVRTが機能してくれるのが楽しみです。