Files
pezkuwi-dev/packages/dev-test/src/env/browser.ts
T
Satoshi Qazi Muhammed 13a6acf80d dev-test: JSDOM's WebSocket in the browser environment
The browser environment replaces Node's Event with JSDOM's. Node's WebSocket
dispatches through Node's EventTarget, which rejects that Event, so no open,
message or error reached a listener and no connection ever came up: an
ApiPromise against a live node never became ready. JSDOM's WebSocket now
replaces Node's, as Event and CustomEvent already do. A spec completes a
handshake with a local server and receives a frame.
2026-10-11 06:06:19 +04:00

189 lines
6.9 KiB
TypeScript

// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DOMWindow } from 'jsdom';
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.
*
* WebSocket follows from that: Node's own dispatches its events through
* Node's EventTarget, which rejects the JSDOM Event that replaced Node's, so
* no open, message or error ever reaches a listener and a connection never
* comes up. JSDOM's WebSocket dispatches events from its own realm.
*/
export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event', 'WebSocket'];
/**
* 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
};
}
/** Network, see BROWSER_OVERRIDES for why WebSocket is JSDOM's */
function network (window: DOMWindow) {
return {
WebSocket: window.WebSocket
};
}
/** 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
*
* FIXME: This approach is actually _explicitly_ discouraged by JSDOM - when
* using window you should run the tests inside that context, instead of just
* blindly relying on the globals as we do here
*/
export function browser () {
const { window } = new JSDOM('', { url: 'http://localhost' });
return {
...nodes(window),
...events(window),
...layout(window),
...network(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
HTMLAnchorElement: window.HTMLAnchorElement,
HTMLAreaElement: window.HTMLAreaElement,
HTMLAudioElement: window.HTMLAudioElement,
HTMLBRElement: window.HTMLBRElement,
HTMLBaseElement: window.HTMLBaseElement,
HTMLBodyElement: window.HTMLBodyElement,
HTMLButtonElement: window.HTMLButtonElement,
HTMLCanvasElement: window.HTMLCanvasElement,
HTMLDListElement: window.HTMLDListElement,
HTMLDataElement: window.HTMLDataElement,
HTMLDataListElement: window.HTMLDataListElement,
HTMLDetailsElement: window.HTMLDetailsElement,
HTMLDialogElement: window.HTMLDialogElement,
HTMLDirectoryElement: window.HTMLDirectoryElement,
HTMLDivElement: window.HTMLDivElement,
HTMLElement: window.HTMLElement,
HTMLEmbedElement: window.HTMLEmbedElement,
HTMLFieldSetElement: window.HTMLFieldSetElement,
HTMLFontElement: window.HTMLFontElement,
HTMLFormElement: window.HTMLFormElement,
HTMLFrameElement: window.HTMLFrameElement,
HTMLFrameSetElement: window.HTMLFrameSetElement,
HTMLHRElement: window.HTMLHRElement,
HTMLHeadElement: window.HTMLHeadElement,
HTMLHeadingElement: window.HTMLHeadingElement,
HTMLHtmlElement: window.HTMLHtmlElement,
HTMLIFrameElement: window.HTMLIFrameElement,
HTMLImageElement: window.HTMLImageElement,
HTMLInputElement: window.HTMLInputElement,
HTMLLIElement: window.HTMLLIElement,
HTMLLabelElement: window.HTMLLabelElement,
HTMLLegendElement: window.HTMLLegendElement,
HTMLLinkElement: window.HTMLLinkElement,
HTMLMapElement: window.HTMLMapElement,
HTMLMarqueeElement: window.HTMLMarqueeElement,
HTMLMediaElement: window.HTMLMediaElement,
HTMLMenuElement: window.HTMLMenuElement,
HTMLMetaElement: window.HTMLMetaElement,
HTMLMeterElement: window.HTMLMeterElement,
HTMLModElement: window.HTMLModElement,
HTMLOListElement: window.HTMLOListElement,
HTMLObjectElement: window.HTMLObjectElement,
HTMLOptGroupElement: window.HTMLOptGroupElement,
HTMLOptionElement: window.HTMLOptionElement,
HTMLOutputElement: window.HTMLOutputElement,
HTMLParagraphElement: window.HTMLParagraphElement,
HTMLParamElement: window.HTMLParamElement,
HTMLPictureElement: window.HTMLPictureElement,
HTMLPreElement: window.HTMLPreElement,
HTMLProgressElement: window.HTMLProgressElement,
HTMLQuoteElement: window.HTMLQuoteElement,
HTMLScriptElement: window.HTMLScriptElement,
HTMLSelectElement: window.HTMLSelectElement,
HTMLSlotElement: window.HTMLSlotElement,
HTMLSourceElement: window.HTMLSourceElement,
HTMLSpanElement: window.HTMLSpanElement,
HTMLStyleElement: window.HTMLStyleElement,
HTMLTableCaptionElement: window.HTMLTableCaptionElement,
HTMLTableCellElement: window.HTMLTableCellElement,
HTMLTableColElement: window.HTMLTableColElement,
HTMLTableElement: window.HTMLTableElement,
HTMLTableRowElement: window.HTMLTableRowElement,
HTMLTableSectionElement: window.HTMLTableSectionElement,
HTMLTemplateElement: window.HTMLTemplateElement,
HTMLTextAreaElement: window.HTMLTextAreaElement,
HTMLTimeElement: window.HTMLTimeElement,
HTMLTitleElement: window.HTMLTitleElement,
HTMLTrackElement: window.HTMLTrackElement,
HTMLUListElement: window.HTMLUListElement,
HTMLUnknownElement: window.HTMLUnknownElement,
HTMLVideoElement: window.HTMLVideoElement,
// normal service resumes, the base top-level names
crypto: window.crypto,
document: window.document,
localStorage: window.localStorage,
navigator: window.navigator,
// window...
window
};
}