From cb15405cfb3aa8169f67d5bda5b06163676ff187 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 09:09:38 +0400 Subject: [PATCH] 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,