char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
c5dd844
main
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 > ( 41test :( window :Window ) => T | Promise < T >, 42{ url= "http://berry.test/" , globals= {} } :{ 43url ?:string ; 44globals ?:Record < string , unknown >; 45} = {}, 46) :Promise < T > { 47const window = new Window ({ url}); 48const page = window as unknown as Record < string , unknown >; 49const installed :Record < string , unknown > = {}; 50for ( const key of PAGE_GLOBALS ) { 51const value = page [ key ]; 52// methods need their window; constructors keep their statics 53installed [ key ] = typeof value === "function" && / ^[a-z] / . test ( key ) ?value . bind ( window ) :value ; 54} 55Object . assign ( installed , globals ); 56const originals = new Map < string , PropertyDescriptor | undefined >(); 57for ( const [ key , value ] of Object . entries ( installed )) { 58originals . set ( key , Object . getOwnPropertyDescriptor ( globalThis , key )); 59Object . defineProperty ( globalThis , key , { configurable :true , writable :true , value}); 60} 61try { 62return await test ( window ); 63} finally { 64await window . happyDOM . close (); 65for ( const [ key , original ] of originals ) { 66if ( original ) Object . defineProperty ( globalThis , key , original ); 67else 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 ) { 74field . value = text ; 75field . 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 { 80return ! target . dispatchEvent ( 81new 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 => ({ 87tid :"3mxd6epszps5d" , 88model :null , 89route :"" , 90latest :null , 91href :( route ) => route , 92go () {}, 93request :() => Promise . reject ( new Error ( "no handlers here" )), 94connect :() => { 95throw new Error ( "no channels here" ); 96}, 97command :() => 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 () { 108const requests :FakeRequest [] = []; 109let respond :( request :FakeRequest , signal ?:AbortSignal ) => unknown = ({ method}) => 110method === "GET" ?[] :{}; 111let socket :FakeSocket | undefined ; 112class FakeSocket { 113static OPEN = 1 ; 114readyState = 1 ; 115onopen ?:() => void ; 116onmessage ?:( event :{ data :string }) => void ; 117onclose ?:( event :{ code :number }) => void ; 118constructor () { 119socket = this ; 120} 121send () {} 122close () { 123this . readyState = 3 ; 124this . onclose ?.({ code :1000 }); 125} 126} 127const deliver = ( message :object ) => socket ?. onmessage ?.({ data :JSON . stringify ( message ) }); 128return { 129 requests, 130/** The requests that change something. */ 131changes :() => requests . filter (( request ) => request . method !== "GET" ), 132respond ( handler :typeof respond ) { 133respond = handler ; 134}, 135 deliver, 136snapshot :( topic :string , value :unknown ) => deliver ({ t :"snapshot" , topic, value}), 137globals :{ 138WebSocket :FakeSocket , 139fetch :async ( path :string , init ?:RequestInit ) => { 140const request = { 141method :init ?. method ?? "GET" , 142 path, 143body :init ?. body === undefined ?undefined :JSON . parse ( String ( init . body )), 144}; 145requests . push ( request ); 146const answer = await respond ( request , init ?. signal ?? undefined ); 147return answer instanceof Response ?answer :Response . json ( answer ); 148}, 149}, 150}; 151} 152 153export type FakeBerry = ReturnType < typeof fakeBerry >;