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.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-10 09:09:38 +04:00
1 parent 4e3bf04010
commit cb15405cfb
2 files changed
+89 -12

No files matched your search

+21
View File
@@ -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();
+68 -12
View File
@@ -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,