char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somdiff: add (codemirror) inline editing of diffsc5dd844

main
4.8 KiB153 linesraw
1// A happy-dom page for frontend tests, and a fake berry server for the app to talk to.
2
3import { Window } from "happy-dom";
4import type { ViewContext } from "@berry/api/web.ts";
5
6/** What frontend code reads off `globalThis`, taken from the page. */
7const PAGE_GLOBALS = [
8  "window",
9  "navigator",
10  "document",
11  "location",
12  "localStorage",
13  "Node",
14  "Element",
15  "HTMLElement",
16  "HTMLInputElement",
17  "HTMLTextAreaElement",
18  "HTMLSelectElement",
19  "Text",
20  "Event",
21  "KeyboardEvent",
22  "MouseEvent",
23  "PointerEvent",
24  "MutationObserver",
25  "ResizeObserver",
26  "IntersectionObserver",
27  "addEventListener",
28  "removeEventListener",
29  "requestAnimationFrame",
30  "cancelAnimationFrame",
31  "matchMedia",
32  "getSelection",
33  "getComputedStyle",
34] as const;
35
36/**
37 * Run `test` with a happy-dom page installed as the globals frontend code reads, and `globals` over them; afterwards
38 * the globals are as they were.
39 */
40export async function withDom<T>(
41  test: (window: Window) => T | Promise<T>,
42  { url = "http://berry.test/", globals = {} }: {
43    url?: string;
44    globals?: Record<string, unknown>;
45  } = {},
46): Promise<T> {
47  const window = new Window({ url });
48  const page = window as unknown as Record<string, unknown>;
49  const installed: Record<string, unknown> = {};
50  for (const key of PAGE_GLOBALS) {
51    const value = page[key];
52    // methods need their window; constructors keep their statics
53    installed[key] = typeof value === "function" && /^[a-z]/.test(key) ? value.bind(window) : value;
54  }
55  Object.assign(installed, globals);
56  const originals = new Map<string, PropertyDescriptor | undefined>();
57  for (const [key, value] of Object.entries(installed)) {
58    originals.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
59    Object.defineProperty(globalThis, key, { configurable: true, writable: true, value });
60  }
61  try {
62    return await test(window);
63  } finally {
64    await window.happyDOM.close();
65    for (const [key, original] of originals) {
66      if (original) Object.defineProperty(globalThis, key, original);
67      else Reflect.deleteProperty(globalThis, key);
68    }
69  }
70}
71
72/** Set a field's value as typing would. */
73export function fill(field: HTMLInputElement | HTMLTextAreaElement, text: string) {
74  field.value = text;
75  field.dispatchEvent(new Event("input", { bubbles: true }));
76}
77
78/** Press `key` on `target`; whether something prevented its default. */
79export function press(target: EventTarget, key: string, init: KeyboardEventInit = {}): boolean {
80  return !target.dispatchEvent(
81    new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...init }),
82  );
83}
84
85/** What a view or header widget is shown with, in a conversation that offers it nothing `patch` does not. */
86export const viewContext = (patch: Partial<ViewContext> = {}): ViewContext => ({
87  tid: "3mxd6epszps5d",
88  model: null,
89  route: "",
90  latest: null,
91  href: (route) => route,
92  go() {},
93  request: () => Promise.reject(new Error("no handlers here")),
94  connect: () => {
95    throw new Error("no channels here");
96  },
97  command: () => Promise.reject(new Error("no commands here")),
98  ...patch,
99});
100
101export type FakeRequest = { method: string; path: string; body: unknown };
102
103/**
104 * Berry as the app sees it: a websocket whose topics the test delivers, and an api that records each request and
105 * answers with what `respond` returns, a `Response` as it is and anything else as json.
106 */
107export function fakeBerry() {
108  const requests: FakeRequest[] = [];
109  let respond: (request: FakeRequest, signal?: AbortSignal) => unknown = ({ method }) =>
110    method === "GET" ? [] : {};
111  let socket: FakeSocket | undefined;
112  class FakeSocket {
113    static OPEN = 1;
114    readyState = 1;
115    onopen?: () => void;
116    onmessage?: (event: { data: string }) => void;
117    onclose?: (event: { code: number }) => void;
118    constructor() {
119      socket = this;
120    }
121    send() {}
122    close() {
123      this.readyState = 3;
124      this.onclose?.({ code: 1000 });
125    }
126  }
127  const deliver = (message: object) => socket?.onmessage?.({ data: JSON.stringify(message) });
128  return {
129    requests,
130    /** The requests that change something. */
131    changes: () => requests.filter((request) => request.method !== "GET"),
132    respond(handler: typeof respond) {
133      respond = handler;
134    },
135    deliver,
136    snapshot: (topic: string, value: unknown) => deliver({ t: "snapshot", topic, value }),
137    globals: {
138      WebSocket: FakeSocket,
139      fetch: async (path: string, init?: RequestInit) => {
140        const request = {
141          method: init?.method ?? "GET",
142          path,
143          body: init?.body === undefined ? undefined : JSON.parse(String(init.body)),
144        };
145        requests.push(request);
146        const answer = await respond(request, init?.signal ?? undefined);
147        return answer instanceof Response ? answer : Response.json(answer);
148      },
149    },
150  };
151}
152
153export type FakeBerry = ReturnType<typeof fakeBerry>;