2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vitest】ブラウザモードを利用してコンポーネントをテストする

2
Posted at

はじめに

この記事では、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 の設定を追加します。

vite.config.ts
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 にテストの設定も統合できます。

テスト対象のコンポーネントを実装

次はテスト対象のコンポーネントを実装します。

シンプルなカウンターコンポーネントを作成します。

src/components/Counter.tsx
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 でコンポーネントを使用します。

src/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 にアクセスし、カウンターが動作することを確認します。

vitest-browser-demo-Google-Chrome-2026-01-19-19-23-19.gif

テストコードを実装

ブラウザモードでのテストコードを実装します。

テストファイルを作成します。

src/components/Counter.spec.tsx
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

ブラウザが開き、テスト結果をインタラクティブに確認できます。

image.png

ブラウザモードの主な特徴

1. 実際のブラウザ環境

jsdom などのシミュレーション環境ではなく、実際のブラウザでテストを実行するため、以下のメリットがあります。

  • CSS の計算が正確
  • ブラウザ固有の API が利用可能
  • 実際のユーザー操作に近いテストが可能

2. 複数ブラウザでのテスト

設定で複数のブラウザを指定できます。

vite.config.ts
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 スナップショット、ネットワークアクティビティ、スクリーンショットなどが記録され、テストが失敗した際のデバッグに役立ちます。

設定ファイルで有効化します。

vite.config.ts
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 コンポーネントやページのスクリーンショットを撮影し、参照画像と比較して意図しない視覚的変更を検出します。

src/components/Counter.test.tsx
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

設定ファイルで比較オプションをカスタマイズできます。

vite.config.ts
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 とは異なるアプローチが必要です。

src/api/auth.ts
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' })
}
src/components/__tests__/LoginForm.spec.tsx
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 を使用するコンポーネントのテストに特に有効です。

参考

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?