Vinyl

Playwright + GitHub ActionsでVRT環境を作る

はじめに

GitHub Actionsを使用し、PlaywrightでVisual Regression TestをCIのDocker上で実施し、Slack通知まで行う基盤を構築したので、自分用の記録として手順を残しておきます。

環境

  • Vercel
  • Next.js "15.2.1",
  • Playwright ^1.51.0",

環境はとりあえずVercelを使ってますが、複数環境を作れるのであれば何でも問題ないです。

下記完成品のURLです。

GitHub - mdkk11/playwright-vrtContribute to mdkk11/playwright-vrt development by creating an account on GitHub.favicon icongithub.com

PlaywrightでVRTを実装する

Playwright環境をセットアップ

npm init playwright@latest

スクリプトを追加する

テスト実行と、スクリーンショットを撮るだけのスクリプトを追加します。

"playwright": "playwright test",
"playwright:update": "playwright test --update-snapshots",

Playwright.config.ts を編集

BASE_URLを参照するようにします。testDirは自身がテストを置きたい任意のパスを

export default defineConfig({
  testDir: './src/tests/__vrt__', // 任意の好きなpathで
  ...
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
  },

テストを作成する

toHaveScreenshotを使用してスクリーンショットの保存・比較を行います。

const ROUTE = {
  home: () => '/',
  services: () => '/services',
  about: () => '/about',
  contact: () => '/contact',
  auth: {
    signin: () => '/auth/signin',
    register: () => '/auth/register',
  },
} as const;
 
export type TestRoute = {
  name: string;
  path: string;
};
 
export const testRoutes = [
  {
    name: 'Home',
    path: ROUTE.home(),
  },
  {
    name: 'Services',
    path: ROUTE.services(),
  },
  {
    name: 'About',
    path: ROUTE.about(),
  },
  {
    name: 'Contact',
    path: ROUTE.contact(),
  },
  {
    name: 'SingIn',
    path: ROUTE.auth.signin(),
  },
  {
    name: 'Register',
    path: ROUTE.auth.register(),
  },
] as const satisfies TestRoute[];
 
test.describe('Visual Regression Testing', () => {
  for (const route of testRoutes) {
    test(`snapshot test ${route.name}`, async ({ page }) => {
      await page.goto(route.path);
      await expect(page).toHaveScreenshot([route.name, `${route.path === '/' ? 'home' : route.path}.png`], {
        fullPage: true,
      });
    });
  }
});

デプロイする

自分はVervelを使ったのでとりあえずVercelにデプロイする。

Vercel: Build and deploy the best web experiences with the Frontend Cloud – VercelVercel's Frontend Cloud gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web.favicon iconvercel.com

Preview 環境を作成を作成する

Vercelだとデフォルトでmain以外のbranchを作成しpushされるとPreview環境が整備されるので、previewブランチを作成してpushするだけでOKです。

Github Page を作成する

--orphan オプションを使って、親コミットを持たない gh-pages ブランチ作成

git checkout --orphan gh-pages

全部空にする

git rm -rf .

空コミット作成

git commit --allow-empty -m "Setup GitHub Pages"

リポジトリへ push

git push --set-upstream origin gh-pages

以下を参考にしてます。

GitHub Actions上で実行したPlaywrightのレポートをGitHub Pagesで見るfavicon iconzenn.dev

Githubの設定をする

Developer Settingsから Personal Access Token (PAT) を作成する

Github Actionsの bot に権限を与える為に使います。

(本当はGithub Appsトークンの方がいいらしい)

GitHub Appsトークン解体新書:GitHub ActionsからPATを駆逐する技術favicon iconzenn.dev

Repository permissions の ContentsRead and writeにするだけでOK。

SlackでWeb Hook URLを取得

Incoming WebHooks> _Please note, this is a legacy custom integration - an outdated way for teams to integrate with Slack. These integrations lack newer features and they will be deprecated and possibly removed in the favicon iconnotion-axi7538.slack.com

リポジトリ内にある Security の Secrets and vatiavles の Actions の中にある Repository secrets にそれぞれを登録する

VRT を実行する GitHub Actions ワークフローを作成する

name: Playwright VRT
on:
  deployment_status:
jobs:
  playwright:
    if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'preview'
    name: visual-regression-test
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.51.0-noble
      options: --user 1001
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Capture Production Screenshots
        run: npm run playwright:update
        env:
          BASE_URL: // Preview環境URL
      - name: Compare Screenshot To Preview
        run: npm run playwright
        env:
          BASE_URL: ${{ github.event.deployment_status.environment_url }}
      - name: Upload Test Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report
 
  publish_report:
    name: Publish Report
    needs: [playwright]
    if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'preview'
    runs-on: ubuntu-latest
    env:
      HTML_REPORT_URL_PATH: reports/${{ github.run_id }}/${{ github.run_attempt }}
    outputs:
      report_url_path: ${{ env.HTML_REPORT_URL_PATH }}
      test_status: ${{ needs.playwright.result }}
    steps:
      - name: Checkout GitHub Pages Branch
        uses: actions/checkout@v4
        with:
          ref: gh-pages
          token: ${{ secrets.PAT }}
 
      - name: Set Git User
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
 
      - name: Download Test Report
        uses: actions/download-artifact@v4
        with:
          name: playwright-report
          path: ${{ env.HTML_REPORT_URL_PATH }}
 
      - name: Push HTML Report
        run: |
          git add .
          git commit -m "VRT Report: ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
          for i in {1..5}; do
            git pull --rebase
            git push https://${{ secrets.PAT }}@github.com/${{ github.repository }}.git gh-pages && break || sleep 10
          done
 
  notify_slack:
    name: Notify Slack
    if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'preview'
    needs: [publish_report]
    runs-on: ubuntu-latest
    env:
      REPORT_URL_PATH: ${{ needs.publish_report.outputs.report_url_path }}
      TEST_STATUS: ${{ needs.publish_report.outputs.test_status }}
    steps:
      - name: Construct Report URL
        run: echo "REPORT_URL=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ env.REPORT_URL_PATH }}/index.html" >> $GITHUB_ENV
 
      - name: Wait for GitHub Pages Deployment
        run: |
          echo "Waiting for GitHub Pages deployment..."
          for i in {1..10}; do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$REPORT_URL")
            if [ "$STATUS" -eq 200 ]; then
              echo "GitHub Pages is live!"
              break
            fi
            echo "Not ready yet. Retrying in 10 seconds..."
            sleep 10
          done
 
      - name: Determine Status Message
        run: |
          if [ "$TEST_STATUS" == "success" ]; then
            echo "STATUS_EMOJI=✅" >> $GITHUB_ENV
            echo "STATUS_TEXT=成功" >> $GITHUB_ENV
          else
            echo "STATUS_EMOJI=⚠️" >> $GITHUB_ENV
            echo "STATUS_TEXT=失敗あり" >> $GITHUB_ENV
          fi
 
      - name: Send Slack Notification
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          curl -X POST -H 'Content-type: application/json' \
          --data "{
            \"blocks\": [
              {
                \"type\": \"header\",
                \"text\": {
                  \"type\": \"plain_text\",
                  \"text\": \"$STATUS_EMOJI VRT Report 生成完了 - テスト: $STATUS_TEXT\"
                }
              },
              {
                \"type\": \"section\",
                \"text\": {
                  \"type\": \"mrkdwn\",
                  \"text\": \"*リポジトリ:* ${{ github.repositoryUrl }}\\n*デプロイURL:* ${{ github.event.deployment_status.environment_url }}\"
                }
              },
              {
                \"type\": \"actions\",
                \"elements\": [
                  {
                    \"type\": \"button\",
                    \"text\": {
                      \"type\": \"plain_text\",
                      \"text\": \"レポートを表示\"
                    },
                    \"url\": \"$REPORT_URL\",
                    \"style\": \"primary\"
                  },
                  {
                    \"type\": \"button\",
                    \"text\": {
                      \"type\": \"plain_text\",
                      \"text\": \"GitHub Actionsの結果を表示\"
                    },
                    \"url\": \"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
                  }
                ]
              }
            ]
          }" \
          "$SLACK_WEBHOOK_URL" || echo "Failed to send Slack notification"

