Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JSDOMの限界と実ブラウザテスト - Vitest Browser Mode実践

Avatar for Sho Sho
April 30, 2026

JSDOMの限界と実ブラウザテスト - Vitest Browser Mode実践

Reactコンポーネントのテストで広く使われるJSDOMは、ブラウザ環境の代用品であり、レイアウト・イベント・非同期処理などで実ブラウザと差異があります。「テストは通るのに本番で動かない」問題が起こり得ます。
Vitest Browser Modeは実ブラウザ上でテストを実行するアプローチです。本トークでは、Testing Libraryからの移行を通じて見えた技術的な違いを深掘りします。

非同期アサーション: expect.element()とリトライ設計の関係
userEventのAPI設計: setup()が不要になった背景
イベント処理の違い: JSDOMで見逃される実ブラウザ特有の挙動
レンダリングと副作用: DOM APIやイベントループにより異なるuseEffectの振る舞い

JSDOMの限界、実ブラウザテストのトレードオフ、選択基準を実例とともにお話しします。

Avatar for Sho

Sho

April 30, 2026

Other Decks in Technology

Transcript

  1. $ vitest --browser F E C - N A G

    O Y A L T · 5 M I N JSDOMの限界と 実ブラウザテスト ─ Vitest Browser Mode 実践 ─ しょう @Sho_26_ts
  2. JSDOM × Vitest Browser Mode 02 /13 @Sho_26_ts ABOUT ME

    しょう @Sho_26_ts ROLE 26卒/フロントエンドエンジニア COMPANY 株式会社PR TIMES STACK React / TypeScript / Vitest INTEREST アクセシビリティ・甘いもの TALKS 本日が初登壇 RECENT PR TIMES で vitest-browser-react に全面移行
  3. JSDOM × Vitest Browser Mode 03 /13 @Sho_26_ts PREMISE ·

    前提 こ の トー ク で 出 て くる 用 語 前提はこの3つ JSDOM テスト環境(偽のブラウザ) Node.js 上で DOM API を JS で再実装。ブラウザを起動せず高速だが、 レイアウトや描画は持たない。 Vitest / Jest でenvironment: 'jsdom'を指定して使う。 (両者ともデフォルトは node) Testing Library テストの書き方ライブラリ screen.getByRole() など、ユーザー視点でUIを操作するAPIを提供。 JSDOMでも Browser Modeでも使える。 Vitest Browser Mode 実ブラウザで動かすテスト実行環境 Playwright 経由で Chromium などを起動し、テストを本物のブラウザ上で走らせる。 本番と同じ環境でテストできる。 Vitest テストランナー 環境指定 → Browser Mode → JSDOM(偽のブラウザ) Playwright → 実ブラウザ どちらも ↓ Testing Library で同じように書ける
  4. JSDOM × Vitest Browser Mode 04 /13 @Sho_26_ts THE PROBLEM

    こ ん な経 験 、 あ り ませ ん か ? テストは通る。 でも本番では動かない。 ・Tooltip の位置が本番だけズレる ・Modal の外側クリックで閉じない ・無限スクロールが動かない テストフレームワークのバグではなく、環境の問題。
  5. JSDOM × Vitest Browser Mode 05 /13 @Sho_26_ts PAUSE Q

    U E S T I O N そのズレ、 なぜ起きる?
  6. JSDOM × Vitest Browser Mode 06 /13 @Sho_26_ts DIAGNOSIS な

    ぜ ズレ る か JSDOM は レイアウトを計算せず、 一部の API を実装しない 前提 で触れた「レイアウトや描画を持たない」の、具体的な中身。 レイアウト 座標・サイズが計算されない —要素の幅・高さ・位置が確定しない —重なり判定もできない —auto を解決できない Observer 系 API コールバックが呼ばれない —サイズ変化に反応できない —可視判定もできない —ビューポート連動も動かない テストが緑なのは、環境差を検知できていないだけかもしれない。
  7. JSDOM × Vitest Browser Mode 07 /13 @Sho_26_ts GAP A

    J S D O M の 欠 如が 直 接 影 響 する ケ ー ス JSDOMで通るが、実ブラウザで失敗する レイアウト計算なし・Observer未実装 が原因 01 クリック判定 モーダルの外クリックや重なり要素のクリックは座標計算 が必要。JSDOMは要素の座標を持たないため、これらの判 定ができない。 JSDOM el.getBoundingClientRect() // → { width: 0, top: 0, ... } Real Browser el.getBoundingClientRect() // → { width: 320, top: 120, ... } 02 副作用と useEffect useLayoutEffect はどちらも commit 後に実行されるが、 JSDOMではサイズが0のまま確定する。ResizeObserverも 未実装。 JSDOM new ResizeObserver(cb) // → cb は呼ばれない(未実装) Real Browser new ResizeObserver(cb) // → サイズ変化で cb が発火 polyfillで埋めれば動く。でもそれは「テストが通る」だけで「動作を保証している」わけではない。
  8. JSDOM × Vitest Browser Mode 08 /13 @Sho_26_ts GAP B

    B R O W S E R M O D E 移 行 で変 わ る こ と 書き方が素直になる JSDOM の欠如ではなく、実ブラウザが本物を持つことで不要になるもの 03 非同期アサーション retry-until-pass の対象がセレクタに。waitFor の二段リトラ イが消えて書き方がシンプルになる。 JSDOM import { screen, waitFor } from '@testing-library/react' const btn = await waitFor(() => screen.getByRole('button')) expect(btn).toBeDisabled() Browser Mode import { page } from '@vitest/browser/context' await expect.element( page.getByRole('button') ).toBeDisabled() 04 userEvent の API 実ブラウザが本物のイベントシステムを持つのでシミュレ ーション状態の初期化が不要になる。 JSDOM import userEvent from '@testing-library/user-event' const user = userEvent.setup() await user.click(btn) await user.type(input, 'hi') Browser Mode import { userEvent } from '@vitest/browser/context' await userEvent.click(btn) await userEvent.type(input, 'hi') // setup() 不要 → JSDOMが悪いのではなく、実ブラウザが持つものを素直に使えるようになる。
  9. JSDOM × Vitest Browser Mode 09 /13 @Sho_26_ts PREMISE 前

    提 Browser は起動コストがかかる JSDOM は Node.js プロセス内で完結するが、 Vitest Browser Mode は Playwright で実ブラウザを起動するためオーバーヘッドがある JSDOM Node.js 内で完結 テスト単体はms 単位 起動コスト = ほぼゼロ Vitest Browser Mode Playwright で Chromium を起動 ブラウザの起動・操作に時間がかかる その分起動コストが高い
  10. JSDOM × Vitest Browser Mode 10 /13 @Sho_26_ts PAUSE Q

    U E S T I O N じゃあ、 実際どのくらい?
  11. JSDOM × Vitest Browser Mode 11 /13 @Sho_26_ts PERFORMANCE 速

    度 の実 測 基本はBrowser が遅い。でも逆転する瞬間がある polyfill ありで揃えた JSDOM と比べても、 重い import や Radix 系では Browser のほうが速い ケース BROWSER JSDOM (+POLYFILL) 差 軽いテスト pure logic / 50 件 / warm 2.81s 1.86s JSDOM +0.95s 重い import Chart + Floating UI / 6 件 3.10s 4.30s Browser +1.20s Radix 系 Tooltip / Dropdown / 1〜2 件 1.32〜1.39s 1.45〜1.85s Browser +0.1〜0.5s ※ PR TIMES 実コードベース / 同一マシン / warm 計測 / JSDOM 側は polyfill 込み
  12. JSDOM × Vitest Browser Mode 12 /13 @Sho_26_ts DECISION ふたつを使い分ける

    JSDOM JSDOM が向く — DOM API を使う表示ロジック — テキスト・属性・classの検証 — 大量のスナップショット — 速さが正義のフィードバック Real Browser Real Browser が向く — ユーザー操作のシナリオ — レイアウトや観測APIに依存するUI — focus / pointer / scroll の検証 — 本番に近い忠実度が要る所 → ファイル単位・テストスイート単位で切り替える、設定を分けるなど、工夫の仕方はさまざま
  13. T A K E A W A Y S 今日の3つ

    01 JSDOMの欠如が、テストを嘘 にする レイアウト計算なし・Observer未実装。クリ ック判定やサイズ計測が動かない。polyfillで 通っても保証ではない。 02 Browser Modeは書き方も変わ る クリック判定が本物に · expect.element で自動リトライ · setup() 不要。実ブラウ ザが持つものを素直に使える。 03 使い分け方はさまざま 基本はBrowserが遅いが、テストが重くなる と逆転する。ファイル単位・スイート単位で 共存できる。 しょう @Sho_26_ts Thankyou.