Vitest コンポーネントテスト実践:Browser Mode と Playwright 連携

正直に言うと、初めて Canvas コンポーネントをテストしたときは大きな落とし穴にハマりました。
テストレポートの緑色の「PASS」を見て、自信満々でコミットしたのです。翌日、同僚が実ブラウザでページを開いたら——Canvas はまったく描画されていませんでした。テストは通ったのに?!
そのとき初めて気づきました。jsdom はあくまで「疑似」ブラウザで、DOM API は再現しても、Canvas の実描画、CSS の計算スタイル、Web Components のライフサイクルまでは検証できないのです。半年かけて整えたユニットテスト設定も、表面だけ触れていたに過ぎませんでした。
だから Vitest 3.0 では Browser Mode が登場しました。実ブラウザ上でテストを走らせます。jsdom のように Node.js 内で DOM を模すのではなく、Chromium / Firefox / Safari を起動してコンポーネントを描画し、Playwright の API で操作します。テストが通れば、本当に通ったと言えます。
本記事では、Browser Mode の設定から React / Vue のコンポーネントテスト、CI のカバレッジゲートまでをゼロから説明します。Vitest テストガイドシリーズの第 3 回で、前 2 回のユニットテスト設定と TDD の流れに、コンポーネントテストのピースをはめ込みます。
なぜ Browser Mode が必要か
「jsdom じゃ足りないの?」と思うかもしれません。
純粋なロジックだけのコンポーネント——電卓やフォームバリデーションなど——なら jsdom で十分です。Node.js 上の DOM シミュレーターで速く、設定も簡単。シリーズ前編のユニットテストも jsdom で、リアクティブデータやイベント発火は問題なく検証できました。
次のような場面では、jsdom は力不足です。
- Canvas 描画:Canvas API はありますが、実際には描画しません。
ctx.fillRect()の呼び出し回数は測れても、描いた図形が正しいかは分かりません。 - CSS 計算スタイル:jsdom の
getComputedStyle()は空オブジェクトです。実ブラウザでは親要素、padding、border が幅に効きますが、jsdom では計算できません。 - Web Components:
connectedCallbackやdisconnectedCallbackは模倣されますが、発火タイミングは実ブラウザと一致しません。 - 非同期レンダリング:アニメーションフレーム、
requestIdleCallback、IntersectionObserverは未実装か不完全です。
私も同じ轍を踏みました。昨年のプロジェクトで、CSS アニメーションでボタンの開閉を制御するコンポーネントがありました。jsdom では transitionend は決して発火しません。transition そのものがないからです。モックイベントでテストを通しても、実ブラウザでアニメーション時間を変えた瞬間に UI は壊れ、テストだけが「合格」のまま残りました。
Browser Mode はここを埋めます。実ブラウザにコンポーネントを載せ、Chromium(や Firefox / Safari)上で Playwright API によりクリック・入力・待機を行います。ブラウザで起きたことが、そのままテスト対象になります。
参考データとして、Vitest 3.0 の Browser Mode は Chromium コンテキストを共有し、起動は 1 回だけで全テストが同じインスタンスを使います。公式では従来の Playwright E2E より約 30% 速いとされています。50 個のコンポーネントを測っても、ブラウザの起動・終了を繰り返す必要はありません。
テストピラミッドについても議論があります。従来型はユニットが厚く、E2E が薄い形です。一方 Vue 公式ブログや alexop.dev では「逆ピラミッド」——統合 70%、ユニット 20%、E2E 10%——も提案されています。コンポーネントはテンプレート・スタイル・ロジックの統合単位なので、jsdom のユニットだけでは全体挙動は見えません。Browser Mode は jsdom より実態に近く、Playwright E2E より軽い、中間のレイヤーと言えます。
Browser Mode 設定の実践
設定自体は難しくありません。ただし、私が踏んだ穴を先に共有します。
依存関係のインストール
Vitest と Browser Mode プロバイダを入れます。公式は Playwright を推奨しています。
npm install -D vitest @vitest/browser-playwright
Playwright は Chromium、Firefox、WebKit をまとめて入れます。Chromium だけで足りるなら、次のように絞れます。
npx playwright install chromium
Chromium パッケージは約 170MB あり、少し時間がかかります。ダウンロードが終われば、ほぼ準備完了です。
vitest.config.ts の設定
設定はシンプルですが、1 点だけ注意があります。
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: playwright(),
enabled: true,
instances: [{ browser: 'chromium' }],
},
},
})
instances で実行ブラウザを決めます。互換性を広げるなら Firefox と WebKit も追加できます。
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' }, // Safari
]
私は通常 Chromium のみです。マルチブラウザは遅く、多くのフロントエンド不具合は Chromium で十分見つかります。Safari 固有の問題は、Playwright E2E で重要パスを押さえます。
Headless モードと UI モード
使い分けは次のとおりです。
- Headless モード:ウィンドウを出さずバックグラウンド実行。CI 向きで速い一方、描画過程は見えません。
- UI モード:ブラウザウィンドウが開き、描画・クリック・入力を目で追えます。テスト作成時のデバッグに向きます。
開発中は UI モードで、次を実行します。
npx vitest --browser.ui
Vitest のテストワークベンチが開き、左にテスト一覧、右にブラウザが並びます。ファイルを選ぶとコンポーネントが描画され、実行の様子がその場で分かります。ボタンが反応しない?ブラウザでそのまま調べられます。
CI では Headless にします。設定に 1 行足します。
browser: {
provider: playwright(),
enabled: true,
headless: true, // CI で headless を強制
instances: [{ browser: 'chromium' }],
}
テストファイルの命名
公式は .browser.test.ts で Browser Mode 用と通常の .test.ts を分けることを推奨しています。メリットは次のとおりです。
- jsdom のユニットと Browser Mode のコンポーネントテストを同時実行しても混ざらない。
- CI が落ちたとき、ファイル名でブラウザ系だとすぐ分かる。
Vitest は命名を強制しません。.test.ts のままでも動きます。重要なのは、Browser Mode 対象ディレクトリを設定で指定するか、全体を Browser Mode に載せるかです。私はユニットは jsdom、コンポーネントは Browser Mode と分けています。
React / Vue コンポーネントテストの実践
ここが Browser Mode の本丸です。Testing Library とは少し違いますが、慣れれば手早く書けます。
React コンポーネントテスト
React アダプタを入れます。
npm install -D @vitest/browser-react
Counter コンポーネントで、クリックのたびにカウントが 1 増える例です。
// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter'
test('ボタンクリックでカウント', async () => {
// コンポーネントをブラウザにマウント
await page.mount(<Counter />)
// ボタン要素を取得
const button = page.getByRole('button', { name: 'Count: 0' })
// クリック
await userEvent.click(button)
// テキスト変化を検証
await expect.element(button).toHaveTextContent('Count: 1')
})
Testing Library との対応は次のとおりです。render() が page.mount()、screen.getByRole() が page.getByRole()。API は近く、page が Browser Mode のコンテキストです。
await expect.element(button) は Vitest の Web Testing API で、要素の状態変化を自動待機します。手動の waitFor() が不要で、Testing Library より短く書けます。
Vue コンポーネントテスト
Vue 用アダプタが必要です。
npm install -D @vitest/browser-vue
Vue の Counter テスト例です。
// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter.vue'
test('ボタンクリックでカウント', async () => {
// Vue コンポーネントをマウント
await page.mount(Counter)
const button = page.getByRole('button', { name: 'Count: 0' })
await userEvent.click(button)
await expect.element(button).toHaveTextContent('Count: 1')
})
Vue 2 は @vue/test-utils の世界で、Browser Mode は直接サポートしません。Vue 3 なら @vitest/browser-vue で足ります。
実例:ドラッグ並べ替え
プロジェクトに HTML5 Drag & Drop のソートコンポーネントがありました。jsdom では dragstart や drop を本物に近い形で再現できず、モックでは「イベントが飛んだか」しか分からず、並べ替えロジックが正しいかは不明でした。
Browser Mode なら次のように書けます。
test('ドラッグでソート', async () => {
await page.mount(<SortableList items={['A', 'B', 'C']} />)
const itemA = page.getByText('A')
const itemC = page.getByText('C')
// A を C の後ろへドラッグ
await userEvent.dragTo(itemA, itemC)
// 順序を検証
const items = page.getByRole('listitem')
await expect.element(items.nth(2)).toHaveTextContent('A')
})
実ブラウザで Drag & Drop が動き、ソートが走り、DOM の順序も変わります。テストが通れば、実利用でも通る可能性が高い——それが Browser Mode の価値です。
Playwright と Browser Mode の使い分け
「どちらもブラウザテストでは?」と混乱しがちです。
一言でいうと、Browser Mode はコンポーネント、Playwright はフローです。
違いの整理
| 特徴 | Browser Mode | Playwright |
|---|---|---|
| テスト範囲 | 単一コンポーネントの隔離テスト | 複数ページのフローテスト |
| 実行速度 | 約 200ms/テスト | 2〜5 秒/テスト |
| 起動コスト | ブラウザインスタンスを共有 | テストごとに独立起動 |
| 設定の複雑さ | 低(Vitest に統合) | 高(別プロジェクト構成) |
| 向く場面 | 開発中の高速イテレーション | リリース前の重要パス検証 |
Browser Mode はコンポーネントをブラウザに載せ、その部品だけの挙動を見ます。Playwright E2E はアプリ全体を開き、遷移・ログイン・送信といった一連の流れを検証します。
たとえば、Browser Mode は顕微鏡で細胞を、Playwright は健診で臓器系統全体を見るイメージです。役割が違い、置き換えはできません。
組み合わせ方
実プロジェクトでは次のように分けています。
- Browser Mode:ボタン、フォーム、カード、モーダルなど UI コンポーネント全般。開発と同時に書き、コミット単位で回す。速く、すぐフィードバックが返る。
- Playwright E2E:ログイン後ホーム、検索結果、注文送信など 3〜5 本の重要パス。リリース前や CI の日次ビルドで実行。
メリットは 3 つです。
- 多くの UI 不具合を開発段階で Browser Mode が拾う。
- ページをまたぐ統合不具合を Playwright がリリース前に拾う。
- コンポーネント 50 本、E2E 5 本程度に抑え、CI を重くしすぎない。
どちらを選ぶか
- Browser Mode:1 コンポーネントのクリック、入力、描画、スタイル。コードを変えたらすぐ結果が欲しいとき。
- Playwright:ログイン → 遷移 → 操作 → 検証のような複数ページフロー。フロント + API + DB の横断が必要なとき。
日付ピッカーの範囲制限や無効日、表示形式は Browser Mode。予約ページで日付を選び、注文して決済へ進む流れは Playwright です。
Browser Mode はフロントのみ、Playwright はフルスタックも扱えます。バックエンド API があるなら E2E で前後をつなげられます。Browser Mode ではバックエンドはモックが前提です。
CI 環境のカバレッジゲート
カバレッジは CI の最後の砦です。私も以前は軽視していました。あるリファクタでカバレッジが 80% から 60% に落ち、本番で不具合が出て初めてゲートの意味を実感しました。
カバレッジ設定
vitest.config.ts に閾値を足します。
test: {
coverage: {
provider: 'v8', // または 'istanbul'
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80
}
}
}
閾値の目安は次のとおりです。
- 新規プロジェクト:50% から始め、徐々に上げる。最初から高すぎると負担が大きい。
- 成熟プロジェクト:80% が妥当。コアは 90〜95% もあり。
- 100% は不要:例外分岐など、無理にテストを書いてもコスパが悪いケースがあります。
実行は次のとおりです。
npx vitest run --coverage
閾値を下回ると Vitest が失敗し、CI も止まります。これがゲート——基準未満の変更は main に入れません。
GitHub Actions 連携
ワークフローに「テスト実行」と「カバレッジレポート」を足します。
# .github/workflows/test.yml
name: Test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install chromium --with-deps
- name: Run tests with coverage
run: npx vitest run --coverage
- name: Report coverage
uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-path: './coverage/coverage-summary.json'
この Action は PR コメントに変化を載せます。
- 全体の増減(例:80% → 79% は -1%)
- カバレッジが下がったファイル
- 未カバーの新規コード
作者は一目で「新規コードのテストが足りない」と分かります。
CI での Browser Mode 注意点
- Playwright インストール:
--with-depsを付けないと Chromium が起動しないことがあります。 - Headless:
headless: trueを明示。CI にディスプレイはありません。 - タイムアウト:jsdom より遅いので 30 秒程度に延ばすのが無難です。
- 並列:ブラウザを共有するため
maxWorkers: 4程度に抑えます。
私の CI 設定例です。
- name: Run browser tests
run: npx vitest run --coverage --browser.headless
env:
CI: true
--browser.headless でウィンドウを出さず、CI: true で Vitest が色付き出力などを調整します。
まとめ
要点はシンプルです。jsdom では実ブラウザの挙動は測れません。Canvas、CSS 計算スタイル、Web Components、ドラッグ、アニメーション——こうした場面では Browser Mode が現実的な選択です。
設定も @vitest/browser-playwright の導入と vitest.config.ts の数行で始められます。React / Vue の API は Testing Library に近く、学習コストは低めです。
万能ではありません。単体 UI は Browser Mode、ユーザーフローは Playwright。開発中は前者で UI を広く押さえ、リリース前に後者で重要パスを確認する組み合わせが、カバレッジと CI 速度のバランスに効きます。
最後にカバレッジゲート。閾値を下回る PR はマージ不可にし、緩やかな低下を防ぎます。vitest-coverage-report-action を入れれば、PR 上で変化が見えるので、抜けもすぐ気づけます。
まだ試していないなら、ボタンや入力のような小さなコンポーネントから始めてください。設定が動くことを確認してから、複雑な部品へ広げれば十分です。つまずきはありますが、一度慣れるとテストが開発の加速装置になります。
Vitest Browser Mode でコンポーネントテストを設定する
ゼロから Browser Mode を設定し、React/Vue のコンポーネントテストを実行し、CI にカバレッジゲートを組み込みます
⏱️ 目安時間: 20 分
- 1
ステップ 1: Playwright プロバイダーをインストール
npm install -D vitest @vitest/browser-playwright で依存関係を入れ、npx playwright install chromium でブラウザを取得します。 - 2
ステップ 2: vitest.config.ts を設定
test.browser で provider: playwright()、enabled: true、instances: [{ browser: 'chromium' }] を指定します。CI では headless: true を追加します。 - 3
ステップ 3: コンポーネントテストを書く
page.mount() でコンポーネントを描画し、page.getByRole() で要素を取得、userEvent.click() で操作し、expect.element() で結果を検証します。 - 4
ステップ 4: カバレッジゲートを設定
vitest.config.ts の coverage.thresholds に閾値(lines: 80 など)を設定し、GitHub Actions に vitest-coverage-report-action を入れて PR のカバレッジ変化を表示します。
FAQ
Browser Mode と jsdom の違いは?
Browser Mode と Playwright E2E はどう使い分ける?
カバレッジ閾値はどのくらいが妥当?
Browser Mode は Vue 2 に対応している?
CI で Browser Mode を使うときの注意点は?
6分で読めます · 公開日: 2026年5月17日 · 更新日: 2026年7月14日



コメント
GitHubアカウントでログインしてコメントできます