1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Playwright + Storybook で始める VRT 環境構築

1
Posted at

はじめに

フロントエンド開発において「気づかないうちに UI が崩れていた」という経験はないでしょうか。CSS の修正が別のコンポーネントに影響していたり、ライブラリのアップデートで微妙にレイアウトがずれていたり。こうした問題を自動で検知するのが VRT(Visual Regression Testing)です。
VRT はコンポーネントのスクリーンショットを撮影し、以前のベースラインと比較することで視覚的なデグレを検出します。本記事ではPlaywright の toHaveScreenshot() を使って Storybook コンポーネントの VRT 環境を構築する手順を解説します。

使用技術

  • Next.js 15 / React 19 / TypeScript
  • Storybook 10(@storybook/nextjs-vite)
  • Playwright(@playwright/test)
  • Docker(Playwright 公式イメージ)
  • GitHub Actions

VRT とは

VRT はコンポーネントの 見た目をスナップショットとして保存し、変更前後を自動比較 するテスト手法です。

1回目の実行: スクリーンショットを撮影 → ベースラインとして保存
2回目以降: スクリーンショットを撮影 → ベースラインと比較 → 差異があれば失敗

実際にブラウザでレンダリングした結果を比較するため、CSSの変化やフォントのずれも検出できます。

なぜ Storybook と組み合わせるのか

Storybook はコンポーネントをストーリーとして独立した状態で管理しています。各ストーリーは一意の URL(iframeURL)でアクセスできるため、VRT の対象として非常に扱いやすいです。

/iframe.html?id=コンポーネントID--ストーリー名&viewMode=story

この URL に Playwright でアクセスしてスクリーンショットを撮るだけで、コンポーネント単位の VRT が実現できます。

構成技術の選定

toHaveScreenshot() を選んだ理由

Playwright には toHaveScreenshot() というスクリーンショット比較の組み込みメソッドがあります。

await expect(page).toHaveScreenshot('button-primary.png');    

jest-image-snapshot などのサードパーティライブラリを使う方法もありますが、toHaveScreenshot() は以下の点で優れています。

  • Playwright に組み込みのため追加インストール不要
  • アニメーションの自動無効化オプションがある
  • CI との連携が容易
  • HTML レポートで差分を視覚的に確認できる

実装手順

1. プロジェクトのセットアップ

npx create-next-app@latest vrt-test                                                          
cd vrt-test                                                                                                                       
npx storybook@latest init --builder vite

2. Playwright のインストール

npm install -D @playwright/test                                                                                                     
npx playwright install chromium

vitest や jest-image-snapshot は今回不使用です。Playwright standalone に統一します。

3. playwright.config.ts の作成

import { defineConfig, devices } from '@playwright/test';
                                                                                                                                    
export default defineConfig({                                                                                                     
  testDir: './tests/vrt',                                                                                                           
  fullyParallel: true,                                                                                                            
  retries: process.env.CI ? 2 : 0,          
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report', open: 'never' }],                                                                 
    ['list'],                           
  ],                                                                                                                                
                                                                                                                                    
  use: {
    baseURL: 'http://localhost:6006',                                                                                               
    viewport: { width: 1280, height: 720 },                                                                                       
  },                                    
  
  expect: {                                                                                                                         
    toHaveScreenshot: {
      maxDiffPixels: 100,   // 許容するピクセル差異数                                                                               
      animations: 'disabled',                                                                                                     
      caret: 'hide',                    
    },                                      
  },                                                                                                                                
  
  projects: [                                                                                                                       
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },                                                                  
  ],                                                                                                                                
                                                                                                                                          
  // Storybook をテスト実行前に自動起動     
  webServer: {                                                                              command: 'npm run storybook',
    url: 'http://localhost:6006',                                       
    reuseExistingServer: !process.env.CI,                    
    timeout: 120 * 1000,
  },                                                                                                                              
                                        
  snapshotPathTemplate: 'tests/vrt/__snapshots__/{testFilePath}/{arg}{ext}',
});                                                                                                                                 

webServer の設定がポイントです。playwright test 実行時に Storybookを自動で起動し、テスト完了後に停止します。ローカルでは起動中のサーバーを再利用(reuseExistingServer: true)し、CIでは毎回新規起動します。

4. VRT テストファイルの作成

tests/vrt/button.vrt.spec.ts
                                        
import { test, expect } from '@playwright/test';
                                                                                                                                    
const STORYBOOK_ROOT = '#storybook-root';   
                                                                                                                                    
test.describe('Button VRT', () => {                                                                                               
  test('Primary', async ({ page }) => {                                                                                             
    await page.goto('/iframe.html?id=example-button--primary&viewMode=story');
    await page.waitForSelector(STORYBOOK_ROOT);                                                                                     
    await page.waitForLoadState('networkidle');                                                                                   
    await expect(page).toHaveScreenshot('button-primary.png');                                                                      
  });
                                                                                                                                    
  test('Secondary', async ({ page }) => {                                                                                         
    await page.goto('/iframe.html?id=example-button--secondary&viewMode=story');                                                    
    await page.waitForSelector(STORYBOOK_ROOT);                                                                                     
    await page.waitForLoadState('networkidle');
    await expect(page).toHaveScreenshot('button-secondary.png');                                                                    
  });                                                                                                                             
});

storybook-rootの表示を待ってからnetworkidleを確認することで、コンポーネントが完全にレンダリングされた状態でスクリーンショットを撮影できます。インタラクションが必要なストーリー(ログイン後の状態など)は Playwright で操作を再現します。

