•23 min read

Playwright大規模ビジュアルリグレッションテスト: Dockerベースライン、Pixelmatch、不安定なCIからの保護

Playwright大規模ビジュアルリグレッションテスト: Dockerベースライン、Pixelmatch、不安定なCIからの保護

ビジュアルリグレッションテスト(VRT)は、堅牢なCI/CDパイプラインにおいて不可欠な要素であり、デプロイメント全体でUIの一貫性を保証します。しかし、特にTurborepoのようなモノレポ内でVRTを大規模に実装するには、環境の不整合、動的なコンテンツ、ピクセルレベルの比較に内在する不安定性といった特有の課題があります。このガイドでは、これらの問題を軽減し、安定した信頼性の高いVRTシステムを提供するために、Playwright、Docker、およびpixelmatchを使用する本番レベルのアプローチについて詳しく説明します。

Audio Briefing
0:00 / 0:00

ベースラインの問題:環境の決定論

VRTにおける根本的な課題は、一貫性のあるベースラインを確立することです。開発者のmacOSマシンで取得されたスクリーンショットは、フォントレンダリング、アンチエイリアシング、さらにはGPUアクセラレーションの違いにより、LinuxベースのCI環境でキャプチャされたものとは必然的に異なります。これらの不一致は誤検知につながり、VRTシステムへの信頼を損ないます。

解決策は環境の決定論です。ベースラインスクリーンショットがCI/CDパイプラインとまったく同じ環境で生成されることを保証します。Dockerがこの分離を提供します。

Docker化されたベースライン生成

CI環境をミラーリングするDockerイメージを使用して、ベースラインを生成および更新します。このイメージには、Playwrightのブラウザ依存関係を含める必要があります。

まず、VRT環境用にDockerfileを定義します。

# Dockerfile for Playwright VRT baseline generation and CI execution
FROM mcr.microsoft.com/playwright/chromium:v1.45.0-jammy

# Set working directory
WORKDIR /app

# Install pnpm globally
RUN npm install -g pnpm

# Copy package.json and pnpm-lock.yaml for dependency installation
COPY package.json pnpm-lock.yaml ./
# If using Turborepo, copy workspace root package.json and pnpm-workspace.yaml
# COPY pnpm-workspace.yaml ./
# COPY apps/web/package.json apps/web/
# COPY packages/ui/package.json packages/ui/

# Install dependencies
# For Turborepo, you might need to install dependencies at the root
# RUN pnpm install --frozen-lockfile
# Or, if installing within a specific app/package:
# RUN pnpm install --frozen-lockfile --filter=@your-org/web

# Copy the rest of the application code
COPY . .

# Expose any ports if your application needs to run inside the container
# For VRT, typically the app runs externally, and Playwright connects to it.
# EXPOSE 3000

# Define a default command (optional, can be overridden)
CMD ["pnpm", "test:visual"]

このイメージをビルドします。

docker build -t playwright-vrt-env .

次に、このコンテナ内でPlaywrightを実行してベースラインを生成または更新します。

# Example: Running Playwright tests to update baselines
# Assuming your Playwright config points to 'test-results' for diffs
# and 'screenshots' for baselines.
docker run --rm -v "$(pwd):/app" playwright-vrt-env pnpm playwright test --update-snapshots

-v "$(pwd):/app"は、ローカルプロジェクトディレクトリをコンテナにマウントし、Playwrightがベースラインをホストファイルシステムに直接読み書きできるようにします。これにより、ベースラインがGitリポジトリにコミットされます。

Advertisement

VRTのためのPlaywright設定

Playwrightのexpect(page).toHaveScreenshot()は、VRTのコアアサーションです。適切な設定が重要です。

playwright.config.ts

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

// Determine if running in CI
const isCI = !!process.env.CI;

