// oxlint-disable unbound-method import { isFocusable, noop } from "@ariakit/utils"; import { isBrowser, isHappyDOM } from "./__utils.ts"; // Apply the browser shims once for the whole test environment, instead of only // while a simulated interaction runs. Components read layout and focusability // between interactions too — for example a dialog's auto-focus effect calls // `getFirstTabbableIn`, which uses the `getClientRects` shim to decide whether // an element is visible. If the shims only existed inside `wrapAsync`, those // reads would hit jsdom's empty layout and misbehave (focus landing on the // dialog container instead of the first tabbable). // // `index.ts` imports this module for its side effect, so the shims are applied // automatically when importing `@ariakit/test` (and `@ariakit/test/react`, // which re-exports `index.ts`). function applyBrowserShims() { if (isBrowser) return noop; // Run at import (the call below), so guard against a missing DOM — when // `@ariakit/test` is imported in a plain Node context there's nothing to // shim, and patching the DOM constructors below would throw. if ( typeof window === "undefined" || typeof HTMLElement === "undefined" || typeof Element === "undefined" ) { return noop; } const originalFocus = HTMLElement.prototype.focus; HTMLElement.prototype.focus = function focus(options) { if (!isFocusable(this)) return; return originalFocus.call(this, options); }; const originalGetClientRects = Element.prototype.getClientRects; // @ts-expect-error Element.prototype.getClientRects = function getClientRects() { const isHidden = (element: Element) => { if (!element.isConnected) return true; if (element.parentElement && isHidden(element.parentElement)) return true; if (!(element instanceof HTMLElement)) return false; if (element.hidden) return true; const style = getComputedStyle(element); return style.display === "none" || style.visibility === "hidden"; }; if (isHidden(this)) return []; return [{ width: 1, height: 1 }]; }; if (!Element.prototype.scrollIntoView) { Element.prototype.scrollIntoView = noop; } if (!Element.prototype.hasPointerCapture) { Element.prototype.hasPointerCapture = noop; } if (!Element.prototype.setPointerCapture) { Element.prototype.setPointerCapture = noop; } if (!Element.prototype.releasePointerCapture) { Element.prototype.releasePointerCapture = noop; } if ( typeof window.ClipboardEvent === "undefined" && typeof Event !== "undefined" ) { // @ts-expect-error window.ClipboardEvent = class ClipboardEvent extends Event {}; } if ( typeof window.PointerEvent === "undefined" && typeof MouseEvent !== "undefined" ) { // @ts-expect-error window.PointerEvent = class PointerEvent extends MouseEvent {}; } // happy-dom doesn't implement window.alert (jsdom and real browsers do). // Provide a no-op so code that calls or spies on it works under happy-dom. if (isHappyDOM() && typeof window.alert !== "function") { window.alert = () => {}; } // happy-dom diverges from real browsers in a few spec-conformance areas; these // shims patch them for the whole test environment (jsdom already behaves // correctly). Each helper returns a function that restores the original // behavior. const restoreHappyDOMShims = isHappyDOM() ? [ patchHappyDOMValidationMessage(), patchHappyDOMFormData(), patchHappyDOMSelectionChange(), patchHappyDOMAnimationFrame(), ] : []; return () => { HTMLElement.prototype.focus = originalFocus; Element.prototype.getClientRects = originalGetClientRects; for (const restore of restoreHappyDOMShims) restore(); }; } // happy-dom returns an empty validationMessage for built-in constraint // violations (only setCustomValidity populates it); jsdom and real browsers // return a non-empty message. Ariakit's form validation reads // element.validationMessage to register errors, so mirror that here. The exact // string matches jsdom's generic message so the shared form example tests assert // the same text under both environments (real browsers use locale-specific text, // which these tests don't depend on). function patchHappyDOMValidationMessage() { const restores: Array<() => void> = []; for (const Constructor of [ window.HTMLInputElement, window.HTMLTextAreaElement, window.HTMLSelectElement, ]) { if (!Constructor) continue; const descriptor = Object.getOwnPropertyDescriptor( Constructor.prototype, "validationMessage", ); if (!descriptor?.get || !descriptor.configurable) continue; const originalGet = descriptor.get; Object.defineProperty(Constructor.prototype, "validationMessage", { configurable: true, get(this: { validity?: ValidityState }) { const message = originalGet.call(this) as string; if (message) return message; if (this.validity && !this.validity.valid) { return "Constraints not satisfied"; } return message; }, }); restores.push(() => { Object.defineProperty( Constructor.prototype, "validationMessage", descriptor, ); }); } return () => { for (const restore of restores) restore(); }; } // happy-dom's FormData constructor only checks `disabled` for controls, // so disabled