ブログ記事

Vitest×Playwright Browser Mode実践ガイド2025 - コンポーネントテストの新標準

Vitest Browser ModeとPlaywrightを組み合わせた最新のコンポーネントテスト手法を徹底解説。実際のブラウザ環境でのテスト実行、パフォーマンス最適化、CI/CD統合、トラブルシューティングまで、現場で使える実践的なノウハウを紹介します。

ツール
Vitest Playwright テスト コンポーネントテスト Browser Mode
Vitest×Playwright Browser Mode実践ガイド2025 - コンポーネントテストの新標準のヒーロー画像

2025 年のフロントエンド開発において、コンポーネントテストの品質が製品の成功を左右する時代になりました。JSDOM や Happy DOM では再現できない複雑なユーザーインタラクション、ブラウザ固有の API、視覚的な状態変化。これらを正確にテストするために、Vitest Browser Mode × Playwrightの組み合わせが新たな標準として注目されています。

本記事では、実際のプロジェクトで直面する課題を解決しながら、Browser Mode の真の力を引き出す方法を徹底的に解説します。

この記事で学べること

  • Vitest Browser Mode と Playwright の統合方法と最適な設定
  • JSDOM テストから Browser Mode への移行戦略
  • 実際のブラウザ環境でのコンポーネントテスト実装
  • パフォーマンス最適化と ci/cd 統合のベストプラクティス
  • よくある問題と解決方法、デバッグテクニック
  • React/Vue/Solid での実践的な実装例

目次

  1. なぜいま Browser Mode が必要なのか
  2. Browser Mode vs 従来のテスト手法
  3. 環境構築と基本設定
  4. 実践的なテストパターン
  5. フレームワーク別実装ガイド
  6. パフォーマンス最適化と ci/cd 統合
  7. トラブルシューティングとデバッグ
  8. 今後の展望とまとめ

なぜいまBrowser Modeが必要なのか

モダンフロントエンドの課題

従来のテスト手法とBrowser Modeの比較
テストの課題 従来の手法の限界 Browser Modeでの解決
複雑なユーザーインタラクション イベントのシミュレーションのみ 実際のブラウザイベントを発火
ブラウザAPI依存 モックが必要 ネイティブAPIをそのまま利用
CSS/レイアウト テスト不可能 実際のレンダリング結果を検証
Web Components Shadow DOM非対応 完全サポート
視覚的回帰 別ツールが必要 スクリーンショット機能内蔵
パフォーマンス 測定困難 実環境での計測が可能

2025年のテストトレンド

Vitest 0.x Browser Mode登場

実験的機能として初登場

Playwright統合強化

並列実行とCDP対応

主要フレームワーク対応

React/Vue/Solidの公式パッケージ提供

エンタープライズ採用加速

大手企業での採用事例増加

デファクトスタンダード化

コンポーネントテストの新標準へ

Browser Mode vs 従来のテスト手法

テスト手法の比較

テスト手法の位置づけ

チャートを読み込み中...

いつBrowser Modeを選ぶべきか

Browser Mode選択の判断基準

✅ ユーザーインタラクションが重要なコンポーネント ✅ ブラウザ api に依存する機能(File API、Clipboard api 等) ✅ css アニメーションやトランジション ✅ レスポンシブデザインのテスト ✅ web Components やカスタムエレメント ❌ 純粋なロジックのテスト(通常の Vitest で十分) ❌ シンプルな表示のみのコンポーネント

環境構築と基本設定

ステップ1:必要なパッケージのインストール

# Vitest と Browser Mode関連パッケージ
npm install -D vitest @vitest/browser

# Playwright provider
npm install -D playwright @vitest/browser-driver-playwright

# フレームワーク別パッケージ(必要に応じて)
npm install -D @vitest/browser-react    # React
npm install -D @vitest/browser-vue      # Vue
npm install -D @vitest/browser-solid    # Solid
# Vitest と Browser Mode関連パッケージ
pnpm add -D vitest @vitest/browser

# Playwright provider
pnpm add -D playwright @vitest/browser-driver-playwright

# フレームワーク別パッケージ(必要に応じて)
pnpm add -D @vitest/browser-react    # React
pnpm add -D @vitest/browser-vue      # Vue
pnpm add -D @vitest/browser-solid    # Solid
# Vitest と Browser Mode関連パッケージ
yarn add -D vitest @vitest/browser

# Playwright provider
yarn add -D playwright @vitest/browser-driver-playwright

# フレームワーク別パッケージ(必要に応じて)
yarn add -D @vitest/browser-react    # React
yarn add -D @vitest/browser-vue      # Vue
yarn add -D @vitest/browser-solid    # Solid
# Vitest と Browser Mode関連パッケージ
bun add -d vitest @vitest/browser

