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 単体でビジュアル回帰テストを実行できるようになりました。
主な特徴
- ネイティブサポート: 外部ライブラリなしで VRT を実行可能
- Playwright互換: Playwright の手法を参考にした安定した実装
- 柔軟な比較: Pixelmatch を使用した高精度な画像比較
- 拡張可能: カスタムコンパレーターやコーデックの追加が可能
動作の仕組み
toMatchScreenshot
は以下の 6 つの結果パターンを持ちます:
- Pass: スクリーンショットが一致
- Fail: スクリーンショットが不一致
- New: 新規スクリーンショット(基準画像なし)
- Updated: スクリーンショットを更新
- Deleted: スクリーンショットを削除
- 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 機能が提供されることが期待されます。今のうちから基本的な使い方をマスターし、チームの品質保証プロセスを強化していきましょう。
参考文献
📖 公式ドキュメント
- Vitest Browser Mode Visual Regression Testing
- Vitest Browser Mode Guide
- GitHub PR #8041 - feat(browser): introduce toMatchScreenshot
📚 関連記事
- Effective Visual Regression Testing for Developers: Vitest vs Playwright
- Visual Regression Testing in Vue with Vitest
- Vitest Browser ModeにVisual Regression Testが来るぞ - Zenn