// A happy-dom page for frontend tests, and a fake berry server for the app to talk to. import { Window } from "happy-dom"; import type { ViewContext } from "@berry/api/web.ts"; /** What frontend code reads off `globalThis`, taken from the page. */ const PAGE_GLOBALS = [ "window", "navigator", "document", "location", "localStorage", "Node", "Element", "HTMLElement", "HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement", "Text", "Event", "KeyboardEvent", "MouseEvent", "PointerEvent", "MutationObserver", "ResizeObserver", "IntersectionObserver", "addEventListener", "removeEventListener", "requestAnimationFrame", "cancelAnimationFrame", "matchMedia", "getSelection", "getComputedStyle", ] as const; /** * Run `test` with a happy-dom page installed as the globals frontend code reads, and `globals` over them; afterwards * the globals are as they were. */ export async function withDom( test: (window: Window) => T | Promise, { url = "http://berry.test/", globals = {} }: { url?: string; globals?: Record; } = {}, ): Promise { const window = new Window({ url }); const page = window as unknown as Record; const installed: Record = {}; for (const key of PAGE_GLOBALS) { const value = page[key]; // methods need their window; constructors keep their statics installed[key] = typeof value === "function" && /^[a-z]/.test(key) ? value.bind(window) : value; } Object.assign(installed, globals); const originals = new Map(); for (const [key, value] of Object.entries(installed)) { originals.set(key, Object.getOwnPropertyDescriptor(globalThis, key)); Object.defineProperty(globalThis, key, { configurable: true, writable: true, value }); } try { return await test(window); } finally { await window.happyDOM.close(); for (const [key, original] of originals) { if (original) Object.defineProperty(globalThis, key, original); else Reflect.deleteProperty(globalThis, key); } } } /** Set a field's value as typing would. */ export function fill(field: HTMLInputElement | HTMLTextAreaElement, text: string) { field.value = text; field.dispatchEvent(new Event("input", { bubbles: true })); } /** Press `key` on `target`; whether something prevented its default. */ export function press(target: EventTarget, key: string, init: KeyboardEventInit = {}): boolean { return !target.dispatchEvent( new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...init }), ); } /** What a view or header widget is shown with, in a conversation that offers it nothing `patch` does not. */ export const viewContext = (patch: Partial = {}): ViewContext => ({ tid: "3mxd6epszps5d", model: null, route: "", latest: null, href: (route) => route, go() {}, request: () => Promise.reject(new Error("no handlers here")), connect: () => { throw new Error("no channels here"); }, command: () => Promise.reject(new Error("no commands here")), ...patch, }); export type FakeRequest = { method: string; path: string; body: unknown }; /** * Berry as the app sees it: a websocket whose topics the test delivers, and an api that records each request and * answers with what `respond` returns, a `Response` as it is and anything else as json. */ export function fakeBerry() { const requests: FakeRequest[] = []; let respond: (request: FakeRequest, signal?: AbortSignal) => unknown = ({ method }) => method === "GET" ? [] : {}; let socket: FakeSocket | undefined; class FakeSocket { static OPEN = 1; readyState = 1; onopen?: () => void; onmessage?: (event: { data: string }) => void; onclose?: (event: { code: number }) => void; constructor() { socket = this; } send() {} close() { this.readyState = 3; this.onclose?.({ code: 1000 }); } } const deliver = (message: object) => socket?.onmessage?.({ data: JSON.stringify(message) }); return { requests, /** The requests that change something. */ changes: () => requests.filter((request) => request.method !== "GET"), respond(handler: typeof respond) { respond = handler; }, deliver, snapshot: (topic: string, value: unknown) => deliver({ t: "snapshot", topic, value }), globals: { WebSocket: FakeSocket, fetch: async (path: string, init?: RequestInit) => { const request = { method: init?.method ?? "GET", path, body: init?.body === undefined ? undefined : JSON.parse(String(init.body)), }; requests.push(request); const answer = await respond(request, init?.signal ?? undefined); return answer instanceof Response ? answer : Response.json(answer); }, }, }; } export type FakeBerry = ReturnType;