| 1 | // Run with playwright-cli run-code --filename after opening the disposable UX relay. |
| 2 | async page => { |
| 3 | const base = await page.evaluate(()=>location.origin); |
| 4 | const context = await page.context().browser().newContext({serviceWorkers:'block' ,viewport:{width:1280,height:900}}); |
| 5 | const probe=await context.newPage(), results=[], errors=[]; |
| 6 | probe.on('pageerror' ,error=>errors.push(error.message)); |
| 7 | const check=(ok,message)=>{if(!ok)throw Error(message);}; |
| 8 | const run=async(name,action)=>{try{results.push({name,ok:true,detail:await action()});}catch(error){results.push({name,ok:false,error:error.message});}}; |
| 9 | try { |
| 10 | await probe.goto(base+'/' ); |
| 11 | await probe.evaluate(()=>window.__uxMarker='same' ); |
| 12 | await run('network and HTTP failures leave the current page usable' ,async()=>{ |
| 13 | await probe.route('**/articles' ,route=>route.fulfill({status:503,body:'Temporary failure' })); |
| 14 | await probe.locator('#rail' ).getByRole('link' ,{name:'/articles' ,exact:true}).click(); |
| 15 | await probe.waitForFunction(()=>document.querySelector('#navigation-status' )?.textContent.includes('Unable' )); |
| 16 | check(probe.url()===base+'/' ,'HTTP error changed the URL' ); |
| 17 | await probe.unroute('**/articles' ); |
| 18 | await probe.route('**/articles' ,route=>route.abort()); |
| 19 | await probe.locator('#rail' ).getByRole('link' ,{name:'/articles' ,exact:true}).click(); |
| 20 | await probe.waitForFunction(()=>document.querySelector('#navigation-status' )?.textContent.includes('Unable' )); |
| 21 | check(await probe.locator('#content' ).getAttribute('aria-busy' )!=='true' ,'network error leaves busy state' ); |
| 22 | await probe.unroute('**/articles' ); |
| 23 | }); |
| 24 | await run('slow earlier navigation cannot overwrite the latest click' ,async()=>{ |
| 25 | let finish; |
| 26 | const gate=new Promise(resolve=>{finish=resolve;}); |
| 27 | await probe.route('**/articles' ,async route=>{await gate;await route.continue().catch(()=>{});}); |
| 28 | await probe.locator('#rail' ).getByRole('link' ,{name:'/articles' ,exact:true}).click(); |
| 29 | await probe.locator('#rail' ).getByRole('link' ,{name:'/repos' ,exact:true}).click(); |
| 30 | await probe.waitForURL(base+'/repos' ); |
| 31 | finish(); |
| 32 | await probe.unroute('**/articles' ); |
| 33 | check(await probe.locator('#content h1' ).textContent()==='Repositories' ,'stale response replaced content' ); |
| 34 | }); |
| 35 | await run('sign-in errors use current status after a shell swap' ,async()=>{ |
| 36 | await probe.locator('#who' ).getByRole('link' ,{name:'Sign in' ,exact:true}).click(); |
| 37 | await probe.waitForURL(base+'/signin' ); |
| 38 | await probe.locator('#session-login' ).click(); |
| 39 | await probe.waitForFunction(()=>document.querySelector('#signer-status' )?.textContent.includes('Connect a Nostr signer first' )); |
| 40 | await probe.locator('#bunker-url' ).fill('https://example.invalid' ); |
| 41 | await probe.locator('#bunker button' ).click(); |
| 42 | await probe.waitForFunction(()=>document.querySelector('#signer-status' )?.textContent.includes('valid bunker URL' )); |
| 43 | check(await probe.evaluate(()=>window.__uxMarker)==='same' ,'sign-in navigation reloaded' ); |
| 44 | }); |
| 45 | await run('modified clicks retain native new-tab behavior' ,async()=>{ |
| 46 | const newTab=context.waitForEvent('page' ); |
| 47 | await probe.locator('#rail' ).getByRole('link' ,{name:'/repos' ,exact:true}).click({modifiers:['ControlOrMeta' ]}); |
| 48 | const opened=await newTab; |
| 49 | await opened.waitForLoadState(); |
| 50 | check(probe.url()===base+'/signin' ,'modified click navigated the current page' ); |
| 51 | check(opened.url()===base+'/repos' ,'modified click opened wrong URL' ); |
| 52 | await opened.close(); |
| 53 | }); |
| 54 | await run('seeded sequence of 60 read-only navigation actions' ,async()=>{ |
| 55 | let seed=0x74696e79; |
| 56 | const routes=['/home' ,'/search' ,'/repos' ,'/files' ,'/sites' ,'/articles' ,'/inbox' ,'/outbox' ]; |
| 57 | for(let i=0;i<60;i++){ |
| 58 | seed=(Math.imul(seed,1664525)+1013904223)>>>0; |
| 59 | const name=routes[(seed>>>16)%routes.length], path=name==='/home' ?'/' :name; |
| 60 | await probe.locator('#rail' ).getByRole('link' ,{name,exact:true}).click(); |
| 61 | await probe.waitForURL(base+path); |
| 62 | await probe.waitForFunction(()=>document.querySelector('#content' )?.getAttribute('aria-busy' )!=='true' ); |
| 63 | const state=await probe.evaluate(()=>({marker:window.__uxMarker,ids:[...document.querySelectorAll('[id]' )].map(e=>e.id),classes:document.querySelectorAll('[class]' ).length,sections:document.querySelectorAll('#content>section' ).length})); |
| 64 | check(state.marker==='same' ,'document reloaded at ' +i); |
| 65 | check(state.classes===0,'class attributes introduced at ' +i); |
| 66 | check(new Set(state.ids).size===state.ids.length,'duplicate DOM IDs at ' +i); |
| 67 | check(state.sections===1,'content section missing at ' +i); |
| 68 | } |
| 69 | return {seed:'0x74696e79' ,actions:60}; |
| 70 | }); |
| 71 | } finally { await context.close(); } |
| 72 | results.push({name:'uncaught browser errors' ,ok:errors.length===0,errors}); |
| 73 | return {passed:results.filter(r=>r.ok).length,failed:results.filter(r=>!r.ok).length,results}; |
| 74 | } |