Playwright + GitHub Actions / Pagesで確認しやすいStorybookのVRT環境を作る
きっかけ
個人開発のプロジェクトでStorybookのVRTをやりたくなったので、Chromaticを導入しました。
手軽に導入でき、テストも安定していて満足していたのですが、無料枠がちょっと心許ない感じでした。
5,000 free snapshots per month
https://www.chromatic.com/pricing
有料プランは最安でも $149/ month と、個人で利用するには負担が大きい価格だったため、「それなら作っちゃおう!」と思ったのがきっかけです。
目指すもの
PR作成時 or PRへの追加コミット時に以下が実行される環境を作ります。
- Storybookのユニットテスト
- StorybookのVRT
- VRTが失敗したらPRのコメントで通知
- VRTの結果をWeb上で確認できる
環境
- Storybook: v8.3.6
- Playwright: v1.48.1
プロジェクト固有の部分について
- ランタイムとパッケージマネージャーに Bun を使用
- Bunのバージョン管理に mise を使用
- main と develop ブランチに対するPRでのみVRTを実行
作っていく
Storybookのユニットテスト
これはドキュメントにあるサンプルをベースにしました。
内容としては Storybook をビルドして、test-runner
を実行するシンプルなものです。
storybook-test.yml(全体)
name: Storybook Tests
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
jobs:
storybook-test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup mise
uses: jdx/mise-action@v2
with:
install: true
cache: true
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1385567519
- name: Get Playwright Version
run: |
PLAYWRIGHT_VERSION=$(bun pm ls | grep @playwright | sed 's/.*@//')
echo "Playwright v$PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
- name: Serve Storybook and run tests
run: |
bunx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"bun preview-storybook" \
"bunx wait-on tcp:127.0.0.1:6006 && bun test-storybook"
Playwrightをキャッシュして実行時間を短縮しています。
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1385567519
- name: Get Playwright Version
run: |
PLAYWRIGHT_VERSION=$(bun pm ls | grep @playwright | sed 's/.*@//')
echo "Playwright v$PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
StorybookのVRT
テストコードを書く
Playwrightを使って各コンポーネントのスナップショットを撮るテストコードを書きます。
Storybookのビルド時に生成されるindex.json
(各ストーリーの情報が入ったファイル)を読み込んで、スナップショットを撮って回ります。
画像周りが安定しないことが多いので、Issueを参考に画像を読み込むまで待つようにしています。
import { readFileSync } from "node:fs";
import test, { expect } from "@playwright/test";
import type { StoryIndex } from "@storybook/types";
// スナップショットを取得しないストーリー
const skip = [
"hoge--default"
// ここにスキップしたいストーリーのidを追加
];
const indexJson = readFileSync("storybook-static/index.json");
const json = JSON.parse(indexJson.toString()) as StoryIndex;
for (const [id, { tags }] of Object.entries(json.entries)) {
// Play関数を持つストーリーはスナップショットを取得しない
if (tags?.includes("play-fn")) {
continue;
}
if (skip.includes(id)) {
continue;
}
test(id, async ({ page }) => {
const url = new URL("http://localhost:6006/iframe.html");
url.searchParams.set("id", id);
await page.goto(url.toString());
// 画像の読み込みを待つ
// https://github.com/microsoft/playwright/issues/6046#issuecomment-1803609118
try {
// NOTE: getByRole ではなく locator を使っているのは、SVG 画像を拾ってほしくないため
for (const img of await page.locator("img").all()) {
await expect(img).toHaveJSProperty("complete", true);
await expect(img).not.toHaveJSProperty("naturalWidth", 0);
}
} catch (e) {
// 失敗しても大丈夫なので無視
}
await expect(page).toHaveScreenshot(`${id}.png`, {
fullPage: true
});
});
}
Playwrightの設定はこんな感じです。
撮影したスナップショットはプロジェクトルートの__snapshots__
に保存しています。
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
snapshotPathTemplate: "../__snapshots__/{testFilePath}/{arg}{ext}",
reporter: "html",
timeout: 30 * 1000,
retries: 1,
fullyParallel: true,
expect: {
timeout: 10 * 1000
},
use: {
baseURL: "http://localhost:6006",
trace: "retain-on-failure"
},
projects: [
{
name: "chrome",
use: {
...devices["Desktop Chrome"]
}
}
]
});
Expectedのスナップショットを保存するワークフロー
これはデフォルトブランチ(今回はdevelop)へのマージ時に実行されるようにします。
スナップショットは、GitHub Actionsのキャッシュに保存する方法を取りました。
storybook-vrt-update.yml(全体)
name: Storybook VRT Update
on:
workflow_dispatch:
push:
branches:
- develop
jobs:
storybook-vrt-update:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup mise
uses: jdx/mise-action@v2
with:
install: true
cache: true
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1385567519
- name: Get Playwright Version
run: |
PLAYWRIGHT_VERSION=$(bun pm ls | grep @playwright | sed 's/.*@//')
echo "Playwright v$PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
- name: Serve Storybook and run tests
run: |
bunx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"bun preview-storybook" \
"bunx wait-on tcp:127.0.0.1:6006 && bun playwright test -c .storybook/playwright.config.ts .storybook/ --update-snapshots"
- name: Update Cache VRT snapshots
if: success()
uses: actions/cache/save@v4
id: storybook-vrt-cache
with:
path: __snapshots__
key: storybook-vrt-snapshots-${{ github.sha }}-${{ github.run_id }}
PR作成時にVRTを実行するワークフロー
まずは、VRTを実行して結果を保存するワークフローを作ります。
流れとしては以下の通りです。
-
Restore Cache VRT snapshots
でキャッシュからスナップショットを復元 -
Serve Storybook and run tests
でVRTを実行 -
Store Artifacts
でVRTの結果をArtifactsに保存
Artifactsは後続のジョブでデプロイするので、保存期間を7日間と短めにしています。
jobs:
storybook-vrt:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup mise
uses: jdx/mise-action@v2
with:
install: true
cache: true
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1385567519
- name: Get Playwright Version
run: |
PLAYWRIGHT_VERSION=$(bun pm ls | grep @playwright | sed 's/.*@//')
echo "Playwright v$PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
- name: Restore Cache VRT snapshots
uses: actions/cache/restore@v4
id: storybook-vrt-cache
with:
path: __snapshots__
key: storybook-vrt-snapshots-${{ github.sha }}-${{ github.run_id }}
restore-keys: |
storybook-vrt-snapshots-
- name: Serve Storybook and run tests
run: |
bunx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"bun preview-storybook" \
"bunx wait-on tcp:127.0.0.1:6006 && bun playwright test -c .storybook/playwright.config.ts .storybook/"
- name: Store Artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: vrt-report
path: playwright-report/
retention-days: 7
次にテストレポートをデプロイするジョブを作ります。
これは以下のブログ記事を参考にしました。
空っぽの gh-pages
ブランチを用意しておいて、そこにテストレポートをpushし、GitHub Pagesで公開するという流れです。
記事内では触れられていませんが、permissions
の設定がないとpushできないので注意が必要です。
# レポートを GitHub Pages にデプロイ
# https://ysfaran.github.io/blog/2022/09/02/playwright-gh-action-gh-pages/#publish-html-report-to-github-pages
deploy-report:
name: Deploy VRT Report
permissions:
contents: write
pull-requests: write
# テスト失敗時のみ
if: failure()
needs: storybook-vrt
runs-on: ubuntu-latest
continue-on-error: true
concurrency:
group: ${{ github.ref_name }}
cancel-in-progress: true
env:
HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Download zipped HTML report
uses: actions/download-artifact@v4
with:
name: vrt-report
path: ${{ env.HTML_REPORT_URL_PATH }}
- name: Push HTML Report
timeout-minutes: 3
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
# this is necessary when this job is running at least twice at the same time (e.g. through two pushes at the same time)
run: |
git add .
git commit -m "🍱 VRTのレポートを追加 (${{ github.run_id }} / attempt: ${{ github.run_attempt }})"
max_attempts=100
attempt=1
while [ $attempt -le $max_attempts ]; do
if git pull --rebase; then
if git push; then
echo "デプロイに成功"
exit 0
else
echo "pushに失敗"
fi
else
echo "rebaseに失敗"
fi
attempt=$((attempt + 1))
echo "再試行: $attempt / $max_attempts"
done
echo "最大試行回数($max_attempts)に達しました"
exit 1
PRにテストレポートのURLをコメントするステップを追加すれば完成です。
以下の記事を参考にしました。
- name: Create comment
run: |
{
echo "COMMENT_BODY<<EOF"
echo "## 🚨 VRTが失敗しました"
echo "テストレポートを確認してください!"
echo "> https://yondako.github.io/yondako/$HTML_REPORT_URL_PATH"
echo EOF
} >> "$GITHUB_ENV"
- name: Comment to PR
# コメントが無い場合 --edit-last が失敗するので、失敗したらコメントを追加する
# 参考: https://blog.omuomugin.com/posts/2024-06-03/
run: gh pr comment ${{ github.ref_name }} --body "${{ env.COMMENT_BODY }}" --edit-last || gh pr comment ${{ github.ref_name }} --body "${{ env.COMMENT_BODY }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
少し長いですが、全体はこのようになります。
storybook-vrt-pr.yml(全体)
name: Storybook VRT
on:
push:
branches-ignore: [main, develop, gh-pages]
jobs:
storybook-vrt:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup mise
uses: jdx/mise-action@v2
with:
install: true
cache: true
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1385567519
- name: Get Playwright Version
run: |
PLAYWRIGHT_VERSION=$(bun pm ls | grep @playwright | sed 's/.*@//')
echo "Playwright v$PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
- name: Restore Cache VRT snapshots
uses: actions/cache/restore@v4
id: storybook-vrt-cache
with:
path: __snapshots__
key: storybook-vrt-snapshots-${{ github.sha }}-${{ github.run_id }}
restore-keys: |
storybook-vrt-snapshots-
- name: Serve Storybook and run tests
run: |
bunx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"bun preview-storybook" \
"bunx wait-on tcp:127.0.0.1:6006 && bun playwright test -c .storybook/playwright.config.ts .storybook/"
- name: Store Artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: vrt-report
path: playwright-report/
retention-days: 7
# レポートを GitHub Pages にデプロイ
# https://ysfaran.github.io/blog/2022/09/02/playwright-gh-action-gh-pages/#publish-html-report-to-github-pages
deploy-report:
name: Deploy VRT Report
permissions:
contents: write
pull-requests: write
# テスト失敗時のみ
if: failure()
needs: storybook-vrt
runs-on: ubuntu-latest
continue-on-error: true
concurrency:
group: ${{ github.ref_name }}
cancel-in-progress: true
env:
HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Download zipped HTML report
uses: actions/download-artifact@v4
with:
name: vrt-report
path: ${{ env.HTML_REPORT_URL_PATH }}
- name: Push HTML Report
timeout-minutes: 3
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
# this is necessary when this job is running at least twice at the same time (e.g. through two pushes at the same time)
run: |
git add .
git commit -m "🍱 VRTのレポートを追加 (${{ github.run_id }} / attempt: ${{ github.run_attempt }})"
max_attempts=100
attempt=1
while [ $attempt -le $max_attempts ]; do
if git pull --rebase; then
if git push; then
echo "デプロイに成功"
exit 0
else
echo "pushに失敗"
fi
else
echo "rebaseに失敗"
fi
attempt=$((attempt + 1))
echo "再試行: $attempt / $max_attempts"
done
echo "最大試行回数($max_attempts)に達しました"
exit 1
- name: Create comment
run: |
{
echo "COMMENT_BODY<<EOF"
echo "## 🚨 VRTが失敗しました"
echo "テストレポートを確認してください!"
echo "> https://yondako.github.io/yondako/$HTML_REPORT_URL_PATH"
echo EOF
} >> "$GITHUB_ENV"
- name: Comment to PR
# コメントが無い場合 --edit-last が失敗するので、失敗したらコメントを追加する
# 参考: https://blog.omuomugin.com/posts/2024-06-03/
run: gh pr comment ${{ github.ref_name }} --body "${{ env.COMMENT_BODY }}" --edit-last || gh pr comment ${{ github.ref_name }} --body "${{ env.COMMENT_BODY }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ブランチの削除時にテストレポートを削除するワークフロー
PRをマージした後にテストレポートを削除するワークフローも用意しておきます。
これは先程のブログで紹介されているものをほぼそのまま使いました 🙏
storybook-vrt-delete.yml(全体)
name: Delete Storybook VRT Report
on:
delete:
branches-ignore: [main, develop, gh-pages]
concurrency:
group: ${{ github.event.ref }}
cancel-in-progress: true
jobs:
delete-storybook-vrt-report:
name: Delete Storybook VRT Report
permissions:
contents: write
runs-on: ubuntu-latest
env:
BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Set Git User
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Check for workflow reports
run: |
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
else
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
fi
- name: Delete reports from repo for branch
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
timeout-minutes: 3
run: |
cd $BRANCH_REPORTS_DIR/..
rm -rf ${{ github.event.ref }}
git add .
git commit -m "🔥 ${{ github.event.ref }}のVRTのレポートを削除"
max_attempts=100
attempt=1
while [ $attempt -le $max_attempts ]; do
if git pull --rebase; then
if git push; then
echo "デプロイに成功"
exit 0
else
echo "pushに失敗"
fi
else
echo "rebaseに失敗"
fi
attempt=$((attempt + 1))
echo "再試行: $attempt / $max_attempts"
done
echo "最大試行回数($max_attempts)に達しました"
exit 1
完成形
PRを作成するとGitHub Actionsが実行されて、
VRTが失敗すると、PRにコメントが追加されます。
URLにアクセスすると、テストレポートが表示されて、
スライダーでスナップショットの比較もできます 🙌
まとめ
GitHub Pagesで公開することで、VRTのテストレポートを見るためにArtifactsをダウンロードする手間が省け、より良い開発体験を得られるようになりました!
Publicリポジトリであればコストもかからないので、個人開発で使う分にはちょうど良いかなと思います。
今回のワークフローは以下のリポジトリで使用しています。

ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion