From 2e3c47bbd7603faa08254899049c1a4b9139c1ba Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 08:42:29 +0400 Subject: [PATCH 1/5] run-test: pass --experimental-test-module-mocks through to node Specs that replace a module need mock.module from node:test, which Node only exposes behind this flag. The runner rejected unknown flags, so there was no way to turn it on. It is now accepted and forwarded to node like --import and --require. The repo's own test run enables it, and a spec checks that a module registered as a mock is what a later dynamic import receives. --- package.json | 2 +- packages/dev-test/src/env/moduleMocks.spec.ts | 14 ++++++++++++++ packages/dev/README.md | 5 +++++ packages/dev/scripts/pezkuwi-dev-run-test.mjs | 5 +++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 packages/dev-test/src/env/moduleMocks.spec.ts diff --git a/package.json b/package.json index bb88130..d0caa8f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "docs": "pezkuwi-dev-build-docs", "lint": "pezkuwi-dev-run-lint", "postinstall": "./packages/dev/scripts/pezkuwi-dev-yarn-only.mjs", - "test": "yarn build && pezkuwi-dev-run-test --dev-build --env browser" + "test": "yarn build && pezkuwi-dev-run-test --dev-build --experimental-test-module-mocks --env browser" }, "devDependencies": { "@pezkuwi/dev": "workspace:packages/dev", diff --git a/packages/dev-test/src/env/moduleMocks.spec.ts b/packages/dev-test/src/env/moduleMocks.spec.ts new file mode 100644 index 0000000..c4fb1b6 --- /dev/null +++ b/packages/dev-test/src/env/moduleMocks.spec.ts @@ -0,0 +1,14 @@ +// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { mock } from 'node:test'; + +describe('mock.module', () => { + it('replaces a module loaded after the mock is registered', async () => { + mock.module('node:os', { namedExports: { platform: () => 'mocked' } }); + + const { platform } = await import('node:os'); + + expect(platform()).toBe('mocked'); + }); +}); diff --git a/packages/dev/README.md b/packages/dev/README.md index 4038350..c0d1d7d 100644 --- a/packages/dev/README.md +++ b/packages/dev/README.md @@ -252,6 +252,11 @@ This script runs test files in the repository, filtering by file extensions and - **`--logfile `**: Specifies a log file to capture test output. +- **`--experimental-test-module-mocks`**: + Passed to Node.js, enabling `mock.module` from `node:test`. Node only swaps a module that has not + been loaded yet, so a spec registers its mocks first and then loads the code under test with a + dynamic `import()`. + - **`--import `**: Imports the specified module. diff --git a/packages/dev/scripts/pezkuwi-dev-run-test.mjs b/packages/dev/scripts/pezkuwi-dev-run-test.mjs index 0e2878a..9e45fa5 100755 --- a/packages/dev/scripts/pezkuwi-dev-run-test.mjs +++ b/packages/dev/scripts/pezkuwi-dev-run-test.mjs @@ -58,6 +58,11 @@ for (let i = 0; i < args.length; i++) { cmd.push(args[++i]); break; + // node flags with no params + case '--experimental-test-module-mocks': + nodeFlags.push(args[i]); + break; + // node flags that could have additional params case '--import': case '--loader': From 1ba9b3e57f6a62a3b0de6e632780e7f053b881f8 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 08:42:41 +0400 Subject: [PATCH 2/5] Release dev, dev-test and dev-ts together as 0.86.6 Carries the run-test passthrough for --experimental-test-module-mocks. dev depends on ^0.86.6 of the other two. --- packages/dev-test/package.json | 2 +- packages/dev-test/src/packageInfo.ts | 2 +- packages/dev-ts/package.json | 2 +- packages/dev-ts/src/packageInfo.ts | 2 +- packages/dev/package.json | 6 +++--- packages/dev/src/packageInfo.ts | 2 +- yarn.lock | 8 ++++---- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/dev-test/package.json b/packages/dev-test/package.json index 03d6f04..ed29e2a 100644 --- a/packages/dev-test/package.json +++ b/packages/dev-test/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.5", + "version": "0.86.6", "main": "./index.js", "exports": { "./globals.d.ts": "./src/globals.d.ts" diff --git a/packages/dev-test/src/packageInfo.ts b/packages/dev-test/src/packageInfo.ts index 058817c..65cf17e 100644 --- a/packages/dev-test/src/packageInfo.ts +++ b/packages/dev-test/src/packageInfo.ts @@ -3,4 +3,4 @@ // Do not edit, auto-generated by @pezkuwi/dev -export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.5' }; +export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.6' }; diff --git a/packages/dev-ts/package.json b/packages/dev-ts/package.json index 6a93946..d600536 100644 --- a/packages/dev-ts/package.json +++ b/packages/dev-ts/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.5", + "version": "0.86.6", "main": "./index.js", "exports": { "./globals.d.ts": "./src/globals.d.ts" diff --git a/packages/dev-ts/src/packageInfo.ts b/packages/dev-ts/src/packageInfo.ts index 58403f8..e5aec23 100644 --- a/packages/dev-ts/src/packageInfo.ts +++ b/packages/dev-ts/src/packageInfo.ts @@ -3,4 +3,4 @@ // Do not edit, auto-generated by @pezkuwi/dev -export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.5' }; +export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.6' }; diff --git a/packages/dev/package.json b/packages/dev/package.json index 6cc6e20..559a971 100644 --- a/packages/dev/package.json +++ b/packages/dev/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.5", + "version": "0.86.6", "bin": { "pezkuwi-ci-ghact-build": "./scripts/pezkuwi-ci-ghact-build.mjs", "pezkuwi-ci-ghact-docs": "./scripts/pezkuwi-ci-ghact-docs.mjs", @@ -52,8 +52,8 @@ "dependencies": { "@eslint/compat": "^2.1.1", "@eslint/js": "^10.0.1", - "@pezkuwi/dev-test": "^0.86.5", - "@pezkuwi/dev-ts": "^0.86.5", + "@pezkuwi/dev-test": "^0.86.6", + "@pezkuwi/dev-ts": "^0.86.6", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-dynamic-import-vars": "^2.1.5", diff --git a/packages/dev/src/packageInfo.ts b/packages/dev/src/packageInfo.ts index 8f31535..1ec83e0 100644 --- a/packages/dev/src/packageInfo.ts +++ b/packages/dev/src/packageInfo.ts @@ -3,4 +3,4 @@ // Do not edit, auto-generated by @pezkuwi/dev -export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.5' }; +export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.6' }; diff --git a/yarn.lock b/yarn.lock index 9a80126..ace19c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,7 +1189,7 @@ __metadata: languageName: node linkType: hard -"@pezkuwi/dev-test@npm:^0.86.5, @pezkuwi/dev-test@workspace:packages/dev-test": +"@pezkuwi/dev-test@npm:^0.86.6, @pezkuwi/dev-test@workspace:packages/dev-test": version: 0.0.0-use.local resolution: "@pezkuwi/dev-test@workspace:packages/dev-test" dependencies: @@ -1205,7 +1205,7 @@ __metadata: languageName: unknown linkType: soft -"@pezkuwi/dev-ts@npm:^0.86.5, @pezkuwi/dev-ts@workspace:packages/dev-ts": +"@pezkuwi/dev-ts@npm:^0.86.6, @pezkuwi/dev-ts@workspace:packages/dev-ts": version: 0.0.0-use.local resolution: "@pezkuwi/dev-ts@workspace:packages/dev-ts" dependencies: @@ -1221,8 +1221,8 @@ __metadata: dependencies: "@eslint/compat": "npm:^2.1.1" "@eslint/js": "npm:^10.0.1" - "@pezkuwi/dev-test": "npm:^0.86.5" - "@pezkuwi/dev-ts": "npm:^0.86.5" + "@pezkuwi/dev-test": "npm:^0.86.6" + "@pezkuwi/dev-ts": "npm:^0.86.6" "@rollup/plugin-alias": "npm:^5.1.1" "@rollup/plugin-commonjs": "npm:^29.0.3" "@rollup/plugin-dynamic-import-vars": "npm:^2.1.5" From 0cd4f12cfb695a450665d968fea81f3079d92f8e Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 08:52:46 +0400 Subject: [PATCH 3/5] dev-test: JSDOM's Event and CustomEvent win over Node's in the browser env Globals were only set where Node had none, and Node defines Event and CustomEvent itself. Code running in the browser env therefore built Node events, which a JSDOM node refuses: react-tooltip's window.dispatchEvent(new CustomEvent(...)) threw "parameter 1 is not of type 'Event'". In the browser env these two now come from JSDOM, as they do under jest-environment-jsdom. --- packages/dev-test/src/env/browser.spec.ts | 5 +++++ packages/dev-test/src/env/browser.ts | 10 ++++++++++ packages/dev-test/src/env/index.ts | 8 ++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/dev-test/src/env/browser.spec.ts b/packages/dev-test/src/env/browser.spec.ts index e38c910..30fa22b 100644 --- a/packages/dev-test/src/env/browser.spec.ts +++ b/packages/dev-test/src/env/browser.spec.ts @@ -23,4 +23,9 @@ describe('browser', () => { it('contains HTML*Element', () => { expect(typeof all.HTMLElement).toBe('function'); }); + + it('dispatches the global CustomEvent and Event on window', () => { + expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow(); + expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow(); + }); }); diff --git a/packages/dev-test/src/env/browser.ts b/packages/dev-test/src/env/browser.ts index 39377a1..0a702a0 100644 --- a/packages/dev-test/src/env/browser.ts +++ b/packages/dev-test/src/env/browser.ts @@ -3,6 +3,13 @@ import { JSDOM } from 'jsdom'; +/** + * Constructors that Node defines as well. A DOM node only accepts an event + * created in its own realm, so in the browser environment the JSDOM ones + * replace Node's, as they do under jest-environment-jsdom. + */ +export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event']; + /** * Export a very basic JSDom environment - this is just enough so we have * @testing-environment/react tests passing in this repo @@ -15,6 +22,9 @@ export function browser () { const { window } = new JSDOM('', { url: 'http://localhost' }); return { + // Event constructors, see BROWSER_OVERRIDES + CustomEvent: window.CustomEvent, + Event: window.Event, // All HTML Elements that are defined on the JSDOM window object. // (we copied as-is from the types definition). We cannot get this // via Object.keys(window).filter(...) so we have to specify explicitly diff --git a/packages/dev-test/src/env/index.ts b/packages/dev-test/src/env/index.ts index 37ce94e..8ec9132 100644 --- a/packages/dev-test/src/env/index.ts +++ b/packages/dev-test/src/env/index.ts @@ -1,7 +1,7 @@ // Copyright 2017-2026 @pezkuwi/dev-test authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { browser } from './browser.js'; +import { browser, BROWSER_OVERRIDES } from './browser.js'; import { expect } from './expect.js'; import { jest } from './jest.js'; import { lifecycle } from './lifecycle.js'; @@ -15,7 +15,11 @@ export function exposeEnv (isBrowser: boolean): void { env && Object .entries(env()) .forEach(([key, fn]) => { - globalThis[key as 'undefined'] ??= fn; + if (isBrowser && BROWSER_OVERRIDES.includes(key)) { + (globalThis as Record)[key] = fn; + } else { + globalThis[key as 'undefined'] ??= fn; + } }); }); } From 4e3bf04010f163abf5e0cac8df0631b39e544436 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 08:56:58 +0400 Subject: [PATCH 4/5] dev-test: DOM node hierarchy in the browser env; expect().toContain The browser env exposed the HTML*Element constructors but not the classes above them, so a library checking `instanceof Element` (as react-tooltip does on every hover) threw "Element is not defined". Node, Element, Document, DocumentFragment, Text, ShadowRoot and SVGElement now come from JSDOM. expect().toContain and .not.toContain were stubs. A string contains a substring; any other iterable contains the item itself, not a deep-equal copy, as in jest. --- packages/dev-test/src/env/browser.spec.ts | 12 ++++++++ packages/dev-test/src/env/browser.ts | 11 ++++++- packages/dev-test/src/env/expect.spec.ts | 24 +++++++++++++++ packages/dev-test/src/env/expect.ts | 36 +++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/dev-test/src/env/browser.spec.ts b/packages/dev-test/src/env/browser.spec.ts index 30fa22b..423448e 100644 --- a/packages/dev-test/src/env/browser.spec.ts +++ b/packages/dev-test/src/env/browser.spec.ts @@ -24,6 +24,18 @@ describe('browser', () => { expect(typeof all.HTMLElement).toBe('function'); }); + it('contains the DOM node hierarchy', () => { + const div = document.createElement('div'); + + expect(div).toBeInstanceOf(Element); + expect(div).toBeInstanceOf(Node); + expect(document.createTextNode('text')).toBeInstanceOf(Text); + expect(document.createDocumentFragment()).toBeInstanceOf(DocumentFragment); + expect(document.createElementNS('http://www.w3.org/2000/svg', 'svg')).toBeInstanceOf(SVGElement); + expect(div.attachShadow({ mode: 'open' })).toBeInstanceOf(ShadowRoot); + expect(document).toBeInstanceOf(Document); + }); + it('dispatches the global CustomEvent and Event on window', () => { expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow(); expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow(); diff --git a/packages/dev-test/src/env/browser.ts b/packages/dev-test/src/env/browser.ts index 0a702a0..e51de80 100644 --- a/packages/dev-test/src/env/browser.ts +++ b/packages/dev-test/src/env/browser.ts @@ -22,8 +22,12 @@ export function browser () { const { window } = new JSDOM('', { url: 'http://localhost' }); return { - // Event constructors, see BROWSER_OVERRIDES + // The DOM node hierarchy, which libraries check with instanceof, and the + // event constructors (Node has its own of these, see BROWSER_OVERRIDES) CustomEvent: window.CustomEvent, + Document: window.Document, + DocumentFragment: window.DocumentFragment, + Element: window.Element, Event: window.Event, // All HTML Elements that are defined on the JSDOM window object. // (we copied as-is from the types definition). We cannot get this @@ -99,6 +103,11 @@ export function browser () { HTMLUListElement: window.HTMLUListElement, HTMLUnknownElement: window.HTMLUnknownElement, HTMLVideoElement: window.HTMLVideoElement, + // the rest of the node hierarchy + Node: window.Node, + SVGElement: window.SVGElement, + ShadowRoot: window.ShadowRoot, + Text: window.Text, // normal service resumes, the base top-level names crypto: window.crypto, document: window.document, diff --git a/packages/dev-test/src/env/expect.spec.ts b/packages/dev-test/src/env/expect.spec.ts index 2de1cf8..b14a0c3 100644 --- a/packages/dev-test/src/env/expect.spec.ts +++ b/packages/dev-test/src/env/expect.spec.ts @@ -255,4 +255,28 @@ describe('expect', () => { expect(() => expect([1, 2]).not.toEqual(expect.arrayContaining([2]))).toThrow(); }); }); + + describe('.toContain', () => { + it('finds a substring in a string', () => { + expect('voter-alice').toContain('alice'); + expect(() => expect('voter-alice').toContain('bob')).toThrow(); + }); + + it('finds the item itself in an array or other iterable', () => { + const item = { a: 1 }; + + expect([1, item]).toContain(item); + expect(new Set(['a'])).toContain('a'); + expect(() => expect([{ a: 1 }]).toContain({ a: 1 })).toThrow(); + }); + + it('rejects a value that is neither a string nor an iterable', () => { + expect(() => expect(123).toContain(1)).toThrow(); + }); + + it('supports .not', () => { + expect('voter-alice').not.toContain('bob'); + expect(() => expect([1, 2]).not.toContain(2)).toThrow(); + }); + }); }); diff --git a/packages/dev-test/src/env/expect.ts b/packages/dev-test/src/env/expect.ts index 72ef0d0..2a2fec9 100644 --- a/packages/dev-test/src/env/expect.ts +++ b/packages/dev-test/src/env/expect.ts @@ -144,6 +144,40 @@ function assertMatchObj (value: unknown, check: object): void { .forEach(([key, other]) => assertMatch((value as Record)[key], other)); } +/** + * @internal + * + * Whether a string contains the substring, or another iterable the item + */ +function contains (value: unknown, item: unknown): boolean { + if (typeof value === 'string') { + return typeof item === 'string' && value.includes(item); + } + + assert.ok(typeof (value as Iterable | null | undefined)?.[Symbol.iterator] === 'function', `Expected a string or an iterable, found ${typeof value}`); + + return [...(value as Iterable)].includes(item); +} + +/** + * @internal + * + * A helper for toContain: a string contains the substring, any other + * iterable contains the item itself (not a deep-equal copy) + */ +function assertContains (value: unknown, item: unknown): void { + assert.ok(contains(value, item), `Expected ${String(value)} to contain ${String(item)}`); +} + +/** + * @internal + * + * The inverse of assertContains + */ +function assertNotContains (value: unknown, item: unknown): void { + assert.ok(!contains(value, item), `Expected ${String(value)} not to contain ${String(item)}`); +} + /** * @internal * @@ -269,6 +303,7 @@ export function expect () { toBeDefined: () => assert.ok(value === undefined), toBeNull: (value: unknown) => assert.ok(value !== null), toBeUndefined: () => assert.ok(value !== undefined), + toContain: (item: unknown) => assertNotContains(value, item), toEqual: (other: unknown) => containsMatcher(other) ? assert.throws(() => assertEqualMatch(value, other)) : assert.notDeepEqual(value, other), @@ -287,6 +322,7 @@ export function expect () { toBeNull: (value: unknown) => assert.ok(value === null), toBeTruthy: () => assert.ok(value), toBeUndefined: () => assert.ok(value === undefined), + toContain: (item: unknown) => assertContains(value, item), toEqual: (other: unknown) => containsMatcher(other) ? assertEqualMatch(value, other) : assert.deepEqual(value, other), From cb15405cfb3aa8169f67d5bda5b06163676ff187 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 09:09:38 +0400 Subject: [PATCH 5/5] dev-test: UI events, getComputedStyle and the observers in the browser env react-tooltip, on hover, called getComputedStyle, MutationObserver and ResizeObserver, none of which the browser env exposed. The env now also carries the UI event constructors (MouseEvent, KeyboardEvent, FocusEvent and the rest), DOMRect, MutationObserver and a bound getComputedStyle from JSDOM, grouped by kind. JSDOM has no ResizeObserver. It does no layout either, so no element ever changes size; the env's ResizeObserver accepts observe() and never calls back, which is what a real one would do on a page that never resizes. --- packages/dev-test/src/env/browser.spec.ts | 21 ++++++ packages/dev-test/src/env/browser.ts | 80 +++++++++++++++++++---- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/packages/dev-test/src/env/browser.spec.ts b/packages/dev-test/src/env/browser.spec.ts index 423448e..47f0018 100644 --- a/packages/dev-test/src/env/browser.spec.ts +++ b/packages/dev-test/src/env/browser.spec.ts @@ -36,6 +36,27 @@ describe('browser', () => { expect(document).toBeInstanceOf(Document); }); + it('contains the UI event constructors', () => { + const div = document.createElement('div'); + + expect(() => div.dispatchEvent(new MouseEvent('click'))).not.toThrow(); + expect(() => div.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))).not.toThrow(); + expect(new FocusEvent('focus')).toBeInstanceOf(UIEvent); + }); + + it('contains getComputedStyle and the observers', () => { + const div = document.createElement('div'); + + div.style.display = 'none'; + document.body.appendChild(div); + + expect(getComputedStyle(div).display).toBe('none'); + expect(() => new MutationObserver(() => undefined).observe(div, { childList: true })).not.toThrow(); + expect(() => new ResizeObserver(() => undefined).observe(div)).not.toThrow(); + + div.remove(); + }); + it('dispatches the global CustomEvent and Event on window', () => { expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow(); expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow(); diff --git a/packages/dev-test/src/env/browser.ts b/packages/dev-test/src/env/browser.ts index e51de80..fda9a8b 100644 --- a/packages/dev-test/src/env/browser.ts +++ b/packages/dev-test/src/env/browser.ts @@ -1,6 +1,8 @@ // Copyright 2017-2026 @pezkuwi/dev-test authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { DOMWindow } from 'jsdom'; + import { JSDOM } from 'jsdom'; /** @@ -10,6 +12,69 @@ import { JSDOM } from 'jsdom'; */ export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event']; +/** + * JSDOM does no layout, so no element ever changes size and the callback + * never runs. JSDOM has no ResizeObserver of its own. + */ +class ResizeObserver { + observe (): void { + // nothing is ever resized + } + + unobserve (): void { + // nothing is being watched + } + + disconnect (): void { + // nothing is being watched + } +} + +/** The DOM node hierarchy, which libraries check with instanceof */ +function nodes (window: DOMWindow) { + return { + CharacterData: window.CharacterData, + Comment: window.Comment, + Document: window.Document, + DocumentFragment: window.DocumentFragment, + Element: window.Element, + Node: window.Node, + NodeList: window.NodeList, + SVGElement: window.SVGElement, + ShadowRoot: window.ShadowRoot, + Text: window.Text + }; +} + +/** Event constructors. Node has its own Event and CustomEvent, see BROWSER_OVERRIDES */ +function events (window: DOMWindow) { + return { + CompositionEvent: window.CompositionEvent, + CustomEvent: window.CustomEvent, + Event: window.Event, + FocusEvent: window.FocusEvent, + InputEvent: window.InputEvent, + KeyboardEvent: window.KeyboardEvent, + MouseEvent: window.MouseEvent, + TouchEvent: window.TouchEvent, + UIEvent: window.UIEvent, + WheelEvent: window.WheelEvent + }; +} + +/** Styles, geometry and the observers */ +function layout (window: DOMWindow) { + return { + // present on the JSDOM window, absent from its DOMWindow type + DOMRect: window['DOMRect'] as typeof DOMRect, + DOMRectReadOnly: window['DOMRectReadOnly'] as typeof DOMRectReadOnly, + MutationObserver: window.MutationObserver, + MutationRecord: window.MutationRecord, + ResizeObserver, + getComputedStyle: window.getComputedStyle.bind(window) + }; +} + /** * Export a very basic JSDom environment - this is just enough so we have * @testing-environment/react tests passing in this repo @@ -22,13 +87,9 @@ export function browser () { const { window } = new JSDOM('', { url: 'http://localhost' }); return { - // The DOM node hierarchy, which libraries check with instanceof, and the - // event constructors (Node has its own of these, see BROWSER_OVERRIDES) - CustomEvent: window.CustomEvent, - Document: window.Document, - DocumentFragment: window.DocumentFragment, - Element: window.Element, - Event: window.Event, + ...nodes(window), + ...events(window), + ...layout(window), // All HTML Elements that are defined on the JSDOM window object. // (we copied as-is from the types definition). We cannot get this // via Object.keys(window).filter(...) so we have to specify explicitly @@ -103,11 +164,6 @@ export function browser () { HTMLUListElement: window.HTMLUListElement, HTMLUnknownElement: window.HTMLUnknownElement, HTMLVideoElement: window.HTMLVideoElement, - // the rest of the node hierarchy - Node: window.Node, - SVGElement: window.SVGElement, - ShadowRoot: window.ShadowRoot, - Text: window.Text, // normal service resumes, the base top-level names crypto: window.crypto, document: window.document,