test('LoggedIn(ログインアクション後)', async ({ page }) => {                                                                      
  await page.goto('/iframe.html?id=example-page--logged-out&viewMode=story');                                                       
  await page.waitForSelector(STORYBOOK_ROOT);
  await page.waitForLoadState('networkidle');                                                                                       
  await page.getByRole('button', { name: /Log in/i }).click();                                                                      
  await page.waitForSelector('button:text("Log out")');                                                                             
  await expect(page).toHaveScreenshot('page-logged-in.png');                                                                        
});                                        

5. ベースラインスナップショットの生成

npx playwright test --update-snapshots

tests/vrt/_snapshots_/ 配下に PNG ファイルが生成されます。これをリポジトリにコミットしてベースラインとして管理します。


ハマりどころ:macOS と Linux の環境差異問題

問題

ローカル(macOS)でスナップショットを生成して CI(Linux)で比較すると、意図しない変更がないにもかかわらず常にテストが失敗します。
原因は OS ごとにフォントのアンチエイリアス処理が異なるためです。同じコンポーネントでも macOS と Linux ではピクセル単位で微妙に見た目が違います。

解決策:Docker で環境を統一する

Playwright 公式の Linux Docker イメージを使い、ローカルも CI も同じ環境でテストを実行します。

docker-compose.yml
services:                                                                                                                         
  playwright:                                                                                                                       
    image: mcr.microsoft.com/playwright:v1.59.1-jammy                                                                             
    working_dir: /app                   
    volumes:
      - .:/app                                                                                                                      
      - /app/node_modules      # Linux 用 node_modules を分離
      - pnpm_store:/pnpm_store # pnpm ストアをホストと分離                                                                          
    entrypoint:                                                                                                                   
      [                                                                                                                             
        "sh",                                                                                                                       
        "-c",                                                                                                                       
        "npm install -g pnpm && pnpm config set store-dir /pnpm_store && pnpm install --frozen-lockfile && pnpm exec playwright test
 \"$@\"",                                                                                                                           
        "sh",                                                                                                                       
      ]
    environment:                                                                                                                    
      - CI=true                                                                                                                   
                                                                                                                                    
volumes:                                                                                                                          
  pnpm_store: 

注意点

  • node_modules は Docker ボリュームに分離します。macOS 用バイナリと Linux 用バイナリは互換性がないためです
  • pnpm_store も同様にホストと分離し、コンテナ内の変更がホスト側に同期されないようにします
  • Docker イメージのバージョンは @playwright/test のバージョンと一致させる必要があります
package.json にスクリプトを追加:
{                                                                                                                                   
  "scripts": {                                                                                                                      
    "test:vrt": "playwright test",                                                                                                  
    "test:vrt:update": "playwright test --update-snapshots",                                                                      
    "test:vrt:docker": "docker compose run --rm playwright",
    "test:vrt:docker:update": "docker compose run --rm playwright --update-snapshots",                                              
    "test:vrt:report": "playwright show-report"
  }                                                                                                                                 
}                                         

ローカルでのスナップショット生成・比較は必ず Docker 経由で行います。

ベースライン生成(初回・変更承認時)

npm run test:vrt:docker:update

比較テスト実行

npm run test:vrt:docker

CI 構成(GitHub Actions)

.github/workflows/vrt.yml
                                              
name: VRT                               

on:                                                                                                                                 
  push:
    branches: [main]                                                                                                                
  pull_request:                                                                                                                   
    branches: [main]

jobs:
  vrt:
    runs-on: ubuntu-latest
    container:                              
      image: mcr.microsoft.com/playwright:v1.59.1-jammy
      options: --user 1001   # セキュリティのため非rootで実行
                                                                                                                                    
    steps:                              
      - uses: actions/checkout@v4                                                                                                   
                                                                                                                                    
      - uses: pnpm/action-setup@v4      
        with:                                                                                                                       
          version: 9                                                                                                                
 
      - name: Install dependencies                                                                                                  
        run: pnpm install --frozen-lockfile                                                                                       
                                            
      - name: Run VRT tests             
        run: pnpm run test:vrt
        env:                                                                                                                        
          CI: true
                                                                                                                                    
      - name: Upload test report                                                                                                  
        uses: actions/upload-artifact@v4
        if: failure()
        with:                                                                                                                       
          name: playwright-report
          path: playwright-report/                                                                                                  
          retention-days: 30

container に Playwright 公式イメージを指定することで、docker-compose.yml と同じ Linux 環境で CI が実行されます。ブラウザもイメージに含まれているため playwright install は不要です。

--user 1001 について

セキュリティ上の理由で非 root ユーザーで実行しています。npm install -g pnpm はシステムディレクトリへの書き込みが必要なため root 権限が必要ですが、pnpm/action-setup はツールキャッシュに pnpm をインストールするため root 権限不要です。


運用フロー

コンポーネント変更                                                                                                                
      ↓                                       
npm run test:vrt:docker(ローカルで確認)
    ↓
差分なし ──→ PR 作成 → GitHub Actions が自動 VRT 実行                                                                               
差分あり                                    
    ├─ 意図した変更 → npm run test:vrt:docker:update                                                                                
    │             → スナップショット更新をコミット → PR                                                                          
    └─ 意図しない変更 → コード修正して再テスト 

テスト失敗時は npm run test:vrt:report で HTML レポートを開くと、期待値・実際の値・差分ハイライトを視覚的に確認できます


まとめ

VRT は一度構築すれば「気づかない UI のデグレ」を自動で検知し続けてくれます。
コンポーネント数が増えるほど手動確認のコストは上がります。早い段階で VRTを導入しておくことで、リファクタリングやライブラリアップデート時の安心感が大きく変わります。ぜひ導入を検討してみてください。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?