export default defineConfig({
  testDir: './e2e', // Directory where your visual tests reside
  outputDir: './test-results', // Directory for test artifacts (screenshots, videos, traces)
  snapshotDir: './e2e/snapshots', // Directory for baseline screenshots

  fullyParallel: true, // Run tests in parallel
  forbidOnly: isCI, // Forbid .only in CI
  retries: isCI ? 2 : 0, // Retry tests in CI to mitigate flakiness
  workers: process.env.CI ? 1 : undefined, // Limit workers in CI for stability, or use all available

  reporter: 'html', // Use HTML reporter for easy review

  use: {
    baseURL: 'http://localhost:3000', // Base URL of your application under test
    trace: 'on-first-retry', // Capture trace on first retry failure
    screenshot: 'only-on-failure', // Only capture screenshots on failure
    video: 'on-first-retry', // Capture video on first retry failure

    // Playwright's default browser context options
    // Ensure consistent viewport for screenshots
    viewport: { width: 1280, height: 720 },

    // Emulate a consistent color scheme
    colorScheme: 'light',

    // Use a consistent timezone to prevent date/time rendering differences
    timezoneId: 'America/Los_Angeles',

    // Use a consistent locale
    locale: 'en-US',

    // Use a consistent user agent for consistent font rendering
    userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',

    // Playwright's default browser options
    // headless: true, // Always run headless in CI
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    // Add other browsers if needed, but for VRT, consistency is key.
    // Often, one browser (e.g., Chromium) is sufficient for visual baselines.
  ],

  // Web server to run before tests.
  // This assumes your app is a Next.js app running on port 3000.
  webServer: {
    command: 'pnpm --filter=@your-org/web dev', // Command to start your web app
    url: 'http://localhost:3000',
    reuseExistingServer: !isCI, // Reuse server locally, but start fresh in CI
    timeout: 60 * 1000, // 60 seconds timeout for server to start
  },
});

ビジュアルテストの例

// e2e/home.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Home Page Visual Regression', () => {
  test('should match the home page screenshot', async ({ page }) => {
    await page.goto('/');

    // Mask dynamic elements like timestamps, user avatars, or ads
    // This prevents false positives due to content changes.
    await page.locator('.dynamic-timestamp').evaluate(node => node.style.visibility = 'hidden');
    await page.locator('.user-avatar').evaluate(node => node.style.visibility = 'hidden');

    // Wait for fonts to load, if applicable, to prevent FOUT/FOIT issues
    await page.waitForLoadState('networkidle');

    // Take a full page screenshot
    await expect(page).toHaveScreenshot('home-page.png', {
      fullPage: true,
      maxDiffPixelRatio: 0.01, // Allow 1% of pixels to differ
      threshold: 0.1, // Pixelmatch threshold (0-1, lower is stricter)
      // You can also specify a custom diff algorithm if needed,
      // but Playwright's default (based on pixelmatch) is usually sufficient.
      // diffPixels: 100, // Max number of differing pixels
    });
  });

  test('should match the login form screenshot', async ({ page }) => {
    await page.goto('/login');

    // Mask the input field for password, as its content might be dynamic (e.g., autofill)
    await page.locator('input[type="password"]').evaluate(node => node.style.visibility = 'hidden');

    await page.waitForLoadState('networkidle');

    await expect(page).toHaveScreenshot('login-form.png', {
      maxDiffPixelRatio: 0.02, // Slightly more lenient for forms
      threshold: 0.05,
      // You can also specify a specific region to screenshot
      // clip: { x: 0, y: 0, width: 800, height: 600 },
    });
  });
});

動的要素のマスキング

動的なコンテンツ(タイムスタンプ、ユーザー生成コンテンツ、広告、アニメーション)は、VRTの不安定性の主な原因です。Playwrightはいくつかの戦略を提供します。

  1. locator.evaluate(node => node.style.visibility = 'hidden'): 要素を非表示にし、透明にしてレイアウトに影響を与えないようにします。
  2. locator.evaluate(node => node.remove()): DOMから要素を完全に削除します。レイアウトに影響を与える可能性があるため、注意して使用してください。
  3. mask: [page.locator('.dynamic-element')]: toHaveScreenshotにおけるPlaywrightの組み込みマスキングオプションです。これは多くの場合、最もクリーンなアプローチです。
// Using mask option
await expect(page).toHaveScreenshot('home-page.png', {
  mask: [
    page.locator('.dynamic-timestamp'),
    page.locator('.user-avatar'),
  ],
  maxDiffPixelRatio: 0.01,
});

pixelmatchしきい値

