Vitest Browser ModeにVRTが来た!画像比較でUIバグを撲滅する方法

Vitest Browser ModeにVisual Regression Test機能が登場!toMatchScreenshotによるスクリーンショットテストの実装方法から、CI/CD環境での活用まで徹底解説します。

Vitest Browser ModeにVRTが来た!画像比較でUIバグを撲滅する方法

2025 年、フロントエンドテストの世界に大きな変革が訪れました。Vitest Browser Mode に待望の Visual Regression Test(VRT)機能が正式に追加されたのです。この新機能により、UI の意図しない変更を自動的に検出し、品質保証のプロセスを大幅に改善できるようになりました。本記事では、この画期的な機能の詳細と実装方法について徹底的に解説します。

Visual Regression Testとは

Visual Regression Test(VRT)は、アプリケーションの UI が意図せずに変更されていないかを検証するテスト手法です。従来の機能テストがアプリケーションの動作を検証するのに対し、VRT は見た目の変化を検出します。

なぜVRTが重要なのか

フロントエンド開発において、以下のような問題はよく発生します:

  • CSS の変更が予期しない箇所に影響を与える
  • レスポンシブデザインの崩れ
  • フォントやアイコンの表示不具合
  • レイアウトの微妙なずれ

これらの問題は、手動テストでは見逃しやすく、ユーザーからの報告で初めて気づくケースも少なくありません。VRT を導入することで、これらの視覚的な問題を自動的に検出できるようになります。

Vitest Browser Mode VRTの新機能

2025 年 7 月、Vitest の PR #8041 により、Browser Mode に toMatchScreenshot アサーションが追加されました。この機能により、Playwright や Jest-Image-Snapshot に依存せずに、Vitest 単体でビジュアル回帰テストを実行できるようになりました。

主な特徴

  1. ネイティブサポート: 外部ライブラリなしで VRT を実行可能
  2. Playwright互換: Playwright の手法を参考にした安定した実装
  3. 柔軟な比較: Pixelmatch を使用した高精度な画像比較
  4. 拡張可能: カスタムコンパレーターやコーデックの追加が可能

動作の仕組み

toMatchScreenshot は以下の 6 つの結果パターンを持ちます:

  1. Pass: スクリーンショットが一致
  2. Fail: スクリーンショットが不一致
  3. New: 新規スクリーンショット(基準画像なし)
  4. Updated: スクリーンショットを更新
  5. Deleted: スクリーンショットを削除
  6. Retry: 安定するまでリトライ中

環境構築と基本設定

インストール

まず、必要なパッケージをインストールします:

# Bunを使用する場合
bun add -d vitest@latest @vitest/browser playwright

# npmを使用する場合
npm install -D vitest@latest @vitest/browser playwright

Vitest設定ファイル

vitest.config.ts を以下のように設定します:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      instances: [{ browser: 'chromium' }],
      // VRT固有の設定
      viewport: {
        width: 1280,
        height: 720,
      },
    },
    // スクリーンショット保存先
    screenshotDirectory: './tests/__screenshots__',
    // スクリーンショット比較の閾値
    screenshotFailureThreshold: 0.01, // 1%の差異まで許容
  },
});

プロジェクト構造

推奨されるディレクトリ構造:

project/
├── src/
│   └── components/
│       └── Button.vue
├── tests/
│   ├── __screenshots__/
│   │   └── button-chromium-darwin.png
│   └── visual/
│       └── Button.visual.test.ts
└── vitest.config.ts

toMatchScreenshotの使い方

基本的な使用法

import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';

describe('Button Visual Tests', () => {
  it('should match the default button appearance', async () => {
    // ページにナビゲート
    await page.goto('http://localhost:3000/button');

    // ボタン要素を取得
    const button = await page.locator('button.primary');

    // スクリーンショットを比較
    await expect(button).toMatchScreenshot('button-default');
  });
});

オプションの指定

toMatchScreenshot には様々なオプションを指定できます:

await expect(element).toMatchScreenshot('screenshot-name', {
  // スクリーンショットオプション
  fullPage: false,
  clip: { x: 0, y: 0, width: 100, height: 100 },

  // 比較オプション
  threshold: 0.2, // 20%の差異まで許容
  maxDiffPixels: 100, // 最大100ピクセルの差異まで許容

  // リトライオプション
  timeout: 5000,
  animations: 'disabled', // アニメーションを無効化
});

実践的な実装例

Vueコンポーネントのテスト

import { describe, it, expect } from 'vitest';
import { render } from '@vitest/browser/vue';
import { page } from '@vitest/browser/context';
import Button from '@/components/Button.vue';

describe('Button Component Visual Tests', () => {
  it('should render all button variants correctly', async () => {
    const variants = ['primary', 'secondary', 'danger', 'success'];

    for (const variant of variants) {
      const { container } = render(Button, {
        props: { variant },
      });

      await expect(container).toMatchScreenshot(`button-${variant}`);
    }
  });

  it('should handle hover states', async () => {
    const { container } = render(Button, {
      props: { variant: 'primary' },
    });

    const button = container.querySelector('button');

    // ホバー前の状態
    await expect(button).toMatchScreenshot('button-before-hover');

    // ホバー状態
    await button.hover();
    await expect(button).toMatchScreenshot('button-hover');
  });

  it('should handle disabled state', async () => {
    const { container } = render(Button, {
      props: {
        variant: 'primary',
        disabled: true,
      },
    });

    await expect(container).toMatchScreenshot('button-disabled');
  });
});

