---
title: "Vitest Browser Modeの使い方｜Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】"
url: "https://www.issoh.co.jp/tech/details/7394/"
published: 2025-06-24
updated: 2026-10-02
categories: ["テスト"]
publisher: "株式会社一創"
---

# Vitest Browser Modeの使い方｜Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】

Vitest Browser Modeは、テストファイルをNode.js上の疑似DOM（jsdomやhappy-dom）ではなく、Chromium・Firefox・WebKitの実ブラウザの中で実行する機能です。Vitest 4で正式機能になり、ブラウザを起動するプロバイダは`@vitest/browser-playwright`などの別パッケージに分かれました。

この記事では、2026年9月30日公開のVitest 5.0.3を前提に、Playwrightプロバイダでの導入、headlessとCIでの実行、jsdomのテストとの共存、Vitest 4から上げたときに落ちるテストの直し方までを順に扱います。動作結果は、Node.js 26.5.0・Playwright 1.63.0の環境で実際にテストを実行して確かめた内容です。

## まとめ：Vitest 5でBrowser Modeを動かす要点

- **インストール**：`npm install -D vitest @vitest/browser-playwright`のあと`npx playwright install chromium`でブラウザを入れます。`npx vitest init browser`でも設定ファイルまで作れます。
- **設定**：`browser.provider`に`playwright()`を関数として渡し、`browser.instances`に最低1つのブラウザを書きます。文字列で`'playwright'`と書くのはVitest 3までの書き方です。
- **headless**：既定値は環境変数`CI`の値です。CIでは自動でheadlessになり、ローカルではブラウザ画面が開きます。
- **jsdomとの使い分け**：レイアウト計算や`IntersectionObserver`のようにjsdomが実装していないAPIに触るテストだけをBrowser Modeへ移し、純粋なロジックのテストはNode.jsに残します。
- **Vitest 5で落ちるテスト**：locatorの文字列が完全一致になり、`toHaveTextContent`も完全一致になりました。`await`を付け忘れた`expect.element`は、警告ではなくテスト失敗になります。

## Browser Modeとjsdom・happy-domの違い：実ブラウザでしか通らないテスト

jsdomとhappy-domは、Node.jsの中でDOMの仕様を模倣するライブラリです。描画エンジンを持たないため、要素の大きさや位置を計算しません。Vitestの公式ドキュメントも、シミュレーション環境ではテスト結果に偽陽性・偽陰性が出うる点をBrowser Modeを作った理由に挙げています。

### 同じテストをjsdomと実ブラウザで実行した結果

幅120pxの要素を作って`getBoundingClientRect()`で幅を読むテストと、`IntersectionObserver`の有無を見るテストを、jsdomとChromium（Browser Mode）の両方で実行しました。

| 確認した内容                          | jsdom         | Browser Mode（Chromium） |
| ------------------------------- | ------------- | ---------------------- |
| `getBoundingClientRect().width` | 0（失敗）         | 120（成功）                |
| `typeof IntersectionObserver`   | undefined（失敗） | function（成功）           |

jsdomでは、スタイルで幅を指定しても0が返ります。同じ環境のjsdom 30.1.1では、`ResizeObserver`と`window.matchMedia`も`undefined`でした。スクロール位置で表示を切り替える部品や、要素のサイズで折り返しを変える部品は、jsdomではこれらをモックで差し替えて書くことになります。この場合、モックしたAPIに対する部品の応答は検証できますが、ブラウザによる実際のレイアウト計算や通知のタイミングまでは検証できません。

### Browser ModeとE2Eテストの役割分担

公式ドキュメントはBrowser Modeを「Not a Drop-In Replacement」と位置づけ、E2Eテストは別のテストランナーで補うよう勧めています。Browser Modeが実行するのは1つのコンポーネントや関数の単位で、ページ遷移・ログイン・本物のAPIサーバーとの通信を通した一連のシナリオは、Playwright TestなどのE2Eランナーで補います。Playwright側で部品単位のテストを書く方法は[Playwright Component Testingの使い方｜1.62のstory方式と移行手順](/tech/details/7392/)で扱っています。

## Vitest Browser Modeの導入手順：@vitest/browser-playwrightの設定

### 動作要件：Node.js 22.12以上とVite 6.4以上

Vitest 5.0は、Node.js 22.12.0以上とVite 6.4.0以上が必要です。npmの`engines`は`^22.12.0 || ^24.0.0 || >=26.0.0`で、奇数版のNode.js 23・25は含まれません。Viteは6.4・7・8のいずれかを選べます。Yarnを使う場合は、移行ガイドの指示どおり`vite`を明示的にインストールします。

### パッケージとブラウザのインストール

対話形式で済ませるなら`npx vitest init browser`を実行します。必要なパッケージのインストールと設定ファイルの作成までを行うコマンドです。手動で入れる場合は次のとおりです。

```
npm install -D vitest @vitest/browser-playwright
npx playwright install chromium
```

`@vitest/browser-playwright`は`@vitest/browser`に依存しているため、`@vitest/browser`を別にインストールする必要はありません。`playwright`はpeerDependenciesで、npm 7以降は自動で入ります。ブラウザ本体はnpmパッケージに含まれないので、`npx playwright install`は省略できません。Playwright側の詳しい手順と、インストールに失敗したときの確認方法は[Playwrightのインストール方法とできない時の対処｜バージョン確認・Windows・Python](/tech/details/3563/)にまとめています。

プロバイダは3種類あります。CIで動かすならPlaywrightかWebdriverIOを選び、迷ったら並列実行に対応するPlaywrightを選ぶよう公式ドキュメントは勧めています。

| プロバイダ       | パッケージ                         | 操作の再現方法          | CIでの利用 |
| ----------- | ----------------------------- | ---------------- | ------ |
| Playwright  | `@vitest/browser-playwright`  | Playwright経由の実操作 | 可（推奨）  |
| WebdriverIO | `@vitest/browser-webdriverio` | WebDriver経由の実操作  | 可      |
| preview     | `@vitest/browser-preview`     | イベントの模倣          | 不可     |

WebdriverIOプロバイダは、Vitest 5.0でVitest本体のリポジトリから外れ、コミュニティのvitest-community組織が管理するパッケージになりました。2026年10月2日時点のnpmでは、PlaywrightプロバイダのlatestはVitestと同じ5.0.3、WebdriverIOプロバイダのlatestは5.0.0です。

### vitest.config.tsの最小構成

Reactのプロジェクトなら次の設定で動きます。`provider`には`playwright()`の戻り値を渡し、`instances`には実行するブラウザを最低1つ書きます。

```
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' }],
    },
  },
})
```

Vitest 3までの記事にある`provider: 'playwright'`という文字列指定や、`@vitest/browser/context`からの`page`の読み込みは、Vitest 4で変わった書き方です。Vitest 4で何が変わったかは[Vitest 4の新機能と移行ガイド｜ブラウザモード正式化・VRT・最新バージョン](/tech/details/9465/)で解説しています。Browser Modeのサーバーは開発サーバーと衝突しないよう63315番ポートを使うため、`vite dev`と同時に動かせます。

### Reactコンポーネントの最初のテスト

コンポーネントの描画には`vitest-browser-react`（2026年10月時点のlatestは2.3.0）を使います。ReactとReact DOMを導入済みのプロジェクトなら、次の2つを追加します。

```
npm install -D @vitejs/plugin-react vitest-browser-react
```

テストする部品は、ラベルとクリック回数を表示するだけのカウンターです（`src/Counter.tsx`）。

```
import { useState } from 'react'

export function Counter({ label }: { label: string }) {
  const [n, setN] = useState(0)
  return (
    <div>
      <p>Hello, {label}</p>
      <button onClick={() => setN(n + 1)}>count is {n}</button>
    </div>
  )
}
```

テストファイルは`src/Counter.browser.test.tsx`として置きます。`page`と`userEvent`は`vitest/browser`から読み込みます。

```
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { render } from 'vitest-browser-react'
import { Counter } from './Counter'

test('クリックでカウントが増える', async () => {
  await render(<Counter label="Vitest" />)
  await page.getByRole('button', { name: 'count is 0' }).click()
  await expect.element(page.getByRole('button')).toHaveTextContent('count is 1')
})
```

`render`は`await`で待ちます。Vitest 5では`vitest-browser-vue`と`vitest-browser-svelte`の`render`もPromiseを返すようになったため、VueとSvelteでも同じく`await`が要ります。`click()`はPlaywrightを通した実際のクリックで、jsdomの`fireEvent`のようにイベントを直接発火させる方式とは違います。

DOMの検証は`expect.element`で書きます。条件を満たすまで再試行するため、非同期で表示が変わる部品でも`waitFor`を挟む必要がありません。jsdomとTesting Libraryで書く従来の方法は[Vitestの使い方：Testing Libraryとは何かからReactコンポーネントテストまで【Vitest 5対応】](/tech/details/2867/)で扱っています。

## headlessとCIでVitest Browser Modeを実行する設定

### headlessの既定値と環境変数CIの関係

`browser.headless`の既定値は`process.env.CI`です。GitHub ActionsのようにCI環境変数を設定するサービスでは自動でheadlessになり、ローカルではブラウザ画面とVitest UIが開きます。UIを注入する`browser.ui`の既定値も`!isCI`です。

ローカルでも画面を出さずに実行したい場合は、設定に`headless: true`を書くか、CLIで`npx vitest --browser.headless`を指定します。headlessでもVitest UIを見たいときは`@vitest/ui`を入れて`--ui`を付けます。

### GitHub ActionsでPlaywrightのブラウザを入れる手順

GitHubのランナーにはPlaywright用のブラウザが入っていません。テストの前にブラウザのインストールを挟みます。

```
- name: Install Playwright Browsers
  run: npx --no playwright install --with-deps --only-shell
- name: Run tests
  run: npx vitest run
```

`--only-shell`は、Chromiumをheadless用のシェルだけに絞ってダウンロード量を減らす指定で、Vitestの公式ドキュメントのCI例もこの形です。headlessで実行するChromiumは通常のChromeではなく`chromium_headless_shell`を使います。ブラウザを入れ忘れると、テストは1件も実行されず次のエラーになります。

```
Error: browserType.launch: Executable doesn't exist at .../chromium_headless_shell-1243/...
Please run the following command to download new browsers:
    npx playwright install
```

ディレクトリ名の1243は、Playwright 1.63.0が同梱するChromiumのリビジョン番号です。Playwrightを更新するとこの番号が変わるため、キャッシュに残った古いブラウザでは起動しません。CIでブラウザのキャッシュを使う場合は、キャッシュキーに`playwright`のバージョンを含めます。Dockerで動かす場合は、Playwrightと同じバージョンのイメージ（1.63.0なら`mcr.microsoft.com/playwright:v1.63.0-noble`）を使えばブラウザと依存ライブラリがそろっています。

### 失敗時のスクリーンショットとトレースによる原因調査

`browser.screenshotFailures`の既定値は`!browser.ui`で、headlessで失敗するとスクリーンショットが自動で保存されます。Vitest 5.0から保存先が`.vitest/attachments/failure-screenshots/`配下になり、失敗メッセージにパスが出力されます。

操作の経過まで追いたい場合は`browser.trace`を`'retain-on-failure'`にします。失敗したテストのトレースだけを残し、Playwright Trace Viewerで開けます。既定値は`'off'`で、`'on'`は全テストを記録するため公式ドキュメントも性能面で推奨していません。このトレースはPlaywrightプロバイダ専用です。Vitest 5.0では、プロバイダを問わず各操作をDOMスナップショットとして記録するTrace View（`browser.traceView`・experimental）も加わりました。

## jsdomのテストとBrowser Modeを共存させるprojects構成

### unitとbrowserにプロジェクトを分ける設定

全テストを実ブラウザへ移す必要はありません。以下の共存例ではjsdomも使うため、未導入なら`npm install -D jsdom`を実行します。`test.projects`でjsdomのプロジェクトとブラウザのプロジェクトを分け、ファイル名で振り分けます。以前の`workspace`設定はVitest 3.2で`projects`に改名され、Vitest 4で`vitest.workspace.ts`を読む方式ごと`projects`に置き換わりました。

```
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  plugins: [react()],
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['src/**/*.test.{ts,tsx}'],
          exclude: ['src/**/*.browser.test.{ts,tsx}'],
          environment: 'jsdom',
        },
      },
      {
        test: {
          name: 'browser',
          include: ['src/**/*.browser.test.{ts,tsx}'],
          browser: {
            enabled: true,
            provider: playwright(),
            instances: [{ browser: 'chromium' }],
          },
        },
      },
    ],
  },
})
```

片方だけを実行するときは`npx vitest --project unit`のように指定します。Vitest 5.0から`--project`は`-p`と短く書けます。

### Vitest 5の設定継承とinclude配列の結合

Vitest 5.0では、`projects`に直接書いたプロジェクトがルートの設定を既定で継承するようになりました（`extends`の既定値が`true`）。上の例で`plugins: [react()]`が両プロジェクトに効くのはこのためです。

注意したいのは、配列の設定が上書きではなく結合される点です。ルートに`test.include: ['src/layout.test.ts']`を残したまま、ブラウザのプロジェクトに`include: ['src/layout.both.test.ts']`を書いて`vitest list`を実行すると、ブラウザのプロジェクトに2ファイルとも並びました。jsdom用に書いたテストがブラウザでも走るということです。プロジェクト側に`extends: false`を書くと1ファイルだけになり、Vitest 4と同じ挙動に戻りました。`setupFiles`も同じ規則で結合されるため、jsdom向けのセットアップをルートに置いたままにしないようにします。

### 複数ブラウザで同じテストを実行するinstances

同じテストをChromium・Firefox・WebKitで実行するときは、`projects`を増やさず`browser.instances`に並べます。

```
browser: {
  enabled: true,
  provider: playwright(),
  instances: [
    { browser: 'chromium' },
    { browser: 'firefox' },
    { browser: 'webkit' },
  ],
},
```

公式ドキュメントによると、`instances`は同じViteサーバーを共有するため、ファイルの変換と依存関係の事前バンドルが1回で済みます。ブラウザごとに`projects`を分ける構成に比べ、変換や事前バンドルの重複を減らせます。ブラウザ本体は`npx playwright install chromium firefox webkit`でそろえます。

## Vitest 4から5へ上げたときにBrowser Modeのテストが落ちる変更点

Vitest 5.0（2026年9月3日公開）の破壊的変更のうち、Browser Modeのテストに影響するものを症状と対処で整理しました。変更点の一覧は公式の[Vitest 5.0移行ガイド](https://vitest.dev/guide/migration/)に基づきます。

| 変更点                       | 落ち方                 | 対処                    |
| ------------------------- | ------------------- | --------------------- |
| locatorが完全一致に             | 部分一致の文字列が見つからない     | 全文を書くか`exact: false`  |
| `toHaveTextContent`が完全一致に | 部分一致・正規表現が失敗        | `toMatchTextContent`へ |
| awaitなしの非同期アサーション         | テストが即失敗             | `await`を付ける           |
| Vue・Svelteの`render`が非同期に  | 描画前に検証が走る           | `await render()`      |
| ブラウザでのautomock修正          | モックの戻り値が`undefined` | `{ spy: true }`かファクトリ |
| `clearMocks`の既定が`true`    | 呼び出し履歴が前のテストから消える   | 履歴をテスト内で作る            |

### getByTextの完全一致化と要素未検出時のタイムアウト

Vitest 5.0で`browser.locators.exact`の既定値が`true`になり、`getByText`などのlocatorは大文字小文字を含めた完全一致で探すようになりました。画面に「Hello, Vitest」と表示された状態で`page.getByText('Hello')`を探すと、移行ガイドによればVitest 4までは部分一致で見つかる書き方ですが、5.0.3では見つからず15,215msで失敗しました。ブラウザ有効時のテストのタイムアウト既定値（15,000ms）まで要素を探し続けるため、失敗までに時間がかかります。

失敗メッセージには、探したlocatorとページのARIAツリー（`paragraph: Hello, Vitest`など）が表示されるので、どの文字列で探せば一致するかはそこから読めます。個別に直すなら`page.getByText('Hello', { exact: false })`で通り、プロジェクト全体を元に戻すなら`browser.locators.exact: false`を設定します。

### toHaveTextContentの完全一致化とtoMatchTextContentへの移行

Browser Modeの`toHaveTextContent`も完全一致の比較になり、正規表現を受け付けなくなりました。従来の部分一致と正規表現は、新しく追加された`toMatchTextContent`が引き継いでいます。

```
await expect.element(page.getByRole('paragraph')).toHaveTextContent('Hello, Vitest')
await expect.element(page.getByRole('paragraph')).toMatchTextContent(/hello/i)
```

jsdomのプロジェクトで使う`@testing-library/jest-dom`の`toHaveTextContent`は部分一致のままです。同じマッチャー名でもプロジェクトによって判定が違うため、両方を持つリポジトリでは注意が必要です。

### expect.elementのawait漏れによるテスト失敗

Vitest 4までは、`await`のない非同期アサーションをテストの最後にまとめて待ち、警告を出すだけでした。Vitest 5.0からはテストの失敗になります。`expect.element(...)`の`await`を外して実行すると、次のメッセージで即座に失敗しました。

```
Error: expect.element(locator).toHaveTextContent() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections
```

このアサーションは実行されないまま失敗します。awaitの付け忘れで本来の検証が抜けていたテストが、表に出てくる変更です。

## Browser Modeの制限：spyOnとダイアログ、採用しない場面

### モジュールのexportに対するvi.spyOnの制限

Browser Modeはブラウザのネイティブなモジュール（ESM）を使うため、`import * as mod`で読み込んだ名前空間オブジェクトは書き換えられません。`vi.spyOn(mod, 'greet')`を実行すると次のエラーになりました。

```
TypeError: Cannot spy on export "greet". Module namespace is not configurable in ESM.
```

代わりに`vi.mock('./util', { spy: true })`を書くと、実装を残したまま全exportの呼び出しを記録できます。exportされた変数の値を差し替える方法はないため、値を変える関数をモジュール側でexportします。

### alert・confirmの既定モックと戻り値

`alert`・`confirm`・`print`のようにページの処理を止めるダイアログは、Vitestとページの通信まで止めてしまうため、Browser Modeでは既定のモックに置き換えられています。実行すると、`confirm()`はダイアログを出さずに`false`を返しました。「OK」を押した場合の分岐をテストしたいときは、公式ドキュメントのとおりテスト側でモックして戻り値を指定します。

### Browser Modeに移さないほうがよいテスト

DOMやブラウザ固有APIに依存しない関数・状態管理・バリデーションのテストは、原則としてNode.js環境に残します。公式ドキュメントも、プロバイダとブラウザを起動する分だけ初期化が長くなる点をデメリットに挙げています。ブラウザの種類を増やせば、同じテストをその数だけ実行します。

移行の第一候補は、jsdomが再現できないレイアウト計算やブラウザAPIをモックしているテストです。`IntersectionObserver`・`ResizeObserver`・`matchMedia`のモックや、`getBoundingClientRect`の戻り値の差し替えを含むテストは、モックを外してBrowser Modeで書き直すと、部品の実際の挙動を確かめるテストになります。

## よくある質問

### Vitest Browser Modeとjsdomはどちらを使うべきですか？

両方を使い、`projects`で振り分けるのが基本です。レイアウト・CSS・jsdom未実装のAPIに依存するテストはBrowser Modeへ、DOMに触らないテストやjsdomで問題なく書けているテストはjsdomのまま残します。

### @vitest/browserと@vitest/browser-playwrightの違いは何ですか？

`@vitest/browser`はBrowser Modeの共通部分、`@vitest/browser-playwright`はPlaywrightでブラウザを起動するプロバイダです。Vitest 4からはプロバイダのパッケージを入れれば`@vitest/browser`も依存として入るため、個別のインストールは不要です。

### Vitest Browser Modeをheadlessで実行するには？

設定の`browser.headless`を`true`にするか、`npx vitest --browser.headless`で実行します。環境変数`CI`が設定されていれば、何も指定しなくてもheadlessになります。

### VitestのBrowser ModeはDockerで動かせますか？

動かせます。Playwrightの公式イメージ（例：`mcr.microsoft.com/playwright:v1.63.0-noble`）を、プロジェクトの`playwright`と同じバージョンで使うと、ブラウザとOSの依存ライブラリが入った状態で始められます。バージョンがずれると、同梱ブラウザのリビジョンが合わず起動に失敗します。

### Browser Modeでスクリーンショットを撮るには？

テストの途中で撮るなら`page.screenshot()`、画像を基準と比べるビジュアルリグレッションテストなら`await expect(locator).toMatchScreenshot()`を使います。失敗時のスクリーンショットは、Playwrightプロバイダの既定のheadless構成では自動保存されます（設定は`browser.screenshotFailures`）。

## 関連記事

- [Vitest 4の新機能と移行ガイド｜ブラウザモード正式化・VRT・最新バージョン](/tech/details/9465/)
- [Vitestの使い方：Testing Libraryとは何かからReactコンポーネントテストまで【Vitest 5対応】](/tech/details/2867/)
- [Vitestとは？Viteネイティブなテスト基盤の仕組みと採用判断を実装目線で解説](/tech/details/16023/)
- [Playwright Component Testingの使い方｜1.62のstory方式と移行手順](/tech/details/7392/)
- [Playwrightのインストール方法とできない時の対処｜バージョン確認・Windows・Python](/tech/details/3563/)

---

出典: [Vitest Browser Modeの使い方｜Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】](<https://www.issoh.co.jp/tech/details/7394/>)（株式会社一創）