Playwrightは、内部で差分検出のためにpixelmatchを使用します。toHaveScreenshotのthresholdオプション(0-1)は感度を制御します。値が低いほど、ピクセルの一致が厳しくなります。maxDiffPixelRatio(0-1)またはmaxDiffPixels(数値)は、テストが失敗するまでの最大許容差を定義します。

  • threshold: ピクセル比較の感度。0.1が一般的な開始点です。
  • maxDiffPixelRatio: 異なることができるピクセルの最大割合。0.01は、1%のピクセルが異なる可能性があることを意味します。
  • maxDiffPixels: 異なることができるピクセルの最大絶対数。

これらの値を試してみてください。厳密な値から始め、許容できる視覚的差異によって正当化される場合にのみ緩めます。

Turborepoとの統合

Turborepoモノレポでは、専用のapps/e2eまたはpackages/e2eワークスペースでPlaywrightテストを定義します。

package.jsonスクリプト(ルート)

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "test:visual": "pnpm --filter=@your-org/e2e test",
    "test:visual:update": "pnpm --filter=@your-org/e2e test --update-snapshots"
  }
}

apps/e2e/package.json

// apps/e2e/package.json
{
  "name": "@your-org/e2e",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:ci": "playwright test --reporter=github --forbid-only --retries=2"
  },
  "devDependencies": {
    "@playwright/test": "^1.45.0",
    "@types/node": "^20.14.9"
  }
}

GitHub Actions CI/CDワークフロー

CIパイプラインは、VRTの実行とアーティファクト管理を調整します。

# .github/workflows/visual-regression.yml
name: Visual Regression Tests

on:
  pull_request:
    branches:
      - main
  push:
    branches:
      - main

jobs:
  visual-regression:
    runs-on: ubuntu-latest
    container:
      # Use the same Docker image as for baseline generation
      image: mcr.microsoft.com/playwright/chromium:v1.45.0-jammy
      options: --user 0:0 # Run as root inside container for permissions

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 8
          run_install: false # We'll run install manually in the container

      - name: Get pnpm store directory
        shell: bash
        run: |
          echo "PNPM_CACHE_DIR=$(pnpm store path)" >> $GITHUB_ENV

      - name: Cache pnpm dependencies
        uses: actions/cache@v4
        with:
          path: ${{ env.PNPM_CACHE_DIR }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build application (if necessary)
        # Replace with your actual build command for the app under test
        run: pnpm --filter=@your-org/web build

      - name: Run Playwright Visual Regression Tests
        run: pnpm --filter=@your-org/e2e test:ci

      - name: Upload Playwright Test Report
        if: always() # Upload even if tests fail
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Upload Playwright Test Results (screenshots, videos, traces)
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-test-results
          path: test-results/
          retention-days: 30

このワークフローは次のことを行います。

  1. mcr.microsoft.com/playwright/chromium Dockerイメージを使用し、環境の一貫性を確保します。
  2. 高速な実行のためにpnpm依存関係をキャッシュします。
  3. アプリケーションをビルドします。
  4. Playwrightテストを実行します。
  5. HTMLレポートと生成された差分画像/ビデオをアーティファクトとしてアップロードします。これらのアーティファクトは、失敗のレビューに不可欠です。
Advertisement

本番環境での注意点とトラブルシューティング

1. フォントレンダリングの違い(誤検知)

症状: VRTがCIで一貫して失敗するが、ローカルではパスし、差分には微妙なフォントのバリエーションが表示される。 原因: 異なるオペレーティングシステム(macOS vs. Linux)や、異なるブラウザバージョン/ビルドは、フォントヒンティング、アンチエイリアシングアルゴリズム、利用可能なシステムフォントの違いにより、フォントのレンダリングが異なります。 修正:

  • Docker化されたベースライン(主要な修正): CIで実行される同じDockerコンテナ環境内で、すべてのベースラインを生成および更新します。これが最も堅牢なソリューションです。
  • 一貫したフォントスタック: CSSが、一貫してロードされるWebフォント(例:Google Fonts)またはプラットフォーム間で同様にレンダリングされる広く利用可能なシステムフォント(例:system-ui)を優先する、一貫したフォントスタックを使用していることを確認します。
  • font-display: optional: Webフォントの場合、フォントが異なるタイミングでロードされた場合に視覚的な違いを引き起こす可能性のあるレイアウトシフト(FOIT/FOUT)を防ぐために、font-display: optionalを検討してください。
  • page.waitForLoadState('networkidle'): スクリーンショットを撮る前に、フォントファイルを含むすべてのネットワークリクエストが完了していることを確認します。

2. 動的コンテンツの不安定性

症状: タイムスタンプ、ユーザーアバター、広告バナー、またはデータ駆動型コンポーネントの変更により、VRTが断続的に失敗する。 原因: UI要素は本質的に動的であり、ピクセルパーフェクトな一貫性を意図していません。 修正:

  • マスキング: 比較中に特定の要素を無視するために、toHaveScreenshotでmask: [locator]を使用します。
  • 非表示/削除: より積極的なマスキングにはlocator.evaluate(node => node.style.visibility = 'hidden')またはnode.remove()を使用します。
  • データのモック: データ駆動型コンポーネントの場合、テスト中に一貫したデータがレンダリングされるようにAPIレスポンスをモックします。Playwrightのpage.route()はこれに優れています。
// Example of mocking API response
await page.route('**/api/users/*', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ name: 'Test User', avatar: 'mock-avatar.png' }),
  });
});
await page.goto('/profile');
await expect(page).toHaveScreenshot('profile-page.png');