レスポンシブデザインのテスト

import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';

describe('Responsive Design Tests', () => {
  const viewports = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1920, height: 1080 },
  ];

  for (const viewport of viewports) {
    it(`should render correctly on ${viewport.name}`, async () => {
      await page.setViewportSize({
        width: viewport.width,
        height: viewport.height,
      });

      await page.goto('http://localhost:3000');

      await expect(page).toMatchScreenshot(`homepage-${viewport.name}`);
    });
  }
});

フォーム入力状態のテスト

import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';

describe('Form Visual Tests', () => {
  it('should show validation errors correctly', async () => {
    await page.goto('http://localhost:3000/form');

    // 空のフォームを送信
    await page.click('button[type="submit"]');

    // エラー表示を待つ
    await page.waitForSelector('.error-message');

    // エラー状態のスクリーンショット
    await expect(page.locator('form')).toMatchScreenshot('form-validation-errors');
  });

  it('should show success state', async () => {
    await page.goto('http://localhost:3000/form');

    // フォームに入力
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'password123');

    // 送信
    await page.click('button[type="submit"]');

    // 成功メッセージを待つ
    await page.waitForSelector('.success-message');

    await expect(page.locator('form')).toMatchScreenshot('form-success');
  });
});

CI/CD環境での活用

GitHub Actionsの設定

name: Visual Regression Tests

on:
  pull_request:
    branches: [main]

jobs:
  vrt:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1

      - name: Install dependencies
        run: bun install

      - name: Install Playwright browsers
        run: bunx playwright install chromium

      - name: Run Visual Tests
        run: bun run test:visual

      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: screenshot-diffs
          path: tests/__screenshots__/__diff__/

スクリーンショット更新ワークフロー

name: Update Screenshots

on:
  workflow_dispatch:

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup environment
        uses: oven-sh/setup-bun@v1

      - name: Install dependencies
        run: bun install

      - name: Update screenshots
        run: bun run test:visual -- --update-snapshots

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: 'chore: update visual regression screenshots'
          file_pattern: 'tests/__screenshots__/**/*.png'

ベストプラクティスと注意点

1. 環境の統一

VRT は環境依存性が高いため、以下の点に注意が必要です:

  • OS: Windows、macOS、Linux でフォントレンダリングが異なる
  • ブラウザ: バージョンによってレンダリング結果が変わる可能性
  • 解像度: デバイスピクセル比(DPR)の違いに注意

解決策

  • Dockerコンテナの使用
  • CI 環境での一貫したテスト実行
  • プラットフォーム別のベースライン画像管理

2. Flaky(不安定)なテストへの対処

// アニメーションを無効化
await page.addStyleTag({
  content: `
    *, *::before, *::after {
      animation-duration: 0s !important;
      animation-delay: 0s !important;
      transition-duration: 0s !important;
      transition-delay: 0s !important;
    }
  `,
});

// 動的コンテンツを待つ
await page.waitForLoadState('networkidle');
await page.waitForTimeout(100); // 最後の手段

3. 差分の許容範囲設定

// グローバル設定
export default defineConfig({
  test: {
    screenshotFailureThreshold: 0.02, // 2%の差異まで許容
    screenshotFailureThresholdType: 'percent', // または 'pixel'
  },
});

// テストごとの設定
await expect(element).toMatchScreenshot('name', {
  threshold: 0.05, // このテストのみ5%まで許容
});

4. ベースライン画像の管理

  • Git LFS の使用を検討
  • 定期的なベースライン画像の見直し
  • プルリクエストでの差分確認プロセスの確立

5. テストの範囲を適切に設定

VRT を適用すべき箇所:

  • ✅ 重要な UI コンポーネント
  • ✅ ランディングページ
  • ✅ スタイルガイドのコンポーネント
  • ❌ 頻繁に変更される動的コンテンツ
  • ❌ サードパーティの埋め込みコンテンツ

まとめ

Vitest Browser Mode の Visual Regression Test 機能は、フロントエンドテストに新たな可能性をもたらしました。toMatchScreenshot を使用することで、簡単にビジュアル回帰テストを導入でき、UI の品質を継続的に保証できます。

ただし、VRT は万能ではありません。環境依存性や Flaky test の問題に適切に対処し、チームの開発フローに合わせて段階的に導入することが成功の鍵となります。

今後、Vitest のエコシステムがさらに成熟することで、より安定した高度な VRT 機能が提供されることが期待されます。今のうちから基本的な使い方をマスターし、チームの品質保証プロセスを強化していきましょう。

参考文献

📖 公式ドキュメント

📚 関連記事

🔧 サンプルリポジトリ

BP

BitPluse Team

Building the future of software development, one line at a time.

Keep Learning

Explore more articles and expand your knowledge

View All Articles