# Playwright provider
bun add -d playwright @vitest/browser-driver-playwright

# フレームワーク別パッケージ(必要に応じて)
bun add -d @vitest/browser-react    # React
bun add -d @vitest/browser-vue      # Vue
bun add -d @vitest/browser-solid    # Solid

ステップ2:Workspace設定(推奨)

// vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // すべてのテストでBrowser Mode browser: { enabled: true, name: 'chromium', provider: 'playwright' } } })
// vitest.workspace.ts import { defineWorkspace } from 'vitest/config' export default defineWorkspace([ { // Unit tests (JSDOM) test: { name: 'unit', include: ['src/**/*.{spec,test}.{js,ts,jsx,tsx}'], exclude: ['src/**/*.browser.*'], environment: 'jsdom' } }, { // Browser tests (Playwright) test: { name: 'browser', include: ['src/**/*.browser.{spec,test}.{js,ts,jsx,tsx}'], browser: { enabled: true, name: 'chromium', provider: 'playwright', headless: true, viewport: { width: 1280, height: 720 } }, // Browser Mode特有の設定 poolOptions: { threads: { singleThread: true // デバッグ時に有用 } } } } ])
単一設定(非推奨)
// vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // すべてのテストでBrowser Mode browser: { enabled: true, name: 'chromium', provider: 'playwright' } } })
Workspace設定(推奨)
// vitest.workspace.ts import { defineWorkspace } from 'vitest/config' export default defineWorkspace([ { // Unit tests (JSDOM) test: { name: 'unit', include: ['src/**/*.{spec,test}.{js,ts,jsx,tsx}'], exclude: ['src/**/*.browser.*'], environment: 'jsdom' } }, { // Browser tests (Playwright) test: { name: 'browser', include: ['src/**/*.browser.{spec,test}.{js,ts,jsx,tsx}'], browser: { enabled: true, name: 'chromium', provider: 'playwright', headless: true, viewport: { width: 1280, height: 720 } }, // Browser Mode特有の設定 poolOptions: { threads: { singleThread: true // デバッグ時に有用 } } } } ])

ステップ3:TypeScript設定

// tsconfig.json
{
  "compilerOptions": {
    "types": [
      "node",
      "vite/client",
      "@vitest/browser/matchers"
    ]
  },
  "include": [
    "src/**/*",
    "vitest.workspace.ts"
  ]
}

ステップ4:package.jsonスクリプト

{
  "scripts": {
    "test": "vitest",
    "test:unit": "vitest --project=unit",
    "test:browser": "vitest --project=browser",
    "test:browser:ui": "vitest --project=browser --ui",
    "test:browser:debug": "vitest --project=browser --no-headless"
  }
}

実践的なテストパターン

基本的なコンポーネントテスト

// Button.browser.test.tsx
import { expect, test } from 'vitest'
import { page, userEvent } from '@vitest/browser/context'
import { render } from '@vitest/browser-react'
import { Button } from './Button'

test('ボタンクリックイベントが正しく発火する', async () => {
  // Arrange
  const handleClick = vi.fn()
  const { getByRole } = render(
    <Button onClick={handleClick}>Click me</Button>
  )
  
  // Act
  const button = getByRole('button')
  await userEvent.click(button)
  
  // Assert
  expect(handleClick).toHaveBeenCalledTimes(1)
})

test('フォーカス状態のスタイルが適用される', async () => {
  // Arrange
  const { getByRole } = render(<Button>Focus me</Button>)
  const button = getByRole('button')
  
  // Act
  await userEvent.tab()
  
  // Assert
  // 実際のブラウザでComputedStyleを検証
  const styles = window.getComputedStyle(button)
  expect(styles.outline).toBe('2px solid blue')
})

高度なインタラクションテスト

// DragAndDrop.browser.test.tsx
test('ドラッグ&ドロップが正しく動作する', async () => {
  const { getByTestId } = render(<DragAndDropList />)
  
  const item1 = getByTestId('item-1')
  const item2 = getByTestId('item-2')
  
  // 実際のドラッグ&ドロップ操作
  await userEvent.dragAndDrop(item1, item2)
  
  // 順序が入れ替わったことを確認
  const items = await page.locator('[data-testid^="item-"]').all()
  expect(await items[0].textContent()).toBe('Item 2')
  expect(await items[1].textContent()).toBe('Item 1')
})
// FileUpload.browser.test.tsx
test('ファイルアップロードが正しく処理される', async () => {
  const { getByLabelText } = render(<FileUpload />)
  
  // テスト用ファイルを作成
  const file = new File(['hello'], 'hello.txt', { 
    type: 'text/plain' 
  })
  
  const input = getByLabelText('ファイルを選択')
  
  // ネイティブのファイル選択をシミュレート
  await userEvent.upload(input, file)
  
  // ファイル情報が表示されることを確認
  await expect.element(page.getByText('hello.txt')).toBeVisible()
  await expect.element(page.getByText('10 bytes')).toBeVisible()
})
// Clipboard.browser.test.tsx
test('クリップボードのコピー&ペーストが動作する', async () => {
  const { getByRole, getByLabelText } = render(<ClipboardDemo />)
  
  // コピー元のテキスト
  const source = getByTestId('source')
  await userEvent.fill(source, 'Hello, Browser Mode!')
  
  // コピーボタンをクリック
  await userEvent.click(getByRole('button', { name: 'コピー' }))
  
  // ペースト先にフォーカス
  const target = getByLabelText('ペースト先')
  await userEvent.click(target)
  
  // Ctrl+V でペースト
  await userEvent.keyboard('Control+V')
  
  // ペーストされたことを確認
  expect(await target.inputValue()).toBe('Hello, Browser Mode!')
})

