char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
871f159
main
1import { assertEquals , assertMatch } from "@std/assert" ; 2import { existsSync } from "node:fs" ; 3import type { Channel , ChannelMessage } from "@berry/api/web.ts" ; 4import type { Window } from "happy-dom" ; 5import { until } from "@berry/util" ; 6import { viewContext , withDom } from "@berry/util/dom.ts" ; 7import { Ghostty } from "./ghostty.ts" ; 8import { COLORS } from "./palette.ts" ; 9import { OUTPUT , SNAPSHOT , tagged } from "./protocol.ts" ; 10import { terminalView } from "./view.tsx" ; 11 12const WASM = new URL ( "./ghostty-vt.wasm" , import . meta. url ); 13 14const withDocument = ( test :( window :Window ) => Promise < void >) => 15withDom ( test , { 16globals :{ 17requestAnimationFrame :( callback :() => void ) => setTimeout ( callback , 0 ), 18cancelAnimationFrame :( handle :number ) => clearTimeout ( handle ), 19}, 20}); 21 22/** The server's end, played by the test: what the view sent, and a way to answer. */ 23function server () { 24const sent :ChannelMessage [] = []; 25let say ! :ReadableStreamDefaultController < ChannelMessage >; 26const channel :Channel = { 27send :( message ) => void sent . push ( message ), 28messages :new ReadableStream ({ start :( controller ) => void ( say = controller ) }), 29close :() => {}, 30}; 31return { channel, sent, say :( message :ChannelMessage ) => say . enqueue ( message ) }; 32} 33 34const typed = ( sent :ChannelMessage []) => 35sent . filter (( m ) => m instanceof Uint8Array ). map (( m ) => new TextDecoder (). decode ( m as Uint8Array )); 36 37Deno . test ({ 38name :"the view starts a shell on opening, draws what it writes, and sends what is typed" , 39ignore :! existsSync ( WASM ), 40fn :() => 41withDocument ( async ( window ) => { 42const ghostty = await Ghostty . load ( await Deno . readFile ( WASM )); 43const { channel, sent, say} = server (); 44const matchMedia = window . matchMedia . bind ( window ); 45let resolution = matchMedia ( "(resolution: 1dppx)" ); 46window . matchMedia = ( query ) => resolution = matchMedia ( query ); 47const view = terminalView (() => Promise . resolve ( ghostty )); 48document . body . append ( view . element ); 49view . update ( viewContext ({ connect :() => channel })); 50say ({ t :"none" }); 51await until (() => sent . length > 0 ); 52assertEquals ( sent , [{ t :"start" , cols :80 , rows :24 }]); 53 54const shell = ghostty . terminal ( 80 , 24 ); 55shell . colors ( COLORS ); 56say ( tagged ( SNAPSHOT , shell . snapshot ())); 57say ( tagged ( OUTPUT , new TextEncoder (). encode ( "$ \x1b[1mbold\x1b[0m\r\n$ " ))); 58const drawn = () => [ ...view . element . querySelectorAll ( ".terminal-rows > div" )]; 59await until (() => drawn ()[ 1 ]?. textContent === "$" ); 60const rows = drawn (); 61assertEquals ( rows . length , 24 ); 62assertEquals ( rows [ 0 ]. textContent , "$ bold" ); 63const bold = [ ...rows [ 0 ]. children ]. find (( span ) => span . textContent === "bold" ) as HTMLElement ; 64assertEquals ( bold . style . fontWeight , "bold" ); 65assertEquals ( rows [ 1 ]. textContent , "$" ); 66 67// black text on the terminal's own background is drawn so that it reads, as a gray 68say ( tagged ( OUTPUT , new TextEncoder (). encode ( "\x1b[30mdark\x1b[0m\r\n" ))); 69const dark = () => 70[ ...rows [ 1 ]. children ]. find (( span ) => span . textContent === "dark" ) as HTMLElement ; 71await until (() => dark () !== undefined ); 72assertMatch ( dark (). style . color , / ^rgb\((\d+) \1 \1\)$ / ); 73 74const keys = view . element . querySelector ( "textarea" ) ! ; 75const press = ( init :{ key :string ; code :string ; ctrlKey ?:boolean }) => 76keys . dispatchEvent ( 77new window . KeyboardEvent ( "keydown" , { 78bubbles :true , 79cancelable :true , 80 ...init , 81}) as unknown as Event , 82); 83press ({ key :"l" , code :"KeyL" }); 84press ({ key :"c" , code :"KeyC" , ctrlKey :true }); 85press ({ key :"Enter" , code :"Enter" }); 86assertEquals ( typed ( sent ), [ "l" , "\x03" , "\r" ]); 87 88const selection = window . getSelection (); 89const range = window . document . createRange (); 90range . selectNodeContents ( 91rows [ 0 ] as unknown as Parameters < typeof range . selectNodeContents >[ 0 ], 92); 93selection . addRange ( range ); 94assertEquals ( selection . toString (), "$ bold" ); 95const copied :string [] = []; 96const copy = new window . Event ( "copy" , { bubbles :true , cancelable :true }); 97Object . defineProperty ( copy , "clipboardData" , { 98value :{ 99setData :( type :string , text :string ) => type === "text/plain" && copied . push ( text ), 100}, 101}); 102rows [ 0 ]. dispatchEvent ( copy as unknown as Event ); 103assertEquals ( copy . defaultPrevented , true ); 104assertEquals ( copied , [ "$ bold" ]); 105assertEquals ( press ({ key :"c" , code :"KeyC" , ctrlKey :true }), true ); 106assertEquals ( typed ( sent ), [ "l" , "\x03" , "\r" ]); 107const canvas = view . element . querySelector ( "canvas" ) ! ; 108const width = canvas . width ; 109const messages = sent . length ; 110Object . defineProperty ( window , "devicePixelRatio" , { value :1.25 , configurable :true }); 111resolution . dispatchEvent ( new window . Event ( "change" )); 112await until (() => canvas . width !== width ); 113assertEquals ( canvas . width , Math . round ( width * 1.25 )); 114assertEquals ( selection . toString (), "$ bold" ); 115assertEquals ( sent . length , messages ); 116selection . removeAllRanges (); 117const emptyCopy = new window . Event ( "copy" , { bubbles :true , cancelable :true }); 118Object . defineProperty ( emptyCopy , "clipboardData" , { 119value :{ setData :( _type :string , text :string ) => copied . push ( text ) }, 120}); 121rows [ 0 ]. dispatchEvent ( emptyCopy as unknown as Event ); 122assertEquals ( emptyCopy . defaultPrevented , false ); 123assertEquals ( copied , [ "$ bold" ]); 124assertEquals ( press ({ key :"+" , code :"Equal" , ctrlKey :true }), true ); 125assertEquals ( press ({ key :"-" , code :"Minus" , ctrlKey :true }), true ); 126assertEquals ( press ({ key :"0" , code :"Digit0" , ctrlKey :true }), true ); 127assertEquals ( typed ( sent ), [ "l" , "\x03" , "\r" ]); 128const wheel = new window . WheelEvent ( "wheel" , { deltaY :10 , cancelable :true }); 129Object . defineProperty ( wheel , "ctrlKey" , { value :true }); 130assertEquals ( 131view . element . querySelector ( ".terminal-screen" ) ! . dispatchEvent ( wheel as unknown as Event ), 132true , 133); 134 135say ({ t :"exit" , code :3 }); 136const bar = view . element . querySelector < HTMLElement >( ".terminal-status" ) ! ; 137await until (() => ! bar . hidden ); 138assertEquals ( bar . hidden , false ); 139assertEquals ( bar . querySelector ( "p" ) ! . textContent , "the shell exited with 3" ); 140bar . querySelector ( "button" ) ! . click (); 141assertEquals ( sent . at ( - 1 ), { t :"start" , cols :80 , rows :24 }); 142view . dispose ?.(); 143}), 144}); 145 146Deno . test ({ 147name : 148"a viewer that finds no shell after it has seen one offers to start one rather than starting it" , 149ignore :! existsSync ( WASM ), 150fn :() => 151withDocument ( async () => { 152const ghostty = await Ghostty . load ( await Deno . readFile ( WASM )); 153const { channel, sent, say} = server (); 154const view = terminalView (() => Promise . resolve ( ghostty )); 155view . update ( viewContext ({ connect :() => channel })); 156say ({ t :"exit" , code :0 }); 157say ({ t :"none" }); 158await until (() => view . element . querySelector ( "p" ) ! . textContent ! . startsWith ( "no shell" )); 159assertEquals ( sent , []); 160assertEquals ( view . element . querySelector ( "p" ) ! . textContent , "no shell is running here" ); 161view . dispose ?.(); 162}), 163});