char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
c823fd4
main
1import { assertEquals } from "@std/assert" ; 2import { withDom } from "@berry/util/dom.ts" ; 3import { 4announcements , 5type Notice , 6notificationsAvailable , 7notificationsOn , 8setNotifications , 9} from "./notifications.ts" ; 10 11/** What a page's browser shows. */ 12const shown :FakeNotification [] = []; 13 14class FakeNotification { 15static permission :NotificationPermission = "granted" ; 16static requestPermission = () => Promise . resolve ( "granted" as const ); 17onclick :(() => void ) | null = null ; 18constructor ( readonly title :string , readonly options :{ body ?:string ; tag ?:string }) { 19shown . push ( this ); 20} 21close () {} 22} 23 24/** A page whose browser grants notifications, keeping what it shows. */ 25function withNotifications ( test :() => Promise < void > | void ) { 26shown . length = 0 ; 27return withDom (() => test (), { globals :{ Notification :FakeNotification } }); 28} 29 30function setUp ( options :{ active ?:() => string } = {}) { 31const heard :Notice [] = []; 32const update = announcements ({ 33active :options . active ?? (() => "" ), 34name :( tid ) => `conversation ${ tid } ` , 35speak :( notice ) => heard . push ( notice ), 36}); 37return { heard, update}; 38} 39 40function hidePage ( hidden :boolean ) { 41Object . defineProperty ( document , "visibilityState" , { 42configurable :true , 43get :() => hidden ?"hidden" :"visible" , 44}); 45} 46 47function focusPage ( focused :boolean ) { 48Object . defineProperty ( document , "hasFocus" , { configurable :true , value :() => focused }); 49} 50 51Deno . test ( "a conversation that stops answering is announced by name, with its answer" , async () => { 52await withNotifications ( async () => { 53assertEquals ( await setNotifications ( true ), true ); 54const { heard, update} = setUp (); 55update ({ a :{ busy :true , unread :false } }); 56update ({ a :{ busy :false , unread :true , reply :"the tests pass" } }); 57assertEquals ( heard , [ 58{ tid :"a" , title :"conversation a" , body :"the tests pass" }, 59]); 60}); 61}); 62 63Deno . test ( "the conversation in view is not announced while someone is looking at it" , async () => { 64await withNotifications ( async () => { 65hidePage ( false ); 66focusPage ( true ); 67await setNotifications ( true ); 68const { heard, update} = setUp ({ active :() => "a" }); 69update ({ a :{ busy :true , unread :false } }); 70update ({ a :{ busy :false , unread :true , reply :"done" } }); 71assertEquals ( heard , []); 72 73// the window is still on screen, but the reader has moved on to another one 74focusPage ( false ); 75update ({ a :{ busy :true , unread :false } }); 76update ({ a :{ busy :false , unread :true , reply :"done" } }); 77assertEquals ( heard . map (( notice ) => notice . tid ), [ "a" ]); 78 79hidePage ( true ); 80focusPage ( true ); 81update ({ a :{ busy :true , unread :false } }); 82update ({ a :{ busy :false , unread :true , reply :"done" } }); 83assertEquals ( heard . map (( notice ) => notice . tid ), [ "a" , "a" ]); 84}); 85}); 86 87Deno . test ( "turns still running, and turns returning control without an answer, are not announced" , async () => { 88await withNotifications ( async () => { 89await setNotifications ( true ); 90const { heard, update} = setUp (); 91update ({ a :{ busy :true , unread :false } }); 92update ({ a :{ busy :true , unread :false } }); 93// an aborted turn settles with nothing new to read 94update ({ a :{ busy :false , unread :false } }); 95update ({ b :{ busy :false , unread :true , reply :"already here" } }); 96assertEquals ( heard , []); 97}); 98}); 99 100Deno . test ( "nothing is announced until the switch is on" , async () => { 101await withNotifications (() => { 102const { heard, update} = setUp (); 103update ({ a :{ busy :true , unread :false } }); 104update ({ a :{ busy :false , unread :true , reply :"done" } }); 105assertEquals ( heard , []); 106}); 107}); 108 109Deno . test ( "a page without the API leaves announcements off rather than throwing" , async () => { 110await withDom ( async () => { 111assertEquals ( notificationsAvailable (), false ); 112assertEquals ( await setNotifications ( true ), false ); 113assertEquals ( notificationsOn (), false ); 114// a flag left on from another page's choice must not reach a missing Notification here 115localStorage . setItem ( "berry.notifications" , "on" ); 116const update = announcements ({ active :() => "" , name :( tid ) => tid }); 117update ({ a :{ busy :true , unread :false } }); 118update ({ a :{ busy :false , unread :true , reply :"done" } }); 119}); 120}); 121 122Deno . test ( "a clicked notification opens its conversation" , async () => { 123await withNotifications ( async () => { 124await setNotifications ( true ); 125const update = announcements ({ active :() => "" , name :( tid ) => tid }); 126update ({ a :{ busy :true , unread :false } }); 127update ({ a :{ busy :false , unread :true , reply :"done" } }); 128assertEquals ( shown . length , 1 ); 129assertEquals ( shown [ 0 ]. options . tag , "a" ); 130shown [ 0 ]. onclick ?.(); 131assertEquals ( location . hash , "#/a" ); 132}); 133});