ビジュアルリグレッションテスト

// VisualRegression.browser.test.tsx
import { expect, test } from 'vitest'
import { page } from '@vitest/browser/context'
import { render } from '@vitest/browser-react'
import { Card } from './Card'

test('カードコンポーネントの見た目が変わっていない', async () => {
  // コンポーネントをレンダリング
  render(
    <Card 
      title="テストカード"
      description="これはテスト用のカードです"
      image="/test-image.jpg"
    />
  )
  
  // スクリーンショットを撮影して比較
  await expect.element(page).toMatchScreenshot('card-component.png', {
    maxDiffPixels: 100,
    threshold: 0.2
  })
})

test('ホバー状態のスタイルが正しい', async () => {
  const { getByTestId } = render(<Card hoverable />)
  const card = getByTestId('card')
  
  // ホバー前のスクリーンショット
  await expect.element(card).toMatchScreenshot('card-normal.png')
  
  // ホバー状態
  await userEvent.hover(card)
  await page.waitForTimeout(300) // アニメーション完了待ち
  
  // ホバー後のスクリーンショット
  await expect.element(card).toMatchScreenshot('card-hover.png')
})

フレームワーク別実装ガイド

React実装例

React特有の考慮事項

  • React 18 の Concurrent Features に対応
  • Suspense とエラーバウンダリのテスト
  • カスタムフックのブラウザ環境でのテスト
// useIntersectionObserver.browser.test.tsx
import { renderHook } from '@vitest/browser-react'
import { useIntersectionObserver } from './useIntersectionObserver'

test('要素が画面内に入った時にコールバックが呼ばれる', async () => {
  const callback = vi.fn()
  
  // カスタムフックをレンダリング
  const { result } = renderHook(() => 
    useIntersectionObserver(callback)
  )
  
  // 要素を作成してrefに設定
  const element = document.createElement('div')
  element.style.height = '100px'
  document.body.appendChild(element)
  
  // refに要素を設定
  act(() => {
    result.current.ref.current = element
  })
  
  // スクロールして要素を画面内に
  element.scrollIntoView()
  
  // IntersectionObserverのコールバックを待つ
  await waitFor(() => {
    expect(callback).toHaveBeenCalledWith(true)
  })
})

Vue実装例

<!-- SearchInput.browser.test.ts -->
<script setup lang="ts">
import { expect, test } from 'vitest'
import { page, userEvent } from '@vitest/browser/context'
import { render } from '@vitest/browser-vue'
import SearchInput from './SearchInput.vue'

test('検索入力のデバウンスが正しく動作する', async () => {
  const onSearch = vi.fn()
  
  const { getByPlaceholderText } = render(SearchInput, {
    props: {
      debounceMs: 300,
      onSearch
    }
  })
  
  const input = getByPlaceholderText('検索...')
  
  // 高速に文字を入力
  await userEvent.type(input, 'Vue')
  
  // デバウンス時間前はコールバックが呼ばれない
  expect(onSearch).not.toHaveBeenCalled()
  
  // デバウンス時間経過後
  await page.waitForTimeout(350)
  
  // 最終的な値で1回だけ呼ばれる
  expect(onSearch).toHaveBeenCalledTimes(1)
  expect(onSearch).toHaveBeenCalledWith('Vue')
})
</script>

パフォーマンス最適化とCI/CD統合

パフォーマンス最適化テクニック

並列実行なし 40 %
並列実行あり(Playwright) 85 %
最適化済み設定 95 %
// vitest.config.ts - 最適化設定
export default defineConfig({
  test: {
    browser: {
      provider: 'playwright',
      providerOptions: {
        // 並列実行の設定
        launch: {
          args: ['--disable-blink-features=AutomationControlled']
        }
      }
    },
    // テストの並列実行
    pool: 'threads',
    poolOptions: {
      threads: {
        maxThreads: 4,
        minThreads: 1
      }
    },
    // タイムアウト設定
    testTimeout: 30000,
    hookTimeout: 10000
  }
})

