102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
/**
|
|
* happy-dom polyfills tailored for `@suaveplan/react-ui` tests.
|
|
*
|
|
* happy-dom (catalog:testing) provides a reasonably complete DOM, but a
|
|
* handful of browser globals that the chassis touches are either missing
|
|
* or ship stub implementations:
|
|
*
|
|
* - `matchMedia` — `useMobile` and the FOUC inline script both poll
|
|
* `prefers-color-scheme` / `prefers-reduced-motion` / max-width queries
|
|
* via `window.matchMedia`. happy-dom includes a partial implementation;
|
|
* we install a controllable wrapper so individual tests can flip the
|
|
* stored result.
|
|
* - `ResizeObserver` / `IntersectionObserver` — Radix UI primitives we
|
|
* re-export call these in their effect setup. happy-dom returns
|
|
* `undefined` for both. We polyfill with no-op classes so component
|
|
* mounts do not throw during render.
|
|
*
|
|
* The polyfills are idempotent: if `globalThis` already exposes a
|
|
* functional implementation, we leave it alone.
|
|
*/
|
|
|
|
interface MatchMediaState {
|
|
readonly matches: boolean;
|
|
readonly media: string;
|
|
}
|
|
|
|
class FakeMediaQueryList {
|
|
public readonly matches: boolean;
|
|
public readonly media: string;
|
|
private readonly listeners = new Set<(ev: MatchMediaState) => void>();
|
|
|
|
public constructor(media: string, matches: boolean) {
|
|
this.media = media;
|
|
this.matches = matches;
|
|
}
|
|
|
|
public addEventListener(
|
|
_evt: string,
|
|
listener: (ev: MatchMediaState) => void
|
|
): void {
|
|
this.listeners.add(listener);
|
|
}
|
|
|
|
public removeEventListener(
|
|
_evt: string,
|
|
listener: (ev: MatchMediaState) => void
|
|
): void {
|
|
this.listeners.delete(listener);
|
|
}
|
|
|
|
/** Legacy alias used by some libraries. */
|
|
public addListener(listener: (ev: MatchMediaState) => void): void {
|
|
this.listeners.add(listener);
|
|
}
|
|
|
|
public removeListener(listener: (ev: MatchMediaState) => void): void {
|
|
this.listeners.delete(listener);
|
|
}
|
|
|
|
public dispatchEvent(): boolean {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__reactUiMatchMediaResult?: (query: string) => boolean;
|
|
}
|
|
}
|
|
|
|
if (typeof globalThis.window !== "undefined") {
|
|
if (typeof globalThis.window.matchMedia !== "function") {
|
|
globalThis.window.matchMedia = ((query: string) => {
|
|
const evaluator = (globalThis.window.__reactUiMatchMediaResult ??
|
|
(() => false)) as (q: string) => boolean;
|
|
return new FakeMediaQueryList(query, evaluator(query));
|
|
}) as Window["matchMedia"];
|
|
}
|
|
|
|
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
globalThis.ResizeObserver = class FakeResizeObserver {
|
|
public observe(): void {}
|
|
public unobserve(): void {}
|
|
public disconnect(): void {}
|
|
} as unknown as typeof ResizeObserver;
|
|
}
|
|
|
|
if (typeof globalThis.IntersectionObserver === "undefined") {
|
|
globalThis.IntersectionObserver = class FakeIntersectionObserver {
|
|
public readonly root: Element | null = null;
|
|
public readonly rootMargin: string = "0px";
|
|
public readonly thresholds: readonly number[] = [];
|
|
public observe(): void {}
|
|
public unobserve(): void {}
|
|
public disconnect(): void {}
|
|
public takeRecords(): readonly IntersectionObserverEntry[] {
|
|
return [];
|
|
}
|
|
} as unknown as typeof IntersectionObserver;
|
|
}
|
|
}
|