import { webcrypto } from "node:crypto"; import z from "zod"; import { TEST_VITE_CODES, TestViteError } from "../error-codes/error-codes.js"; const BrowserEnvironmentOptionsSchema = z .object({ target: z.unknown().optional(), mockLocalStorage: z.boolean().default(true), mockMatchMedia: z.boolean().default(true), mockResizeObserver: z.boolean().default(true), mockIntersectionObserver: z.boolean().default(true), mockCrypto: z.boolean().default(true), }) .strict(); export type BrowserEnvironmentOptions = z.input< typeof BrowserEnvironmentOptionsSchema >; type TargetRecord = Record; class MemoryStorage { private readonly store = new Map(); public get length(): number { return this.store.size; } public clear(): void { this.store.clear(); } public getItem(key: string): string | null { return this.store.has(key) ? (this.store.get(key) as string) : null; } public key(index: number): string | null { return Array.from(this.store.keys())[index] ?? null; } public removeItem(key: string): void { this.store.delete(key); } public setItem(key: string, value: string): void { this.store.set(key, String(value)); } } function fakeMatchMedia(query: string): { matches: boolean; media: string; addEventListener: () => void; removeEventListener: () => void; addListener: () => void; removeListener: () => void; onchange: null; dispatchEvent: () => boolean; } { return { matches: false, media: query, addEventListener: () => undefined, removeEventListener: () => undefined, addListener: () => undefined, removeListener: () => undefined, onchange: null, dispatchEvent: () => true, }; } class FakeResizeObserver { public observe(): void { /* noop */ } public unobserve(): void { /* noop */ } public disconnect(): void { /* noop */ } } class FakeIntersectionObserver { public readonly root: null = null; public readonly rootMargin: string = ""; public readonly thresholds: readonly number[] = []; public observe(): void { /* noop */ } public unobserve(): void { /* noop */ } public disconnect(): void { /* noop */ } public takeRecords(): readonly unknown[] { return []; } } /** * Install DOM environment shims on `target` (defaults to `globalThis`). * * Each shim is installed only when absent on the target — re-running the * function on an already-set-up target is a no-op. Individual shims can * be skipped with the `mock*: false` flags. * * @throws {TestViteError} with `TEST_VITE.INVALID_OPTIONS` on bad input. */ export function setupBrowserEnvironment( opts: BrowserEnvironmentOptions = {} ): void { const parsed = BrowserEnvironmentOptionsSchema.safeParse(opts); if (!parsed.success) { throw new TestViteError( TEST_VITE_CODES.INVALID_OPTIONS, "setupBrowserEnvironment: invalid options", { details: { issues: parsed.error.issues } } ); } const o = parsed.data; const target = (o.target ?? globalThis) as TargetRecord; if (o.mockLocalStorage && target["localStorage"] === undefined) { target["localStorage"] = new MemoryStorage(); } if (o.mockMatchMedia && target["matchMedia"] === undefined) { target["matchMedia"] = fakeMatchMedia; } if (o.mockResizeObserver && target["ResizeObserver"] === undefined) { target["ResizeObserver"] = FakeResizeObserver; } if ( o.mockIntersectionObserver && target["IntersectionObserver"] === undefined ) { target["IntersectionObserver"] = FakeIntersectionObserver; } if (o.mockCrypto && target["crypto"] === undefined) { target["crypto"] = webcrypto; } }