ワークフローのトリガー設定

deployment_status イベントが発生した際にワークフローが起動します。

on:
  deployment_status:

VRT実行 (playwright)

条件分岐

  • デプロイメントが成功 (state == 'success')
  • 環境が preview である場合にのみ実行されます。
if: github.event_name == 'deployment_status' &&
    github.event.deployment_status.state == 'success' &&
    github.event.deployment.environment == 'preview'

実行環境

Playwright公式が準備してくれている環境をそのまま利用

Docker | PlaywrightIntroductionfavicon iconplaywright.dev

runs-on: ubuntu-latest
container:
  image: mcr.microsoft.com/playwright:v1.51.0-noble

本番環境に対してスクリーンショットの取得

playwright:update スクリプトで本番環境(固定URL)のスクリーンショットを取得し、基準画像として保存します。

- name: Capture Production Screenshots
  run: npm run playwright:update
  env:
    BASE_URL: // 本番環境URL

Preview環境との比較

デプロイされたプレビュー環境のURLを動的に取得し、基準画像との差分を検出します。

- name: Compare Screenshot To Preview
  run: npm run playwright
  env:
    BASE_URL: ${{ github.event.deployment_status.environment_url }}

テストレポートのアップロード

テスト結果(playwright-report)をアーティファクトとして保存します。

always() でテスト失敗時も実行します。

- name: Upload Test Report
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: playwright-report

レポートのGitHub Pages公開 (publish_report)

環境変数と出力を定義

後続のJobで使用するレポートURLパスとテスト結果ステータスを定義します。

env:
  HTML_REPORT_URL_PATH: reports/${{ github.run_id }}/${{ github.run_attempt }}
outputs:
  report_url_path: ${{ env.HTML_REPORT_URL_PATH }}
  test_status: ${{ needs.playwright.result }}

GitHub Pagesブランチのチェックアウト

uses: actions/checkout@v4
with:
ref: gh-pages
token: ${{ secrets.PAT }}

gh-pages ブランチを操作するために、先ほど作成した PATを使用します。

Slack通知 (notify_slack)

レポートURLの構築

GitHub PagesのURLを環境変数に設定します。

echo "REPORT_URL=..." >> $GITHUB_ENV

GitHub Pagesのデプロイ待ち

レポートがアクセス可能になるまで最大10回(100秒)待機します。

curl -s -o /dev/null -w "%{http_code}" "$REPORT_URL"

テスト結果に応じたメッセージ生成

if [ "$TEST_STATUS" == "success" ]; then ...

Slack通知の送信

SlackのIncoming Webhookを使用して、テスト結果を送信します。

curl -X POST -H 'Content-type: application/json' ...

全体のフローを整理

  1. Playwright設定
    • toHaveScreenshotでスクリーンショット比較を実装し、基準画像を管理。
    • 本番環境(固定URL)とPreview環境(動的URL)の差分を検出。
  2. デプロイ & 環境構築
    • Vercelで本番/Preview環境を自動作成。
    • gh-pagesブランチをGitHub Pagesとして設定し、テストレポートを公開。
  3. GitHub Actions連携
    • Previewデプロイ成功時にワークフローをトリガー。
    • PlaywrightでVRTを実行し、結果をアーティファクトとして保存。
  4. レポート & 通知
    • テスト結果をGitHub Pagesでホスティング。
    • Slackに差分検出結果とレポートリンクを通知(成功/失敗ステータス付き)。

Vercelの時だけ気をつけるポイント

Preview環境にアクセス時の認証を突破する

  • settings→deployment-protection
  • Vercel Authenticationをオフにする(本当は認証プロセスを挟み込む方がベターだが今回はめんどくさいので)

Preview環境のVercel ToolbarをOFFにする

  • Preview環境に対してスクリーンショットを撮っているため、このToolbarのせいで差分がでてしまう為

Managing the visibility of the Vercel ToolbarLearn how to enable or disable the Vercel Toolbar for your team, project, and session.favicon iconvercel.com

ブランチ制御を追加する

  • gh-pagesブランチがpushされる度にVercelでデプロイしようとしてしまうので無効にする必要がある

まとめ

差分が意図したものであるかどうか、の判断ができるレベルの最低限の実装はできました。

しかし実運用だとページ数が増えるに連れてテストの量が肥大化してくるので差分検出で必要な箇所だけ行う仕組みや、PullRequestのワークフローにいい感じに組み込んだりと、実際の開発フローにどういう風に組み込むかはまだ色々と考えないといけないところがありそうです。

参考

Playwright で一番小さく始める VRT と、次のステップの選択肢2024/02/21 ビジュアルリグレッションテストツール4選!ユーザーが語る各ツールのメリット で発表したスライドです。 https://trident-qa.connpass.com/event/308664/ 参照したURL - https://playwright.dev/docs…favicon iconspeakerdeck.com

PR TIMESにおけるPlaywrightを用いたVisual Regression Testこんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。 昨年、I…favicon icondevelopers.prtimes.jp