@storycap-testrun で Desktop / Mobile を分けてスクリーンショットを撮る方法


今回のおはなしのサイト

https://github.com/atsmile/karaoke-compa

今日はコード中心なのでGitHubを貼ります

VRTが動くようになったので、Storybookを整備しはじめました。

方針として Story は Desktop と Mobile で分けて管理する方針です
(Storybookのページ設定を変えなくても両方見れる仕組みですね)

まずはDesktop用Storyから実装し、VRTが動く状態を確認。
次に Mobile 用 Story を追加しようと作業を進めていました。

ところが、Desktop・Mobile 共に同じ状態になっている?ということで

「Mobileのビューポートが反映されていない」と気づき、原因を調べることにしました。

うまくいかなかった方法

最初に試したのは parameters.viewport でビューポートを指定する方法です。

export const Mobile: Story = {
  parameters: {
    viewport: {
      defaultViewport: "mobile2",
    },
  },
};

しかしStorybook上でも変化がなく、撮影される画像のサイズも変わらず、Desktopと同じ幅のままでした。

で、調べてみると、Storybook 9以降は parameters.viewport が非推奨になっているようで、globals.viewport を使うのが正しいとのことで、さっそく書き換えてみました!

export const Mobile: Story = {
  globals: {
    viewport: { value: "mobile2" },
  },
};

お!?

なんと Storybookのプレビュー上ではモバイル表示になりました。
おーやっとこれで行けたかー!?と安心したのもつかの間

いざキャプチャ!してみると、キャプチャの幅はDesktopのまま。。。

解決策を実装するにあたり、@storycap-testrun/browser の正しいAPIを確認する必要がありました。

ドキュメントではわからなかったので、GitHub のパッケージのソースコードを直接読んだりなんかして調べたりもしましたが、まったく解決策が見つからず。。。

そして、さらに調べていくうちに原因がわかりました。

Storybook の viewport addon は iframeの見た目を変えているだけで、ブラウザ自体のビューポートは変わっていないのです。

なので @storycap-testrun/browser はブラウザの実際のビューポートでスクリーンショットを撮るため、Storybookの設定だけでは反映されませんでした。

解決策

解決策はシンプルで、vitestのテストをDesktop・Mobileで分けて、storybookScreenshot() の viewport オプションでブラウザのビューポートを直接指定する方法です。
vitest.config.ts の projects に2つのテストを定義します。

export default defineConfig({
  test: {
    projects: [
      {
        // Desktop
        plugins: [
          storybookTest({ configDir: path.join(dirname, ".storybook") }),
          storybookScreenshot({
            viewport: { width: 1280, height: 800 },
            output: {
              dir: "__screenshots__",
              file: path.join("[file]", "desktop-[name].png"),
            },
          }),
        ],
        test: {
          name: "storybook",
          // ...
        },
      },
      {
        // Mobile
        plugins: [
          storybookTest({ configDir: path.join(dirname, ".storybook") }),
          storybookScreenshot({
            viewport: { width: 414, height: 896 },
            output: {
              dir: "__screenshots__",
              file: path.join("[file]", "mobile-[name].png"),
            },
          }),
        ],
        test: {
          name: "storybook-mobile",
          // ...
        },
      },
    ],
  },
});

出力ファイル名に desktop- / mobile- のプレフィックスをつけることで、reg-suitが同じディレクトリで両方を管理できるようになります。これでDesktop・Mobile両方のスクリーンショットがVRTで比較されるようになりました。

ただ、このままでは全StoryがDesktop・Mobile両方でキャプチャされてしまい、枚数が倍になってしまいます。

そこで vitest.setup.ts でStory名を見てスキップする振り分けを追加しました。

beforeEach(async (context) => {
  const projectName = context.task.file?.projectName ?? "";
  const isMobileProject = projectName.includes("storybook-mobile");
  const isDesktopStory = context.task.name.includes("Desktop");

  if (isMobileProject && isDesktopStory) {
    context.skip();
  }
  if (!isMobileProject && !isDesktopStory) {
    context.skip();
  }
});

振り分けのルールはシンプルで、Story名に Desktop が含まれるものはDesktop専用、それ以外(Default や Mobile など)はMobile扱いとして、対象でないテストをスキップするようにしました。
(運用変更予定のため実際のコードと異なる場合があります)

テストは2つ走りますが、それぞれでスキップが入るためキャプチャ枚数はStory数と同じになります。

あわせてCI/CDのスクリーンショット取得コマンドも2つのテストを指定する形に変更しました。→ vrt.yml

- name: Run screenshots
  run: npx vitest --project storybook-mobile --project storybook-desktop run

 

なお今回はDesktop・Mobileの2つのテストですが、テストの定義が増えたときのために関数化してまとめました。→  vitest.config.ts

const createStorybookProject = (
  name: string,
  viewport: { width: number; height: number },
): TestProjectConfiguration => ({
  extends: true,
  plugins: [
    storybookTest({ configDir: path.join(dirname, ".storybook") }),
    storybookScreenshot({
      viewport,
      output: {
        dir: "__screenshots__",
        file: path.join("[file]", `[name]-${name}.png`),
      },
    }),
  ],
  test: {
    name,
    browser: {
      enabled: true,
      headless: true,
      provider: playwright({}),
      instances: [{ browser: "chromium" }],
    },
    setupFiles: [".storybook/vitest.setup.ts"],
  },
});

export default defineConfig({
  test: {
    projects: [
      createStorybookProject("storybook-mobile", { width: 414, height: 896 }),
      createStorybookProject("storybook-desktop", { width: 1280, height: 800 }),
    ],
  },
});

Tablet等を追加したい場合も1行追加するだけで対応できます。

 

やってみて

よかったこと

Desktop・Mobile両方のレイアウト崩れをVRTで検知できるようになったのは素直によかったです。

これまでMobileの見た目はブラウザで目視確認するしかなかったので、PRのたびに自動で差分が出るのは安心感があります。

また関数化したおかげで、Tabletを追加したくなったときも1行増やすだけで対応できる構成になっています。

大変だったこと

globals.viewport でStorybookのプレビューは変わるのに、キャプチャには反映されないという現象の原因を掴むまでが一番時間がかかりました。

Storybookの設定でのビューポート変更は、Storybook上での見た目が変わるだけで、キャプチャ時のブラウザのビューポートとは別物だと気づいてからは、解決策がすんなり見えた気がします。割と強引ではありますが(笑

VRTの実装って Desktop と Mobile の両方が最低限必要だと思っているので、ようやくスタートラインに立てたような気持ちになっています。

現状のパッケージだと解決策がなさそうなので 、同じような環境の人が多いんじゃないかな?と思うので備忘録として置いておきます。