char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
ece94e9
main
1import { assert , assertEquals } from "@std/assert" ; 2import { withDom } from "@berry/util/dom.ts" ; 3import { rendering } from "./render.tsx" ; 4import type { FileDiff } from "./types.ts" ; 5 6const file = ( lines :string [], path = "file.txt" ) :FileDiff => ({ 7 path, 8from :null , 9status :"modified" , 10modes :{ old :"100644" , new :"100644" }, 11binary :false , 12hunks :[{ old :1 , new :1 , lines}], 13}); 14 15async function page ( 16test :( env :{ 17root :HTMLElement ; 18render :ReturnType < typeof rendering >; 19intersect ( element :Element , near :boolean , visible ?:boolean ) :void ; 20settle () :Promise < void >; 21}) => Promise < void >, 22globals :Record < string , unknown > = {}, 23) { 24const observers :Visibility [] = []; 25class Visibility { 26targets = new Set < Element >(); 27constructor ( 28readonly callback :IntersectionObserverCallback , 29readonly options ?:IntersectionObserverInit , 30) { 31observers . push ( this ); 32} 33observe ( target :Element ) { 34this . targets . add ( target ); 35} 36unobserve ( target :Element ) { 37this . targets . delete ( target ); 38} 39disconnect () { 40this . targets . clear (); 41} 42} 43await withDom ( async ( window ) => { 44const root = document . createElement ( "div" ); 45document . body . append ( root ); 46const render = rendering ( root ); 47try { 48await test ({ 49 root, 50 render, 51intersect ( element , near , visible = near ) { 52for ( const observer of observers ) { 53if ( ! observer . targets . has ( element )) continue ; 54observer . callback ( 55[{ 56target :element , 57isIntersecting :observer . options ?. rootMargin ?near :visible , 58} as IntersectionObserverEntry ], 59observer as unknown as IntersectionObserver , 60); 61} 62}, 63settle :() => window . happyDOM . waitUntilComplete (), 64}); 65} finally { 66render . dispose (); 67} 68for ( const observer of observers ) assertEquals ( observer . targets . size , 0 ); 69}, { globals :{ IntersectionObserver :Visibility , ...globals } }); 70} 71 72Deno . test ( "large diffs keep all text searchable but only nearby chunks have rich rows" , () => 73page ( async ({ root, render, intersect, settle}) => { 74const lines = Array . from ({ length :10_000 }, ( _ , i ) => `+unique line ${ i } ` ); 75const content = render . file ( file ( lines ), "unified" , new Set (), () => {}); 76root . append ( content . element ); 77const chunks = [ ...root . querySelectorAll ( ".diff-chunk" )]; 78assert ( chunks . length > 10 ); 79assertEquals ( root . querySelectorAll ( "table" ). length , 0 ); 80assert ( root . textContent ! . includes ( "unique line 9999" )); 81assert ( root . querySelectorAll ( "*" ). length < 1000 ); 82 83let previous :Element | undefined ; 84for ( const chunk of [ chunks [ 0 ], chunks [ 1 ], chunks . at ( - 1 ) ! ]) { 85if ( previous ) intersect ( previous , false ); 86intersect ( chunk , true ); 87await settle (); 88assertEquals ( root . querySelectorAll ( "table" ). length , 1 ); 89assert ( root . querySelectorAll ( "tr" ). length < 200 ); 90assert ( root . textContent ! . includes ( "unique line 9999" )); 91if ( previous ) assert ( previous . querySelector ( "pre" )); 92previous = chunk ; 93} 94intersect ( chunks [ 0 ], true ); 95content . dispose (); 96await settle (); 97assertEquals ( chunks [ 0 ]. querySelector ( "table" ), null ); 98})); 99 100Deno . test ( "hydrating and evicting chunks leave native find matches and selections intact" , () => 101page ( async ({ root, render, intersect, settle}) => { 102const content = render . file ( file ([ "+a searchable needle" ]), "unified" , new Set (), () => {}); 103root . append ( content . element ); 104const chunk = root . querySelector ( ".diff-chunk" ) ! ; 105const pre = chunk . querySelector ( "pre" ) ! ; 106const selection = document . getSelection () ! ; 107const range = document . createRange (); 108range . setStart ( pre . firstChild ! , 2 ); 109range . setEnd ( pre . firstChild ! , 12 ); 110selection . addRange ( range ); 111intersect ( chunk , true ); 112await settle (); 113assertEquals ( chunk . querySelector ( "pre" ), pre ); 114assertEquals ( selection . toString (), "searchable" ); 115 116selection . removeAllRanges (); 117document . dispatchEvent ( new Event ( "selectionchange" )); 118await settle (); 119const table = chunk . querySelector ( "table" ) ! ; 120assert ( table ); 121range . selectNodeContents ( table . querySelector ( "td.text" ) ! ); 122selection . addRange ( range ); 123intersect ( chunk , false ); 124await settle (); 125assertEquals ( chunk . querySelector ( "table" ), table ); 126assertEquals ( selection . toString (), "a searchable needle" ); 127selection . removeAllRanges (); 128document . dispatchEvent ( new Event ( "selectionchange" )); 129await settle (); 130assertEquals ( chunk . querySelector ( "table" ), null ); 131assertEquals ( chunk . querySelector ( "pre" ) ! . textContent , "a searchable needle\n" ); 132content . dispose (); 133})); 134 135Deno . test ( "line pairing and word changes survive chunk boundaries in either layout" , async () => { 136for ( const layout of [ "unified" , "split" ] as const ) { 137await page ( async ({ root, render, intersect, settle}) => { 138const lines = [ 139 ...Array . from ({ length :150 }, ( _ , i ) => `-const before_ ${ i } = value;` ), 140 ...Array . from ({ length :150 }, ( _ , i ) => `+const after_ ${ i } = value;` ), 141]; 142const content = render . file ( file ( lines ), layout , new Set (), () => {}); 143root . append ( content . element ); 144for ( const chunk of root . querySelectorAll ( ".diff-chunk" )) intersect ( chunk , true ); 145await settle (); 146assertEquals ( root . querySelectorAll ( "td.text.del" ). length , 150 ); 147assertEquals ( root . querySelectorAll ( "td.text.add" ). length , 150 ); 148assertEquals ( root . querySelector ( "td.del mark.word" ) ! . textContent , "before_0" ); 149assertEquals ( root . querySelector ( "td.add mark.word" ) ! . textContent , "after_0" ); 150if ( layout === "split" ) { 151for ( const [ i , row ] of [ ...root . querySelectorAll ( "tr" )]. entries ()) { 152assertEquals ( row . querySelector ( ".del" ) ! . textContent , `const before_ ${ i } = value;` ); 153assertEquals ( row . querySelector ( ".add" ) ! . textContent , `const after_ ${ i } = value;` ); 154} 155} 156content . dispose (); 157}); 158} 159}); 160 161Deno . test ( "syntax work waits for visible chunks, skips departed files, and releases offscreen results" , async () => { 162const requests :{ id :number ; source :string }[] = []; 163let worker :Highlighter | undefined ; 164class Highlighter { 165onmessage ?:( event :{ data :{ id :number ; html :string } }) => void ; 166constructor () { 167worker = this ; 168} 169postMessage ( request :typeof requests [ number ]) { 170requests . push ( request ); 171} 172reply () { 173for ( const { id, source} of requests . splice ( 0 )) { 174this . onmessage ?.({ data :{ id, html :`<a-k> ${ source } </a-k>` } }); 175} 176} 177} 178await page ( async ({ root, render, intersect, settle}) => { 179const first = render . file ( file ([ "-old" , "+new" ], "first.ts" ), "unified" , new Set (), () => {}); 180const second = render . file ( file ([ "+second" ], "second.ts" ), "unified" , new Set (), () => {}); 181root . append ( first . element , second . element ); 182const a = first . element . querySelector ( ".diff-chunk" ) ! ; 183const b = second . element . querySelector ( ".diff-chunk" ) ! ; 184intersect ( a , true , false ); 185await settle (); 186assertEquals ( worker , undefined ); 187intersect ( a , true ); 188intersect ( b , true ); 189await settle (); 190assertEquals ( requests . map (( request ) => request . source ), [ "old" , "new" ]); 191intersect ( a , false ); 192intersect ( b , false ); 193await settle (); 194worker ! . reply (); 195await settle (); 196assertEquals ( requests . length , 0 ); 197assertEquals ( root . querySelectorAll ( "a-k, table" ). length , 0 ); 198 199intersect ( a , true ); 200await settle (); 201assertEquals ( requests . map (( request ) => request . source ), [ "old" , "new" ]); 202const table = a . querySelector ( "table" ) ! ; 203const selection = document . getSelection () ! ; 204const range = document . createRange (); 205range . selectNodeContents ( a . querySelector ( "td.text" ) ! ); 206selection . addRange ( range ); 207worker ! . reply (); 208await settle (); 209assertEquals ( a . querySelector ( "table" ), table ); 210assertEquals ( a . querySelector ( "a-k" ), null ); 211assertEquals ( selection . toString (), "old" ); 212selection . removeAllRanges (); 213document . dispatchEvent ( new Event ( "selectionchange" )); 214await settle (); 215assertEquals ( a . querySelector ( "a-k" )?. textContent , "old" ); 216intersect ( a , true ); 217await settle (); 218assertEquals ( requests . length , 0 ); 219 220intersect ( b , true ); 221await settle (); 222assertEquals ( requests . map (( request ) => request . source ), [ "second" ]); 223second . dispose (); 224second . element . remove (); 225worker ! . reply (); 226await settle (); 227assertEquals ( second . element . querySelector ( "a-k" ), null ); 228first . dispose (); 229}, { Worker :Highlighter }); 230});