3. レイアウトシフト/競合状態

症状: スクリーンショットに要素がわずかに異なる位置に表示されたり、コンテンツが部分的にロードされたりする。 原因: 非同期操作(画像の読み込み、JavaScriptの実行、アニメーション)が異なるタイミングで完了し、スクリーンショットが撮られたときに不安定なDOM状態につながる。 修正:

  • page.waitForLoadState('networkidle'): 500ミリ秒以上、ネットワーク接続が0〜2以下になるまで待機します。
  • page.waitForSelector(selector, { state: 'visible' }): 重要な要素が可視になるまで明示的に待機します。
  • page.waitForTimeout(ms): 最後の手段ですが、複雑なアニメーションやトランジションには必要な場合があります。控えめに使用してください。
  • アニメーションの無効化: アプリケーションのテスト環境で、CSSトランジションとアニメーションを無効にします。
/* In your app's test CSS or a global style */
body.test-env * {
  transition: none !important;
  animation: none !important;
}

4. 大規模な差分アーティファクトとCIストレージ制限

症状: GitHub Actionsの実行がアーティファクトストレージ制限を超過して失敗したり、差分が大きすぎてレビューが困難になったりする。 原因: Playwrightは、すべての失敗に対してフルページのスクリーンショット、差分画像、場合によってはビデオを生成します。 修正:

  • screenshot: 'only-on-failure': テストが失敗した場合にのみスクリーンショットをキャプチャするようにPlaywrightを設定します。
  • maxDiffPixelRatio / threshold: これらの値を調整します。わずかに高い許容値は、「誤検知」の差分の数を減らし、アーティファクトの生成を減らすことができます。
  • ターゲットを絞ったスクリーンショット: fullPage: trueの代わりに、clipまたはlocatorをターゲットにして、特定のコンポーネントまたは領域のスクリーンショットを撮ります。
  • アーティファクトの保持: upload-artifactのretention-daysを適切な値(例:7〜30日)に設定して、古いアーティファクトを自動的に削除します。

アーキテクチャ比較:ベースライン生成

機能ローカルベースライン(開発マシン)Docker化されたベースライン(CI/専用環境)
一貫性低(OS、ブラウザ、フォントレンダリングの違い)高(環境的に決定論的)
セットアップの複雑さ低(playwright test --update-snapshotsを実行するだけ)中(Dockerfile、CI統合)
信頼性低(頻繁な誤検知)高(環境差分を最小限に抑える)
メンテナンス高(環境ドリフトによる頻繁なベースライン更新)低(ターゲット環境で生成されればベースラインは安定)
スケーラビリティ劣る(開発マシンは様々)良好(すべてのCIエージェントで一貫)
推奨用途小規模な個人プロジェクト、初期探索本番レベルのアプリケーション、モノレポ、チーム

よくある質問

Q1: VRTでレスポンシブデザインをどのように扱いますか?

A1: 異なるビューポート用に個別のPlaywrightプロジェクトまたはテストファイルを作成します。playwright.config.tsで、異なるviewport設定を持つ複数のプロジェクトを定義します。

