viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方はこちらまで。
はじめに
Storybookを用いたビジュアルリグレッションテスト(VRT)はChromaticやStorycap + reg-suitなどを用いることが多いかと思います。
しかし、ChromaticはGitHub Enterprise Serverを利用している場合は、Enterpriseプランの契約が必要となり、reg-suitは外部のクラウドストレージが必要です。
上記の制約がある中、ゆるくVRTを検証してみたかったので、リポジトリにベースイメージを保存してVRTを実行できるLost Pixelを使用してみました。
Lost Pixelとは
Lost PixelはOSSのビジュアルリグレッションテストツールで、Storybook、Ladle、Historie、ページスクリーンショット、Playwrightなどを用いたカスタムスクリーンショットをサポートしています。
OSS版の他に有料版のLost Pixel Platformというマネージドサービスも提供されています。
他のビジュアルリグレッションテストツールとは違い、リポジトリにベースイメージのスクリーンショットを保存して比較するのが特徴です。
使用環境
以下ライブラリのバージョンを使用します。
- typescript: 5.6.3
- storybook: 8.3.6
- lost-pixel: 3.21.0
導入
次のコマンドを実行します。
npx lost-pixel init-ts
npm i -D lost-pixel
プロジェクトルートにlostpixel.config.tsが作成されます。
import type { CustomProjectConfig } from "lost-pixel";
export const config: CustomProjectConfig = {
storybookShots: {
storybookUrl: "examples/storybook-build/storybook-static",
},
generateOnly: true,
};
これだけでLostPixelの導入は完了です。
プロジェクトの要件に応じて設定を変更してください。
ベースラインイメージの作成とリグレッションテスト
事前にStorybookをビルドします。
npx storybook build
現在のストーリを正としたいため、まずは次のコマンドでベースラインイメージを作成します。
npx lost-pixel update
コマンドを実行すると.lostpixel/baseline
にベースラインイメージが作成されます。
例えば次の画像をベースラインイメージとします。
ボタンのテキストをButton
からボタン
変更してみます。
次のコマンドを実行すると、現在のスクリーンショットを .lostpixel/current
に出力し、差分が発生した場合は .lostpixel/difference
に差分を出力します。
npx lost-pixel
差分が意図したものである場合は、npx lost-pixel update
を実行してベースラインイメージを更新します。
FlakyなUIをマスクする
FlakyなUIでVRTの実行毎に差分が発生する場合は、特定のUIをマスクすることが可能です。
https://docs.lost-pixel.com/user-docs/recipes/general-recipes/masking-page-elements
export const config: CustomProjectConfig = {
storybookShots: {
mask: [
{
selector: '[data-mask]'
}
]
}
};
例えばボタンのテキストをマスクした場合は、このような感じになります。
マスクを使用するほどでもない場合は、thresholdsで調整することも可能です。
https://docs.lost-pixel.com/user-docs/recipes/general-recipes/thresholds
特定のStoryのみを対象にする
全てのStoryではなく、VRT用のStoryのみをテストの対象にしたいことがあります。
LostPixelではfilterShotというオプションを使用します。
export const config: CustomProjectConfig = {
filterShot: ({ id, story, kind }) => {
return true;
},
};
filterShotの引数に渡されるStorybookのid、story、kindを使用してVRTの対象にするか判定できます。
以下がStoryとfilterShotに渡される引数のサンプルです。
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta = {
title: "Components/Button",
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};
export const VRT: Story = {};
// Primary
{
id: "components-button--primary",
story: "Primary",
kind: "Components/Button"
}
// VRT
{
id: "components-button--vrt",
story: "VRT",
kind: "Components/Button"
}
例えばサフィックスがVRTのStoryのみ対象にしたい場合は、次のような実装になります。
export const config: CustomProjectConfig = {
filterShot: ({ id, story, kind }) => {
// S名がVRTで終わる
return story.endWith("VRT");
},
};
画像比較のエンジンを切り替える
Lost Pixelの画像の比較にはodiffとpixelmatchが使用されており、オプションで切り替えることができます。デフォルト値はpixelmatchです。
ベンチマークではpixelmatchよりodiffの方が高速なため、比較画像が多く、パフォーマンスの問題が発生する際にはodiffの使用を検討してください。
export const config: CustomProjectConfig = {
compareEngine: "odiff", // 'pixelmatch' or 'odiff'
};
まとめ
今回はベースラインイメージをリポジトリ管理して、VRTを実行できるLost Pixelを紹介しました。
画像をリポジトリ管理することの是非はあるかと思いますが、依存が少なく検証も簡単なため、ゆるくVRTを運用していきたい場合は導入してみるのもありかと思います。
参考
一緒に二次元業界を盛り上げていきませんか?
株式会社viviONでは、フロントエンドエンジニアを募集しています。
また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。