はじめに
この記事では、Vitest のブラウザモードを利用してコンポーネントテストを行う手順を記載します。
Vitest のブラウザモードは、従来の jsdom や happy-dom などのシミュレーション環境ではなく、実際のブラウザ上でテストを実行できる機能です。これにより、より正確なブラウザ動作の検証が可能になります。
開発環境
開発環境は以下の通りです。
- Windows 11
- VSCode
- Vite 7.3.1
- React 19.2.3
- TypeScript 5.9.3
- Vitest 4.0.17
- @vitest/browser-playwright 4.0.17
プロジェクト作成
まずは Vite で React プロジェクトを作成します。
npm create vite@latest vitest-browser-demo -- --template react-ts
cd vitest-browser-demo
npm install
Vitest ブラウザモードのインストール
セットアップコマンドを使用すると、必要な依存関係のインストールとブラウザ設定を自動で行えます。
npx vitest init browser
手動でインストールする場合は、Vitest とブラウザモード関連のパッケージをインストールします。
npm install -D vitest @vitest/browser-playwright vitest-browser-react
なぜ Vitest なのに Playwright 関連パッケージをインストールするのか
Vitest ブラウザモードは「実際のブラウザ上でテストを実行する機能」ですが、Vitest 自体にはブラウザを起動・制御する機能がありません。そこで、ブラウザを制御するための外部ツールとして Playwright を利用します。
┌─────────────────────────────────────────────────────┐
│ Vitest ブラウザモード │
│ テストの実行・アサーション・レポートを担当 │
└─────────────────┬───────────────────────────────────┘
│ ブラウザの起動・制御を依頼
▼
┌─────────────────────────────────────────────────────┐
│ @vitest/browser-playwright(プロバイダー) │
│ Playwright をラップし、ブラウザへの命令送信を担当 │
└─────────────────┬───────────────────────────────────┘
│ 「このボタンをクリックしろ」等の命令
▼
┌─────────────────────────────────────────────────────┐
│ Chromium(実際のブラウザ) │
│ 命令を受けて実際に動作を実行 │
└─────────────────────────────────────────────────────┘
つまり、Vitest はテストフレームワーク、@vitest/browser-playwright はブラウザ制御ライブラリ、Chromium は実際のブラウザという役割分担です。
なお、プロバイダーは Playwright 以外にも WebdriverIO(@vitest/browser-webdriverio)や Preview(@vitest/browser-preview、実験的)から選択できます。
設定ファイルの作成
vite.config.ts に Vitest の設定を追加します。
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
},
})
この設定では以下を指定しています。
-
browser.enabled: ブラウザモードを有効化 -
browser.provider: 使用するプロバイダー(playwright()関数を呼び出して指定) -
browser.instances: テストを実行するブラウザの種類
セットアップコマンドを使用すると、vitest.browser.config.ts が自動で作成されます。ただ、Vite を使っている場合、vite.config.ts にテストの設定も統合できます。
テスト対象のコンポーネントを実装
次はテスト対象のコンポーネントを実装します。
シンプルなカウンターコンポーネントを作成します。
import { useState } from 'react'
type CounterProps = {
initialCount?: number
}
export const Counter = ({ initialCount = 0 }: CounterProps) => {
const [count, setCount] = useState(initialCount)
return (
<div>
<h2>Counter</h2>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
App.tsx でコンポーネントを使用します。
import { Counter } from './components/Counter'
function App() {
return (
<div>
<h1>Vitest Browser Mode Demo</h1>
<Counter initialCount={0} />
</div>
)
}
export default App
ローカルサーバーを起動して動作確認します。
npm run dev
ブラウザで http://localhost:5173 にアクセスし、カウンターが動作することを確認します。
テストコードを実装
ブラウザモードでのテストコードを実装します。
テストファイルを作成します。
import { expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { Counter } from "./Counter";
test("Counter should render with initial count", async () => {
const screen = await render(<Counter initialCount={5} />);
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 5");
});
test("Counter should increment when clicking Increment button", async () => {
const screen = await render(<Counter initialCount={0} />);
await screen.getByRole("button", { name: "Increment" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 1");
});
test("Counter should decrement when clicking Decrement button", async () => {
const screen = await render(<Counter initialCount={10} />);
await screen.getByRole("button", { name: "Decrement" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 9");
});
test("Counter should reset to 0 when clicking Reset button", async () => {
const screen = await render(<Counter initialCount={100} />);
await screen.getByRole("button", { name: "Reset" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 0");
});
テスト実行
テストを実行します。
npx vitest
成功すると、以下のように表示されます。
DEV v4.0.17 C:/Users/ymori/Documents/GitHub/frontend-tests/vitest-browser-demo
✓ chromium src/components/Counter.test.tsx (4 tests) 362ms
✓ Counter should render with initial count 69ms
✓ Counter should increment when clicking Increment button 163ms
✓ Counter should decrement when clicking Decrement button 69ms
✓ Counter should reset to 0 when clicking Reset button 61ms
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 21:04:20
Duration 3.00s (transform 0ms, setup 0ms, import 147ms, tests 362ms, environment 0ms)
PASS Waiting for file changes...
press h to show help, press q to quit
UI モードでテストを確認
Vitest には UI モードがあり、ブラウザ上でテスト結果を視覚的に確認できます。
なお、UI モードを利用するためには @vitest/ui をインストールとなります。
npm install -D @vitest/ui
npx vitest --ui
ブラウザが開き、テスト結果をインタラクティブに確認できます。
ブラウザモードの主な特徴
1. 実際のブラウザ環境
jsdom などのシミュレーション環境ではなく、実際のブラウザでテストを実行するため、以下のメリットがあります。
- CSS の計算が正確
- ブラウザ固有の API が利用可能
- 実際のユーザー操作に近いテストが可能
2. 複数ブラウザでのテスト
設定で複数のブラウザを指定できます。
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
},
},
})
DEV v4.0.17 C:/Users/ymori/Documents/GitHub/frontend-tests/vitest-browser-demo
UI started at http://localhost:51204/__vitest__/
✓ chromium src/components/Counter.test.tsx (4 tests) 985ms
✓ Counter should increment when clicking Increment button 587ms
✓ webkit src/components/Counter.test.tsx (4 tests) 1283ms
✓ Counter should increment when clicking Increment button 409ms
✓ Counter should decrement when clicking Decrement button 411ms
✓ Counter should reset to 0 when clicking Reset button 314ms
✓ firefox src/components/Counter.test.tsx (4 tests) 1395ms
✓ Counter should increment when clicking Increment button 800ms
Test Files 3 passed (3)
Tests 12 passed (12)
Start at 21:11:44
Duration 13.45s (transform 0ms, setup 0ms, import 854ms, tests 3.66s, environment 0ms)
PASS Waiting for file changes...
press h to show help, press q to quit
3. Locator API
Playwright の Locator API を活用した要素の取得が可能です。
// Role による取得
screen.getByRole('button', { name: 'Submit' })
// TestId による取得
screen.getByTestId('submit-button')
// Text による取得
screen.getByText('Hello World')
// Placeholder による取得
screen.getByPlaceholder('Enter your name')
4. 非同期アサーション
expect.element() を使用することで、要素が表示されるまで自動的に待機します。
// 要素が表示されるまで待機してアサーション
await expect.element(screen.getByTestId('result')).toHaveTextContent('Success')
5. Trace View
Vitest 4.x では Playwright の Trace 機能がサポートされ、テスト実行の詳細な記録を確認できます。DOM スナップショット、ネットワークアクティビティ、スクリーンショットなどが記録され、テストが失敗した際のデバッグに役立ちます。
設定ファイルで有効化します。
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
// トレースを有効化
trace: 'on-first-retry', // 'on' | 'off' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure'
},
},
})
トレースファイルはテストファイルの隣の __traces__ フォルダに保存されます。Playwright Trace Viewer で確認できます。
npx playwright show-trace "path-to-trace-file.zip"
または https://trace.playwright.dev にアクセスしてトレースファイルをアップロードすることもできます。
6. Visual Regression Testing
スクリーンショットベースのビジュアルリグレッションテストが可能です。UI コンポーネントやページのスクリーンショットを撮影し、参照画像と比較して意図しない視覚的変更を検出します。
import { expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { Counter } from "./Counter";
test("Counter should render with initial count", async () => {
const screen = await render(<Counter initialCount={5} />);
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 5");
});
test("Counter should increment when clicking Increment button", async () => {
const screen = await render(<Counter initialCount={0} />);
await screen.getByRole("button", { name: "Increment" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 1");
});
test("Counter should decrement when clicking Decrement button", async () => {
const screen = await render(<Counter initialCount={10} />);
await screen.getByRole("button", { name: "Decrement" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 9");
});
test("Counter should reset to 0 when clicking Reset button", async () => {
const screen = await render(<Counter initialCount={100} />);
await screen.getByRole("button", { name: "Reset" }).click();
await expect
.element(screen.getByTestId("count"))
.toHaveTextContent("Count: 0");
});
初回実行時に参照スクリーンショットが作成され、以降のテストで比較されます。スクリーンショットを更新するには --update フラグを使用します。
npx vitest --update
設定ファイルで比較オプションをカスタマイズできます。
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
expect: {
toMatchScreenshot: {
// pixelmatch の比較オプション
comparatorOptions: {
threshold: 0.2, // 色の差異の許容値(0-1)
allowedMismatchedPixelRatio: 0.01, // 1% のピクセル差異を許容
},
},
},
},
},
})
ページ全体ではなく、特定のコンポーネントをキャプチャすることで誤検出を減らせます。
// ❌ ページ全体をキャプチャ(関係ない変更で失敗しやすい)
await expect(page).toMatchScreenshot()
// ✅ テスト対象のコンポーネントのみキャプチャ
await expect.element(screen.getByTestId('product-card')).toMatchScreenshot()
7. モジュールモック
ブラウザモードでは { spy: true } オプションを使用することで、モジュールのエクスポートを置き換えずにスパイできます。ブラウザの ESM はモジュール名前空間オブジェクトが sealed されているため、Node.js とは異なるアプローチが必要です。
export const login = async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
return response.json()
}
export const logout = async () => {
await fetch('/api/logout', { method: 'POST' })
}
import { expect, test, vi } from 'vitest'
import { render } from 'vitest-browser-react'
import { LoginForm } from '../LoginForm'
// spy: true でモジュールをスパイ(エクスポートを置き換えずに監視)
vi.mock('../api/auth', { spy: true })
// モック対象のモジュールをインポート
import * as auth from '../api/auth'
test('ログインボタンクリックで login 関数が呼ばれる', async () => {
// モック実装を設定
vi.mocked(auth.login).mockResolvedValue({ success: true, token: 'abc123' })
const screen = await render(<LoginForm />)
await screen.getByLabelText('メールアドレス').fill('test@example.com')
await screen.getByLabelText('パスワード').fill('password123')
await screen.getByRole('button', { name: 'ログイン' }).click()
// 関数が正しい引数で呼ばれたことを確認
expect(auth.login).toHaveBeenCalledWith('test@example.com', 'password123')
})
test('ログイン失敗時にエラーメッセージが表示される', async () => {
// エラーを返すモック
vi.mocked(auth.login).mockRejectedValue(new Error('認証エラー'))
const screen = await render(<LoginForm />)
await screen.getByLabelText('メールアドレス').fill('test@example.com')
await screen.getByLabelText('パスワード').fill('wrong-password')
await screen.getByRole('button', { name: 'ログイン' }).click()
await expect.element(screen.getByRole('alert')).toHaveTextContent('認証エラー')
})
8. toBeInViewport マッチャー
要素がビューポート内に表示されているかを確認できます。IntersectionObserver API を使用しています。
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('スクロール後に要素がビューポートに表示される', async () => {
// 要素がビューポート内にあることを確認
await expect.element(page.getByText('Welcome')).toBeInViewport()
// 要素の50%以上がビューポート内にあることを確認
await expect.element(page.getByTestId('hero-section')).toBeInViewport({ ratio: 0.5 })
})
まとめ
この記事では、Vitest ブラウザモードを利用してコンポーネントテストを行う手順を解説しました。
Vitest 4.x では npx vitest init browser による簡単なセットアップが可能になり、ブラウザモードの導入がより容易になりました。また、Trace View や Visual Regression Testing などの新機能が追加され、デバッグ機能が強化されています。
ブラウザモードを使用することで、jsdom などのシミュレーション環境では再現できない、実際のブラウザ環境での正確なテストが可能になります。CSS の計算やブラウザ固有の API を使用するコンポーネントのテストに特に有効です。