// playwright.config.ts
projects: [
  {
    name: 'chromium-desktop',
    use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 720 } },
  },
  {
    name: 'chromium-mobile',
    use: { ...devices['Pixel 5'], viewport: { width: 390, height: 844 } }, // Example mobile viewport
  },
],

次に、playwright test --project=chromium-desktopまたはplaywright test --project=chromium-mobileを実行します。

Q2: Dockerとマスキングを使用しても、VRTテストがまだ不安定です。他に確認できることはありますか?

A2:

  1. ネットワークレイテンシ: テスト対象のアプリケーションが完全にロードされていることを確認します。重要な要素にはpage.waitForLoadState('networkidle')と特定のpage.waitForSelector()呼び出しを使用します。
  2. アニメーション/トランジション: テスト環境で、すべてのCSSアニメーションとトランジションを明示的に無効にします。微妙なアニメーションでもピクセルシフトを引き起こす可能性があります。
  3. サードパーティスクリプト: アプリケーションがサードパーティスクリプト(分析、広告、チャットウィジェット)をロードする場合、これらは非決定論的な要素を導入する可能性があります。既知のドメインに対してはpage.route('**/*.js', route => route.abort())を使用してVRT中にブロックするか、その動作をモックすることを検討してください。
  4. ブラウザバージョン: Dockerイメージ内のPlaywrightブラウザバージョンが、ローカルでPlaywrightが使用するバージョンと一致していることを確認します(まだローカルでデバッグしている場合)。PlaywrightのDockerイメージは特定のブラウザバージョンに結び付けられています。

Q3: CIで視覚的な差分を効果的にレビューするにはどうすればよいですか?

A3:

  1. GitHub Actionsアーティファクト: CIワークフローのupload-artifactステップにより、playwright-reportおよびtest-resultsディレクトリをダウンロードできるようになります。
  2. Playwright HTMLレポーター: playwright-reportアーティファクトをダウンロードします。解凍し、ブラウザでindex.htmlを開きます。このレポートは、ベースライン、実際の画像、差分画像を並べて明確に表示します。
  3. 専用VRTツール: 非常に大規模なプロジェクトの場合、専用のVRTプラットフォーム(例:Chromatic、Percy、Applitools)との統合を検討してください。これらのツールは、高度な差分アルゴリズム、変更をレビューするためのUI、承認ワークフローを提供します。ただし、コストと複雑さが増します。

Q4: すべてのプルリクエストでVRTを実行すべきですか?

A4: 重要なアプリケーションの場合は、はい。すべてのPRでVRTを実行すると、意図しない視覚的な変更に関する即時のフィードバックが得られます。非常に大規模なモノレポやビルド時間が長いプロジェクトの場合、次のことを検討できます。

  • 選択的VRT: 特定のUI関連パッケージまたはアプリ内の変更に対してのみVRTを実行します。TurborepoのdependsOnとoutputsは、これを最適化するのに役立ちます。
  • スケジュールされたVRT: PRでのより軽量なスイートに加えて、夜間またはスケジュールに基づいて完全なVRTスイートを実行します。
  • ステージング環境VRT: CIでローカルに起動された開発サーバーに対してではなく、ビルドが成功した後にデプロイされたステージング環境に対してVRTを実行します。これにより、実際にデプロイされたアーティファクトがテストされます。

Q5: maxDiffPixelRatioとthresholdの影響は何ですか?

A5:

  • threshold(Pixelmatch感度): この値(0-1)は、2つのピクセルが「差分」と見なされるためにどれだけ異なる必要があるかを決定します。thresholdが低いほど、非常に微妙な色の違いでもフラグが立てられます。これは差分の質に関するものです。
  • maxDiffPixelRatio(全体的な差分許容値): この値(0-1)は、テストが失敗する前に、画像全体のピクセルの最大割合が異なることができることを表します(threshold感度に基づく)。これは差分の量に関するものです。

通常、thresholdを調整して知覚できない色のシフト(例:0.05から0.1)を無視し、maxDiffPixelRatioを調整して、許容できるわずかなレイアウトのバリエーションや、マスクできなかった避けられない小さな動的要素を許容します(例:0.001から0.02)。厳密な値から始め、許容できる違いが失敗を引き起こす場合にのみ段階的に増やします。

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement