テスト

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

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方式と移行手順で扱っています。

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にまとめています。

プロバイダは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・最新バージョンで解説しています。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対応】で扱っています。

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移行ガイドに基づきます。

変更点 落ち方 対処
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)。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  3. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  4. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  5. 2026.05.15 テックブログ scrcpyの最新版は5.0|ハードウェアデコード対応とインストール・使い方【2026年10月】

RELATED POSTS 関連記事

目次