frontend-patterns/research/experiments/boundary-recovery/routing-development-runner.mjs
tegwick 29ccad7002 Test lifecycle-aware Escape ownership routing without settling delays
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
2026-09-26 22:58:07 +02:00

46 lines
5.9 KiB
JavaScript

import {build} from 'esbuild';
import {chromium, firefox} from 'playwright';
import {createServer} from 'node:http';
import {once} from 'node:events';
import {readFile, writeFile} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import {fileURLToPath} from 'node:url';
import assert from 'node:assert/strict';
import {routeEscape} from './routing-transform.mjs';
const dir=fileURLToPath(new URL('.',import.meta.url));
const hash=x=>createHash('sha256').update(x).digest('hex');
const source=await readFile(dir+'node_modules/@radix-ui/react-dismissable-layer/dist/index.mjs','utf8');
assert.equal(hash(source),JSON.parse(await readFile(dir+'provenance.json')).inspected_files['@radix-ui/react-dismissable-layer/dist/index.mjs'].sha256);
const bundles=new Map();
for(const mode of ['development','production']) for(const variant of ['raw','routed']) {
const b=await build({entryPoints:[dir+'routing-fixture.jsx'],bundle:true,write:false,format:'iife',define:{'process.env.NODE_ENV':JSON.stringify(mode)},plugins:variant==='raw'?[]:[{name:'experimental-escape-router',setup(b){b.onLoad({filter:/@radix-ui\/react-dismissable-layer\/dist\/index\.mjs$/},()=>({contents:routeEscape(source),loader:'js',resolveDir:dir+'node_modules/@radix-ui/react-dismissable-layer/dist'}));}}]});
bundles.set(mode+'-'+variant,b.outputFiles[0].contents);
}
const server=createServer((req,res)=>{const u=new URL(req.url,'http://localhost');if(u.pathname==='/bundle.js'){res.setHeader('Content-Type','application/javascript');res.end(bundles.get(u.searchParams.get('key')));}else{res.setHeader('Content-Type','text/html');res.end(`<!doctype html><html lang="en"><title>Routing experiment</title><body><div id="root"></div><script src="/bundle.js?key=${u.searchParams.get('key')}"></script></body></html>`);}});
server.listen(0,'127.0.0.1');await once(server,'listening');
const results=[];
async function focused(page,id){await page.waitForFunction(id=>document.activeElement?.id===id,id,{timeout:1200});}
async function outer(page){await page.locator('#outer-trigger').focus();await page.keyboard.press('Enter');await focused(page,'inner-trigger');}
async function inner(page){await outer(page);await page.keyboard.press('Enter');await focused(page,'inner-close');}
const cases={
'ordinary':async p=>{await outer(p);await p.keyboard.press('Escape');await focused(p,'outer-trigger');assert.deepEqual(await p.evaluate(()=>window.routingObserved.escapes),['outer']);},
'nested-immediate':async p=>{await inner(p);await p.keyboard.press('Escape');await focused(p,'inner-trigger');assert.equal(await p.getByRole('dialog').count(),1);await p.keyboard.press('Escape');await focused(p,'outer-trigger');assert.deepEqual(await p.evaluate(()=>window.routingObserved.escapes),['inner','outer']);},
'cancel-inner':async p=>{await inner(p);await p.keyboard.press('Escape');assert.deepEqual(await p.evaluate(()=>window.routingObserved.escapes),['inner']);assert.equal(await p.getByRole('dialog').count(),2);await focused(p,'inner-close');},
'remove-trigger':async p=>{await inner(p);await p.keyboard.press('Tab');await focused(p,'remove-trigger');await p.keyboard.press('Enter');await focused(p,'outer-close');await p.keyboard.press('Escape');await focused(p,'outer-trigger');},
'remove-and-reopen':async p=>{await inner(p);await p.evaluate(()=>window.removeInner());await focused(p,'inner-trigger');await p.keyboard.press('Enter');await focused(p,'inner-close');await p.keyboard.press('Escape');await focused(p,'inner-trigger');assert.deepEqual(await p.evaluate(()=>window.routingObserved.escapes),['inner']);},
'ancestor-teardown':async p=>{await inner(p);await p.evaluate(()=>window.removeTask());await p.getByRole('dialog').waitFor({state:'detached'});await p.locator('#next').focus();await p.keyboard.press('Escape');assert.deepEqual(await p.evaluate(()=>window.routingObserved.escapes),[]);await focused(p,'next');}
};
try{
for(const [engine,launcher]of [['chromium',chromium],['firefox',firefox]]){
const browser=await launcher.launch({headless:true,...(engine==='chromium'?{executablePath:process.env.FEP_CHROMIUM_PATH}: {})});
try{for(const mode of ['development','production'])for(const variant of ['raw','routed'])for(const [name,run]of Object.entries(cases)){
const page=await browser.newPage();page.setDefaultTimeout(2000);const errors=[];page.on('pageerror',e=>errors.push(e.message));const r={engine,browser:browser.version(),mode,variant,case:name,status:'pass'};
try{await page.goto(`http://127.0.0.1:${server.address().port}/?key=${mode}-${variant}${name==='cancel-inner'?'&cancel=1':''}`);await run(page);assert.deepEqual(errors,[]);}catch(e){r.status='fail';r.error=e.message;}
r.observed=await page.evaluate(()=>({active:document.activeElement?.id,dialogs:document.querySelectorAll('[role=dialog]').length,...window.routingObserved}));r.browserErrors=errors;results.push(r);await page.close();
await writeFile('/tmp/fep-routing-checkpoint.json',JSON.stringify({status:'incomplete',results},null,2)+'\n');
}}finally{await browser.close();}
}
}finally{await new Promise(resolve=>server.close(resolve));}
const files={};for(const name of ['routing.mjs','routing-fixture.jsx','routing-transform.mjs','package.json','package-lock.json'])files[name]=hash(await readFile(dir+name));
const summary=[];for(const engine of ['chromium','firefox'])for(const mode of ['development','production'])for(const variant of ['raw','routed']){const r=results.filter(x=>x.engine===engine&&x.mode===mode&&x.variant===variant);summary.push({engine,mode,variant,passed:r.filter(x=>x.status==='pass').length,total:r.length});}
const output=process.env.FEP_RESULT_PATH||'/tmp/fep-routing.json';await writeFile(output,JSON.stringify({date:new Date().toISOString(),node:process.version,files,sourceSha256:hash(source),transformedSha256:hash(routeEscape(source)),bundleHashes:Object.fromEntries([...bundles].map(([k,v])=>[k,hash(v)])),summary,results},null,2)+'\n');console.log(JSON.stringify({output,summary},null,2));