CI/CD統合

# .github/workflows/test.yml
name: Component Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Install Playwright
        run: pnpm exec playwright install --with-deps chromium
      
      - name: Run Browser Tests
        run: pnpm test:browser
        
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: |
            test-results/
            **/*.png
# .gitlab-ci.yml
browser-tests:
  image: mcr.microsoft.com/playwright:v1.42.0-focal
  stage: test
  script:
    - npm ci
    - npm run test:browser
  artifacts:
    when: always
    paths:
      - test-results/
    reports:
      junit: test-results/junit.xml
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      - ~/.cache/ms-playwright/
# .circleci/config.yml
version: 2.1
orbs:
  browser-tools: circleci/browser-tools@1.4

jobs:
  browser-test:
    docker:
      - image: cimg/node:20.0-browsers
    steps:
      - checkout
      - browser-tools/install-chrome
      - browser-tools/install-chromedriver
      
      - restore_cache:
          keys:
            - v1-deps-{{ checksum "package-lock.json" }}
      
      - run: npm ci
      - run: npx playwright install
      - run: npm run test:browser
      
      - store_test_results:
          path: test-results
      - store_artifacts:
          path: test-results

トラブルシューティングとデバッグ

よくある問題と解決方法

よくあるエラーと対処法

1. “Cannot find module ‘@vitest/browser/context’”

# 解決方法
npm install -D @vitest/browser

2. “Timeout waiting for selector”

// タイムアウトを延長
await expect.element(selector).toBeVisible({ timeout: 10000 })

3. “Browser closed unexpectedly”

// ブラウザの起動オプションを調整
providerOptions: {
  launch: {
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  }
}

デバッグテクニック

// デバッグモードでテストを実行
test('デバッグが必要なテスト', async () => {
  // ブレークポイントを設定
  debugger
  
  // ブラウザの開発者ツールでデバッグ
  await page.pause()
  
  // スクリーンショットを撮影
  await page.screenshot({ path: 'debug.png' })
  
  // HTMLを出力
  console.log(await page.content())
})

パフォーマンスプロファイリング

test('パフォーマンステスト', async () => {
  // パフォーマンス計測開始
  await page.evaluate(() => {
    performance.mark('test-start')
  })
  
  // テスト対象の操作
  render(<HeavyComponent />)
  
  // パフォーマンス計測終了
  const metrics = await page.evaluate(() => {
    performance.mark('test-end')
    performance.measure('test-duration', 'test-start', 'test-end')
    
    const measure = performance.getEntriesByName('test-duration')[0]
    return {
      duration: measure.duration,
      memory: (performance as any).memory?.usedJSHeapSize
    }
  })
  
  // パフォーマンス基準をチェック
  expect(metrics.duration).toBeLessThan(1000) // 1秒以内
})

Browser Modeの限界と回避策

Browser Modeの現在の制限事項
制限事項 影響 回避策
アドレスバーなし URL同期テスト不可 location.hrefを直接操作
デバッグの難しさ 開発効率低下 page.pause()とスクリーンショット活用
CSSセレクタ非対応 要素選択の制限 data-testid属性を活用
実験的機能 破壊的変更の可能性 バージョン固定とテスト
CI環境での不安定性 フレーキーテスト リトライとタイムアウト調整

実プロジェクトでの導入事例

Browser Mode を導入してから、本番環境でのバグが 80%減少しました。 特に、複雑なフォームバリデーションやリアルタイムチャートの テストが実際のブラウザ環境で実行できるようになったことが大きいです。

シニアフロントエンドエンジニア 某フィンテック企業

導入効果の実例

バグ検出率向上 95 %
テスト実行時間短縮 80 %
開発者満足度 90 %

今後の展望とまとめ

2025年後半の注目機能

デバッグ機能強化

Chrome DevToolsとの深い統合

AI支援テスト

テストケース自動生成

マルチブラウザ並列実行

異なるブラウザでの同時テスト

ビジュアルAIテスト

AIによる見た目の差分検出

まとめ:Browser Mode導入チェックリスト

導入前の確認事項

✅ JSDOM では不十分なテストケースの特定 ✅ チーム全体での Playwright 知識の共有 ✅ ci/cd 環境の準備とコスト見積もり ✅ 段階的移行計画の策定 ✅ パフォーマンス基準の設定

Vitest Browser Mode と Playwright の組み合わせは、2025年のコンポーネントテストにおける最適解の 1 つです。実際のブラウザ環境でテストを実行することで、ユーザーが体験する挙動を正確に検証でき、品質の高いアプリケーション開発が可能になります。

まずは小さなコンポーネントから始めて、徐々に適用範囲を広げていくアプローチをお勧めします。

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。