これは、なにをしたくて書いたもの?
jsdomを触っておこうかなということで。
jsdom
GitHub - jsdom/jsdom: A JavaScript implementation of various web standards, for use with Node.js
jsdomは、Node.js上で使用するWHATWG DOMやHTMLなど多くのWeb標準のピュアJavaScript実装です。Webアプリケーションの
テストやスクレイピングなどに役立てる程度に、Webブラウザのサブセットをエミュレーションすることを目標にした
プロジェクトです。
今回はこちらを触ってみます。
Vitestを使ったテストコードで試してみようと思うのですが、environmentをjsdomにするとjsdomを使用したブラウザ環境の
エミュレーションを行えるようなのでこちらも試してみます。
Test Environment | Guide | Vitest
つまり、environmentをnodeとjsdomの2つにします。
環境
今回の環境はこちら。
$ node --version v22.14.0 $ npm --version 10.9.2
Vitestのnode environmentでjsdomを試す
まずはVitestのnode environmentでjsdomを試してみます。Vitestを挙げていますが、要するにNode.js環境で単純にjsdomを
使うだけの話です。
Node.jsプロジェクトを作成して、必要な依存関係などをインストール。
$ npm init -y $ npm i -D typescript $ npm i -D @types/node@v22 $ npm i -D prettier $ npm i -D vitest $ mkdir test
jsdomと型定義もインストールしておきます。
$ npm i jsdom $ npm i -D @types/jsdom
プロジェクトはECMAScript Modulesとして、scriptsや依存関係はこのように設定しました。
"type": "module", ... "scripts": { "build": "tsc --project .", "build:watch": "tsc --project . --watch", "typecheck": "tsc --project ./tsconfig.typecheck.json", "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch", "test": "vitest run", "test:watch": "vitest watch", "format": "prettier --write test" }, ... "devDependencies": { "@types/jsdom": "^21.1.7", "@types/node": "^22.14.1", "prettier": "^3.5.3", "typescript": "^5.8.3", "vitest": "^3.1.1" }, "dependencies": { "jsdom": "^26.1.0" }
各種設定ファイル。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "nodenext", "moduleResolution": "nodenext", "lib": ["esnext"], "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "skipLibCheck": true, "esModuleInterop": true }, "include": [ "src/**/*" ] }
tsconfig.typecheck.json
{ "extends": "./tsconfig", "compilerOptions": { "baseUrl": "./", "noEmit": true }, "include": [ "test/**/*" ] }
vite.config.ts
/// <reference types="vitest" /> import { defineConfig } from 'vite'; export default defineConfig({ test: { environment: 'node', }, });
.prettierrc.json
{ "singleQuote": true, "printWidth": 100 }
では、jsdomを使っていってみます。
テストコードの雛形はこちら。
test/app.test.ts
import { JSDOM, VirtualConsole } from 'jsdom';
import { expect, test } from 'vitest';
// ここに、テストを書く!
まずは基本的なところから。JSDOMのコンストラクターにHTMLを渡すようです。
test('first jsdom', () => { const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`); expect(dom.window.document.querySelector('p')?.textContent).toStrictEqual('Hello world'); });
JSDOMのインスタンスを作った後は、windowオブジェクトを操作できるようですね。
JSDOMのコンストラクターに渡せるのは、string、Buffer、BinaryDataのようです。
オプションの設定。
test('set options', () => { const domNoOptions = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`); expect(domNoOptions.window.location.protocol).toStrictEqual('about:'); expect(domNoOptions.window.location.host).toStrictEqual(''); expect(domNoOptions.window.location.pathname).toStrictEqual('blank'); expect(domNoOptions.window.location.search).toStrictEqual(''); const domWithOptions = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`, { url: 'http://example.com/path?message=hello', }); expect(domWithOptions.window.location.protocol).toStrictEqual('http:'); expect(domWithOptions.window.location.host).toStrictEqual('example.com'); expect(domWithOptions.window.location.pathname).toStrictEqual('/path'); expect(domWithOptions.window.location.search).toStrictEqual('?message=hello'); });
jsdom / Customizing jsdom / Simple options
今回は、HTMLをhttp://example.com/path?message=helloからロードしたことにしてみました。window.locationに反映されて
いることが確認できます。
指定できるオプションはこのあたりのようですね。
https://github.com/jsdom/jsdom/blob/26.1.0/lib/api.js#L202-L221
JavaScriptの実行もできるようです。
test('execute script', () => { const dom = new JSDOM( `<body> <div id="content"></div> <script>document.getElementById("content").append(document.createElement("hr"));</script> </body>`, { runScripts: 'dangerously', }, ); expect(dom.window.document.querySelector('hr')).not.toBeNull(); });
jsdom / Customizing jsdom / Executing scripts
HTML内のscriptにあるJavaScriptの実行はセキュリティ上デフォルトで無効になっていて、runScriptsオプションを
dangerouslyに指定しなければ実行できません。
window.evalを使ってHTML外からJavaScriptを実行する場合は、runScriptsオプションにoutside-onlyを指定すると
実行できます。
Consoleオブジェクトも扱うことができ、window.consoleをVirtual consoleとして割り当てることもできます。
test('virtual console', () => { const virtualConsole = new VirtualConsole(); virtualConsole.sendTo(console); const dom = new JSDOM('<body><script>console.log("Print Console");</script></body>', { virtualConsole, runScripts: 'dangerously', }); });
jsdom / Customizing jsdom / Virtual consoles
今回は、window.consoleの内容をNode.jsのconsoleに転送しています。
このテストを実行すると
$ npm test
標準出力にwindow.console.logの結果が出力されます。
Print Console
最後は外部URLから取得したHTMLを使って、JSDOMのインスタンスを構築してみます。
test('load from url', async () => { const dom = await JSDOM.fromURL('https://kazuhira-r.hatenablog.com/entry/2024/08/18/175254'); expect(dom.window.document.querySelector('.entry-title-link')?.textContent).toStrictEqual( 'TypeScript × Vitest × SuperTestでExpressのテストを書いて動かしてみる', ); });
今回はJSDOM#fromURLを使って指定のURLからHTMLを取得しましたが、JSDOM#fromFileでファイルを指定することも
できます。
Vitestのjsdom environmentでjsdomを試す。
次は、Vitestのjsdom environmentでjsdomを試してみます。
Test Environment | Guide | Vitest
これ、どういうことなんだろう?と思ったのですが、テストコード内でグローバルオブジェクトとしてjsdomのwindowが
使えるようになるみたいですね
// @vitest-environment jsdom import { expect, test } from 'vitest' test('test', () => { expect(typeof window).not.toBe('undefined') })
Node.jsプロジェクトを作成して、必要な依存関係をインストール。
$ npm init -y $ npm i -D typescript $ npm i -D prettier $ npm i -D vitest $ npm i -D jsdom @types/jsdom $ mkdir test
package.jsonやPrettierの設定はほとんど同じ(jsdomがdevDependenciesに移ったくらい)なので省略します。
tsconfig.jsonのlibはDOM、DOM.Iterableとしました。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "nodenext", "moduleResolution": "nodenext", "lib": ["esnext", "DOM", "DOM.Iterable"], "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "skipLibCheck": true, "esModuleInterop": true }, "include": [ "src/**/*" ] }
vite.config.tsに関しては、environmentがjsdomになります。
vite.config.ts
/// <reference types="vitest" /> import { defineConfig } from 'vite'; export default defineConfig({ test: { environment: 'jsdom', }, });
これで、テストコード内でwindowが使えるようになります。
test/app.test.ts
import { expect, test } from 'vitest'; test('jsdom environment', () => { expect(window).not.toBeUndefined(); expect(document).not.toBeUndefined(); }); test('create element', () => { const element = document.createElement('div'); expect(element).not.toBeNull(); }); test('document innerHtml', () => { document.body.innerHTML = '<!DOCTYPE html><p>Hello world</p>'; expect(document.querySelector('p')?.textContent).toStrictEqual('Hello world'); });
ちなみに、jsdomがインストールされていない場合は、Vitestでテストを実行した時にjdsomを依存関係に追加してよいか
聞かれることになります。
MISSING DEPENDENCY Cannot find dependency 'jsdom' ✖ Do you want to install jsdom? …
ちなみにvite.config.tsのenvironmentが他の値であっても
export default defineConfig({ test: { environment: 'node', }, });
テストコードのファイルの最初に@vitest-environment jsdomを含むコメントを以下のように記述することで、そのテストコードを
jsdom環境で動作させることもできます。
/** * @vitest-environment jsdom */ import { expect, test } from 'vitest'; test('jsdom environment', () => { expect(window).not.toBeUndefined(); expect(document).not.toBeUndefined(); }); test('create element', () => { const element = document.createElement('div'); expect(element).not.toBeNull(); }); test('document innerHtml', () => { document.body.innerHTML = '<!DOCTYPE html><p>Hello world</p>'; expect(document.querySelector('p')?.textContent).toStrictEqual('Hello world'); });
基本的な使い方はこんなところでしょうか。
おわりに
jsdomを試してみました。
非ブラウザ環境でのJavaScriptでのHTML操作に関するライブラリーを扱ってこなかったので、こういう感じなんだなーと雰囲気を
押さえられたような気がします。
Vitestでのサポートも見てみましたし、初歩的な内容は掴めたのではないでしょうか。