はじめに
Reactコンポーネントのユニットテストを書いていて、「ブラウザを立ち上げていないのに、なぜdocument.querySelectorが動くのだろう」と疑問に思ったことはないでしょうか。Node.jsにはもともとwindowもdocumentもありません。それでもJestやVitestのテストコードは普通にDOM操作を書けます。
その裏側で動いているのがjsdomです。ブラウザが持つDOM・HTML・CSSOMをJavaScriptだけで再現し、Node.jsのプロセス内に「見えないブラウザ」を作り出すライブラリです。ヘッドレスブラウザのようにChromiumやFirefoxのバイナリを起動するわけではなく、あくまで仕様(Web標準)をJavaScriptで実装している点が特徴です。
この記事では、jsdomが何をしてくれるライブラリなのか、どこまでブラウザの代わりになるのかを、実際のコード例とともに見ていきます。
jsdomとは
jsdomは、WHATWGやW3Cが定めるDOM・HTML・CSSOMなどのWeb標準を、純粋なJavaScriptで実装したライブラリです。2010年から開発が続いており、JestやMocha、Cheerioなど多くのテストツール・ライブラリが内部で利用してきた実績があります。2026年8月時点の最新バージョンは30.0.1で、開発は現在も活発に続いています。
主な特徴
- ブラウザ不要でDOM操作ができる - Chromiumなどの実バイナリを起動せず、Node.jsプロセス内だけで
windowやdocumentを再現します - Web標準への準拠 - Web Platform Testsを使って仕様との整合性を検証しており、実ブラウザに近い挙動が期待できます
- 軽量・高速 - ヘッドレスブラウザに比べて起動が速く、CIでの大量のユニットテスト実行に向いています
- Node.js向けAPI -
JSDOMクラス経由でHTML文字列を読み込み、windowオブジェクトとして取り出せます
インストール
npm install jsdom
yarn add jsdom
pnpm add jsdom
TypeScriptで型定義を使う場合は、あわせて@types/jsdomもインストールしておくと補完が効きます。
npm install -D @types/jsdom
基本的な使い方
jsdomの基本は、JSDOMクラスにHTML文字列を渡してインスタンスを作り、そのwindowプロパティからdocumentなどにアクセスすることです。
const { JSDOM } = require('jsdom')
const dom = new JSDOM(`<!DOCTYPE html>
<p id="message">Hello</p>
`)
const { document } = dom.window
console.log(document.getElementById('message').textContent) // "Hello"
document.getElementById('message').textContent = 'こんにちは、jsdom'
console.log(document.querySelector('#message').textContent) // "こんにちは、jsdom"
document.getElementById()やdocument.querySelector()など、ブラウザで使い慣れたDOM APIがそのまま使えます。要素の中身を書き換えれば、実ブラウザと同じようにDOMツリーが更新されます。
ESModulesで書く場合は次のようにimportします。
import { JSDOM } from 'jsdom'
const dom = new JSDOM('<!DOCTYPE html><body></body>')
const { window } = dom
jsdomはNode.js専用のライブラリで、windowやdocumentを"再現する"ためのものなので、ブラウザ上で動かすLiveCodesのような実行環境では意味のあるサンプルを作れません。実際に試す場合は、手元のNode.js環境で上記のコードを実行してみてください。
実践的なユースケース
外部HTMLからのデータ抽出(スクレイピング)
取得したHTML文字列から特定の要素を取り出したいとき、正規表現でゴリ押しするより、jsdomでDOMとして解釈してからquerySelectorで抜き出す方が確実です。
const { JSDOM } = require('jsdom')
const html = `
<ul class="articles">
<li><a href="/posts/1">記事A</a></li>
<li><a href="/posts/2">記事B</a></li>
</ul>
`
const dom = new JSDOM(html)
const links = [...dom.window.document.querySelectorAll('.articles a')]
const articles = links.map((a) => ({
title: a.textContent,
href: a.getAttribute('href'),
}))
console.log(articles)
// [{ title: '記事A', href: '/posts/1' }, { title: '記事B', href: '/posts/2' }]
外部から取得したHTMLをそのままfetchする処理を書く場合は、信頼できるドメインだけを対象にし、取得した内容をそのままinnerHTMLで描画しないなど、実際の運用では入力元の安全性に注意してください。
JestやVitestでのDOMテスト
フロントエンドのユニットテストでDOM操作を検証したい場合、jsdomをテスト環境として指定するだけで、documentやwindowがグローバルに使えるようになります。JestやVitestは内部でjsdomをテスト環境として利用しており、設定ファイルでtestEnvironment: 'jsdom'と書くだけで有効になります。
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
}
// button.test.js
test('ボタンをクリックするとテキストが変わる', () => {
document.body.innerHTML = '<button id="btn">押す</button>'
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
btn.textContent = '押されました'
})
btn.click()
expect(btn.textContent).toBe('押されました')
})
実ブラウザを起動しないぶん1テストあたりの実行が速く、DOM操作を含むロジックをCIで大量に回すのに向いています。ReactやVueのコンポーネントテストライブラリ(Testing Libraryなど)の裏側でも、この仕組みが使われています。
仮想コンソール・リソース読み込みの制御
jsdomはVirtualConsoleを使うと、jsdom内部で発生したconsole.logやエラーをNode.js側のコンソールに橋渡しできます。またresourcesオプションで外部リソース(画像や別スクリプト)の読み込み方針をコントロールできます。
const { JSDOM, VirtualConsole } = require('jsdom')
const virtualConsole = new VirtualConsole()
virtualConsole.sendTo(console, { omitJSDOMErrors: true })
const dom = new JSDOM(
`<script>console.log('jsdom内から出力')</script>`,
{
runScripts: 'dangerously',
virtualConsole,
}
)
runScripts: 'dangerously'は、渡したHTML内の<script>を実際に実行するオプションです。名前のとおり信頼できないHTML(外部から取得したものなど)に対して使うと任意のスクリプトを実行してしまうため、自分で用意したHTMLやテスト用の固定コンテンツに限定して使うことが重要です。
まとめ
jsdomは、Node.js上にDOM・HTML・CSSOMを再現することで、「ブラウザなしでブラウザ相当の処理を書く」ことを可能にするライブラリです。JestやVitestのDOMテスト、スクレイピングでのHTML解析、SSR環境でのDOM操作の検証など、実ブラウザを起動するほどではないがdocumentやwindowが必要な場面で力を発揮します。
一方で、CSSのレイアウト計算やレンダリング結果の見た目までは再現しないため、ピクセル単位の見た目を検証したい場合はPlaywrightやPuppeteerのような実ブラウザベースのツールと使い分けるのが現実的です。「DOM操作のロジックを軽く速く検証したい」場面ではjsdom、「実際の描画結果を確認したい」場面ではヘッドレスブラウザ、という住み分けを意識すると、テスト戦略がすっきりします。