Test competing navigation ownership and review research thread
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
This commit is contained in:
parent
e484cae45f
commit
e8ccb73a12
22 changed files with 4981 additions and 13 deletions
|
|
@ -53,3 +53,24 @@ Raw rows observe behavior, so 22 passing rows do not mean raw and managed varian
|
|||
meet identical focus/feedback obligations. See NF01–NF06. The policy is authored
|
||||
experimental code; DOM focus and live-region text do not establish assistive-technology
|
||||
announcements or conformance. History and request-race limits remain explicit.
|
||||
|
||||
## Competing completion follow-up and pause
|
||||
|
||||
[LEARN-0013 protocol](../../evolution/cycles/2026-09-27-navigation-ownership.md),
|
||||
[review](../../reviews/NavigationOwnershipExperiment.md), [receipt](ownership-result.json).
|
||||
Use the same browser environment with `node ownership.mjs`; choose a new
|
||||
`FEP_RESULT_PATH` (default `/tmp/fep-ownership-result.json`). Six scenarios × two
|
||||
policies × two browsers: 24 completed rows, all obligations met by guard 12/12 and
|
||||
previous policy 0/12. These are deliberately adversarial cases, not general quality
|
||||
scores. Server barriers impose schedules; client completions are observed without
|
||||
aborting transport or claiming remote rollback.
|
||||
|
||||
The previous focus-policy.js remains byte-for-byte unchanged. New ownership-policy.js
|
||||
uses a generation plus separate focus ownership; its history XHR handler wrapping is
|
||||
explicitly tied to pinned internals. Initial runner/result retained under
|
||||
ownership-initial.mjs / ownership-initial-result.json: four Firefox keyboard-helper
|
||||
failures corrected by backward traversal, without changing policies or obligations.
|
||||
Initial receipt's ownership.mjs hash corresponds to ownership-initial.mjs.
|
||||
|
||||
The [thread review](../../reviews/NavigationThreadReview.md) pauses fixture expansion
|
||||
and hands the next evidence question to an independent pinned application source study.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,64 @@
|
|||
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 assert from 'node:assert/strict';
|
||||
const dir=new URL('.',import.meta.url);
|
||||
const assets=new Map(await Promise.all([['/htmx.js','node_modules/htmx.org/dist/htmx.js'],['/baseline.js','focus-policy.js'],['/guarded.js','ownership-policy.js']].map(async([url,file])=>[url,await readFile(new URL(file,dir))])));
|
||||
const trace=`window.raceLog=[];for(const name of ['htmx:beforeRequest','htmx:historyCacheMiss'])document.addEventListener(name,e=>{const xhr=e.detail.xhr;xhr.addEventListener('load',()=>window.raceLog.push({event:'loaded',url:xhr.responseURL,status:xhr.status}));});`;
|
||||
let requests=[],barriers=new Map();
|
||||
function barrier(key){let resolve;const reached=new Promise(r=>resolve=r);const b={reached,resolve,release:null};barriers.set(key,b);return b;}
|
||||
const server=createServer((req,res)=>{
|
||||
const u=new URL(req.url,'http://local');res.setHeader('Cache-Control','no-store');
|
||||
if(assets.has(u.pathname)){res.setHeader('Content-Type','application/javascript');res.end(assets.get(u.pathname));return;}
|
||||
if(u.pathname==='/trace.js'){res.setHeader('Content-Type','application/javascript');res.end(trace);return;}
|
||||
if(u.pathname!=='/articles'){res.writeHead(404);res.end('Not found');return;}
|
||||
const topic=['alpha','beta','gamma'].includes(u.searchParams.get('topic'))?u.searchParams.get('topic'):'alpha';
|
||||
const mode=u.searchParams.get('mode')==='guarded'?'guarded':'managed';
|
||||
const hx=req.headers['hx-request']==='true',history=req.headers['hx-history-restore-request']==='true';
|
||||
const status=hx&&u.searchParams.has('fail')?503:200;
|
||||
const content=`<div id="payload"><h1 id="heading" tabindex="-1">${topic}</h1><p>Article ${topic}</p></div>`;
|
||||
const boost='hx-boost="true" hx-target="#content" hx-select="#payload" hx-swap="innerHTML"';
|
||||
const href=(t,extra='')=>`/articles?topic=${t}&mode=${mode}${extra}`;
|
||||
const nav=`<nav><a id="beta" href="${href('beta')}" ${boost}>Visit beta</a><a id="old" href="${href('beta','&hold=old')}" ${boost}>Slow beta</a><a id="oldfail" href="${href('beta','&hold=old&fail=1')}" ${boost}>Slow failed beta</a><a id="new" href="${href('gamma')}" ${boost}>Visit gamma</a><a id="newfail" href="${href('gamma','&fail=1')}" ${boost}>Failed gamma</a><a id="recover" href="${href('gamma')}">Native recovery</a></nav>`;
|
||||
const full=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${topic} — Catalog</title><meta name="htmx-config" content='{"historyRestoreAsHxRequest":false}'><script src="/trace.js" defer></script><script src="/${mode==='guarded'?'guarded':'baseline'}.js" defer></script><script src="/htmx.js" defer></script></head><body>${nav}<main id="content" hx-history-elt>${content}</main><label>Persistent search <input id="search"></label><p id="feedback" role="status" aria-live="polite"></p></body></html>`;
|
||||
const record={path:req.url,hx,history,status,held:false,released:false};requests.push(record);
|
||||
const send=()=>{record.released=true;res.writeHead(status,{'Content-Type':'text/html'});res.end(status===503?'Injected failure':hx&&!history?content:full);};
|
||||
const key=history?'history':u.searchParams.get('hold');const b=barriers.get(key);
|
||||
if(b){record.held=true;b.release=send;b.resolve();}else send();
|
||||
});
|
||||
server.listen(0,'127.0.0.1');await once(server,'listening');const base=`http://127.0.0.1:${server.address().port}`;
|
||||
const results=[],browsers={},output=process.env.FEP_RESULT_PATH||'/tmp/fep-ownership-result.json';
|
||||
const inspect=p=>p.evaluate(()=>({topic:document.querySelector('#heading')?.textContent,url:location.pathname+location.search,title:document.title,focus:document.activeElement.id||document.activeElement.tagName,search:document.querySelector('#search').value,feedback:document.querySelector('#feedback').textContent}));
|
||||
async function tabTo(p,id){for(let n=0;n<15;n++){if((await inspect(p)).focus===id)return;await p.keyboard.press('Tab');}throw Error(`Cannot Tab to ${id}`);}
|
||||
async function activate(p,id){await tabTo(p,id);await p.keyboard.press('Enter');}
|
||||
async function selected(p,t){await p.waitForFunction(t=>document.querySelector('#heading')?.textContent===t,t);await p.waitForFunction(t=>document.querySelector('#feedback').textContent===`Loaded ${t}`,t);}
|
||||
async function released(p,b,history=false){b.release();b.release=null;await p.waitForFunction(history=>window.raceLog.some(e=>e.event==='loaded'&&(history?e.url.includes('topic=alpha'):e.url.includes('hold=old'))),history);}
|
||||
async function held(b){await Promise.race([b.reached,new Promise((_,reject)=>setTimeout(()=>reject(Error('Server barrier not reached')),4000))]);}
|
||||
function same(a,b,keys){return keys.every(k=>a[k]===b[k]);}
|
||||
try{
|
||||
for(const [engine,launcher]of [['chromium',chromium],['firefox',firefox]]){
|
||||
const path=engine==='chromium'?process.env.FEP_CHROMIUM_PATH:undefined;const browser=await launcher.launch({headless:true,...(path?{executablePath:path}:{})});
|
||||
browsers[engine]={version:browser.version(),executable:path||launcher.executablePath(),sha256:createHash('sha256').update(await readFile(path||launcher.executablePath())).digest('hex')};
|
||||
try{for(const mode of ['managed','guarded']){
|
||||
async function scenario(id,fn){
|
||||
requests=[];barriers=new Map();const context=await browser.newContext();const p=await context.newPage();p.setDefaultTimeout(4500);
|
||||
const r={id,engine,mode,status:'pass'};
|
||||
try{await p.goto(base+`/articles?topic=alpha&mode=${mode}`);await p.waitForFunction(()=>window.htmx?.version==='2.0.11');Object.assign(r,await fn(p));r.obligationsMet=Object.values(r.obligations).every(Boolean);if(mode==='guarded')assert(r.obligationsMet,JSON.stringify(r.obligations));}
|
||||
catch(e){r.status='fail';r.error=e.message;}
|
||||
r.final=await inspect(p).catch(()=>null);r.events=await p.evaluate(()=>window.probe).catch(()=>[]);r.completions=await p.evaluate(()=>window.raceLog).catch(()=>[]);r.requests=[...requests];results.push(r);
|
||||
for(const b of barriers.values())if(b.release){b.release();b.release=null;}await context.close();await writeFile(output,JSON.stringify({status:'incomplete',results},null,2)+'\n');
|
||||
}
|
||||
async function backMiss(p){await activate(p,'beta');await selected(p,'beta');await p.evaluate(()=>sessionStorage.removeItem('htmx-history-cache'));const b=barrier('history');await p.goBack();await held(b);return b;}
|
||||
await scenario('O01-history-focus-moved',async p=>{const b=await backMiss(p);await tabTo(p,'search');await p.keyboard.type('new task');const before=await inspect(p);await released(p,b,true);const after=await inspect(p);return {before,after,obligations:{selection:after.topic==='alpha'&&new URL(base+after.url).searchParams.get('topic')==='alpha',title:after.title==='alpha — Catalog',focus:after.focus==='search',typedText:after.search==='new task'}};});
|
||||
await scenario('O02-history-superseded',async p=>{const b=await backMiss(p);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b,true);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noFeedbackOrFocusTheft:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O03-old-success',async p=>{const b=barrier('old');await activate(p,'old');await held(b);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noFeedbackOrFocusTheft:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O04-old-error',async p=>{const b=barrier('old');await activate(p,'oldfail');await held(b);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noObsoleteFeedback:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O05-newer-fails',async p=>{const b=barrier('old');await activate(p,'old');await held(b);await activate(p,'newfail');await p.waitForFunction(()=>document.querySelector('#feedback').textContent.includes('Navigation failed'));const before=await inspect(p);assert.equal(before.topic,'alpha');await released(p,b);const after=await inspect(p);return {before,after,obligations:{committedView:same(before,after,['topic','url','title']),newestFailureRetained:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O06-history-newer-than-request',async p=>{await activate(p,'beta');await selected(p,'beta');const b=barrier('old');await activate(p,'old');await held(b);await p.goBack();await selected(p,'alpha');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{historySelection:same(before,after,['topic','url','title']),historyContinuation:same(before,after,['feedback','focus'])}};});
|
||||
}}finally{await browser.close();}
|
||||
}
|
||||
}finally{for(const b of barriers.values())if(b.release)b.release();await new Promise(resolve=>server.close(resolve));}
|
||||
const files={};for(const f of ['ownership.mjs','ownership-policy.js','focus-policy.js','package.json','package-lock.json','node_modules/htmx.org/dist/htmx.js'])files[f]=createHash('sha256').update(await readFile(new URL(f,dir))).digest('hex');
|
||||
const r={date:new Date().toISOString(),node:process.version,kind:'ordered-navigation-completion-races-in-authored-htmx-fixture',files,browsers,results,completed:results.filter(x=>x.status==='pass').length,total:results.length,obligationsMet:results.filter(x=>x.obligationsMet).length};await writeFile(output,JSON.stringify(r,null,2)+'\n');console.log(JSON.stringify({output,completed:r.completed,total:r.total,obligationsMet:r.obligationsMet,failures:results.filter(x=>x.status==='fail')},null,2));if(r.completed!==r.total)process.exitCode=1;
|
||||
31
research/experiments/navigation-history/ownership-policy.js
Normal file
31
research/experiments/navigation-history/ownership-policy.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Experimental adapter pinned to htmx 2.0.11; not a production router.
|
||||
window.probe=[];
|
||||
let generation=0,focusEpoch=0,historyMoveFocus=true;
|
||||
const pending=new WeakMap();
|
||||
document.addEventListener('focusin',()=>focusEpoch++);
|
||||
window.addEventListener('popstate',()=>{generation++;},true);
|
||||
const notice=text=>{document.querySelector('#feedback').textContent=text;};
|
||||
function complete(move){const h=document.querySelector('#heading');document.title=`${h.textContent} — Catalog`;notice(`Loaded ${h.textContent}`);if(move)h.focus();}
|
||||
document.addEventListener('htmx:beforeRequest',e=>{
|
||||
if(e.detail.target.id==='content')pending.set(e.detail.xhr,{owner:++generation,source:e.detail.elt,move:false});
|
||||
});
|
||||
document.addEventListener('htmx:beforeOnLoad',e=>{
|
||||
const state=pending.get(e.detail.xhr);
|
||||
if(state&&state.owner!==generation){e.preventDefault();pending.delete(e.detail.xhr);window.raceLog.push({event:'discard-navigation',url:e.detail.xhr.responseURL});}
|
||||
});
|
||||
document.addEventListener('htmx:beforeSwap',e=>{const state=pending.get(e.detail.xhr);if(state)state.move=document.activeElement===state.source;});
|
||||
document.addEventListener('htmx:afterSwap',e=>{const state=pending.get(e.detail.xhr);if(state){complete(state.move);pending.delete(e.detail.xhr);}});
|
||||
document.addEventListener('htmx:responseError',e=>{if(pending.has(e.detail.xhr)){notice('Navigation failed. Current article retained. Use Native recovery to continue.');pending.delete(e.detail.xhr);}});
|
||||
document.addEventListener('htmx:historyCacheHit',()=>{historyMoveFocus=true;});
|
||||
document.addEventListener('htmx:historyCacheMiss',e=>{
|
||||
const owner=generation,focusAtStart=focusEpoch,path=e.detail.path;
|
||||
const xhr=e.detail.xhr,original=xhr.onload;
|
||||
// The public miss event exposes XHR; wrapping its existing handler relies on the pinned implementation.
|
||||
xhr.onload=function(event){
|
||||
if(owner!==generation||location.pathname+location.search!==path){window.raceLog.push({event:'discard-history',url:xhr.responseURL});return;}
|
||||
historyMoveFocus=focusEpoch===focusAtStart;
|
||||
return original.call(this,event);
|
||||
};
|
||||
});
|
||||
document.addEventListener('htmx:historyRestore',()=>complete(historyMoveFocus));
|
||||
for(const name of ['htmx:afterSettle','htmx:historyRestore','htmx:responseError'])document.addEventListener(name,()=>window.probe.push({name,focus:document.activeElement.id||document.activeElement.tagName,title:document.title,url:location.href,feedback:document.querySelector('#feedback').textContent}));
|
||||
2301
research/experiments/navigation-history/ownership-result.json
Normal file
2301
research/experiments/navigation-history/ownership-result.json
Normal file
File diff suppressed because it is too large
Load diff
64
research/experiments/navigation-history/ownership.mjs
Normal file
64
research/experiments/navigation-history/ownership.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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 assert from 'node:assert/strict';
|
||||
const dir=new URL('.',import.meta.url);
|
||||
const assets=new Map(await Promise.all([['/htmx.js','node_modules/htmx.org/dist/htmx.js'],['/baseline.js','focus-policy.js'],['/guarded.js','ownership-policy.js']].map(async([url,file])=>[url,await readFile(new URL(file,dir))])));
|
||||
const trace=`window.raceLog=[];for(const name of ['htmx:beforeRequest','htmx:historyCacheMiss'])document.addEventListener(name,e=>{const xhr=e.detail.xhr;xhr.addEventListener('load',()=>window.raceLog.push({event:'loaded',url:xhr.responseURL,status:xhr.status}));});`;
|
||||
let requests=[],barriers=new Map();
|
||||
function barrier(key){let resolve;const reached=new Promise(r=>resolve=r);const b={reached,resolve,release:null};barriers.set(key,b);return b;}
|
||||
const server=createServer((req,res)=>{
|
||||
const u=new URL(req.url,'http://local');res.setHeader('Cache-Control','no-store');
|
||||
if(assets.has(u.pathname)){res.setHeader('Content-Type','application/javascript');res.end(assets.get(u.pathname));return;}
|
||||
if(u.pathname==='/trace.js'){res.setHeader('Content-Type','application/javascript');res.end(trace);return;}
|
||||
if(u.pathname!=='/articles'){res.writeHead(404);res.end('Not found');return;}
|
||||
const topic=['alpha','beta','gamma'].includes(u.searchParams.get('topic'))?u.searchParams.get('topic'):'alpha';
|
||||
const mode=u.searchParams.get('mode')==='guarded'?'guarded':'managed';
|
||||
const hx=req.headers['hx-request']==='true',history=req.headers['hx-history-restore-request']==='true';
|
||||
const status=hx&&u.searchParams.has('fail')?503:200;
|
||||
const content=`<div id="payload"><h1 id="heading" tabindex="-1">${topic}</h1><p>Article ${topic}</p></div>`;
|
||||
const boost='hx-boost="true" hx-target="#content" hx-select="#payload" hx-swap="innerHTML"';
|
||||
const href=(t,extra='')=>`/articles?topic=${t}&mode=${mode}${extra}`;
|
||||
const nav=`<nav><a id="beta" href="${href('beta')}" ${boost}>Visit beta</a><a id="old" href="${href('beta','&hold=old')}" ${boost}>Slow beta</a><a id="oldfail" href="${href('beta','&hold=old&fail=1')}" ${boost}>Slow failed beta</a><a id="new" href="${href('gamma')}" ${boost}>Visit gamma</a><a id="newfail" href="${href('gamma','&fail=1')}" ${boost}>Failed gamma</a><a id="recover" href="${href('gamma')}">Native recovery</a></nav>`;
|
||||
const full=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${topic} — Catalog</title><meta name="htmx-config" content='{"historyRestoreAsHxRequest":false}'><script src="/trace.js" defer></script><script src="/${mode==='guarded'?'guarded':'baseline'}.js" defer></script><script src="/htmx.js" defer></script></head><body>${nav}<main id="content" hx-history-elt>${content}</main><label>Persistent search <input id="search"></label><p id="feedback" role="status" aria-live="polite"></p></body></html>`;
|
||||
const record={path:req.url,hx,history,status,held:false,released:false};requests.push(record);
|
||||
const send=()=>{record.released=true;res.writeHead(status,{'Content-Type':'text/html'});res.end(status===503?'Injected failure':hx&&!history?content:full);};
|
||||
const key=history?'history':u.searchParams.get('hold');const b=barriers.get(key);
|
||||
if(b){record.held=true;b.release=send;b.resolve();}else send();
|
||||
});
|
||||
server.listen(0,'127.0.0.1');await once(server,'listening');const base=`http://127.0.0.1:${server.address().port}`;
|
||||
const results=[],browsers={},output=process.env.FEP_RESULT_PATH||'/tmp/fep-ownership-result.json';
|
||||
const inspect=p=>p.evaluate(()=>({topic:document.querySelector('#heading')?.textContent,url:location.pathname+location.search,title:document.title,focus:document.activeElement.id||document.activeElement.tagName,search:document.querySelector('#search').value,feedback:document.querySelector('#feedback').textContent}));
|
||||
async function tabTo(p,id){for(let n=0;n<15;n++){if((await inspect(p)).focus===id)return;const backwards=await p.evaluate(id=>{const a=document.activeElement,t=document.getElementById(id);return a!==document.body&&Boolean(a.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_PRECEDING);},id);await p.keyboard.press(backwards?'Shift+Tab':'Tab');}throw Error(`Cannot Tab to ${id}`);}
|
||||
async function activate(p,id){await tabTo(p,id);await p.keyboard.press('Enter');}
|
||||
async function selected(p,t){await p.waitForFunction(t=>document.querySelector('#heading')?.textContent===t,t);await p.waitForFunction(t=>document.querySelector('#feedback').textContent===`Loaded ${t}`,t);}
|
||||
async function released(p,b,history=false){b.release();b.release=null;await p.waitForFunction(history=>window.raceLog.some(e=>e.event==='loaded'&&(history?e.url.includes('topic=alpha'):e.url.includes('hold=old'))),history);}
|
||||
async function held(b){await Promise.race([b.reached,new Promise((_,reject)=>setTimeout(()=>reject(Error('Server barrier not reached')),4000))]);}
|
||||
function same(a,b,keys){return keys.every(k=>a[k]===b[k]);}
|
||||
try{
|
||||
for(const [engine,launcher]of [['chromium',chromium],['firefox',firefox]]){
|
||||
const path=engine==='chromium'?process.env.FEP_CHROMIUM_PATH:undefined;const browser=await launcher.launch({headless:true,...(path?{executablePath:path}:{})});
|
||||
browsers[engine]={version:browser.version(),executable:path||launcher.executablePath(),sha256:createHash('sha256').update(await readFile(path||launcher.executablePath())).digest('hex')};
|
||||
try{for(const mode of ['managed','guarded']){
|
||||
async function scenario(id,fn){
|
||||
requests=[];barriers=new Map();const context=await browser.newContext();const p=await context.newPage();p.setDefaultTimeout(4500);
|
||||
const r={id,engine,mode,status:'pass'};
|
||||
try{await p.goto(base+`/articles?topic=alpha&mode=${mode}`);await p.waitForFunction(()=>window.htmx?.version==='2.0.11');Object.assign(r,await fn(p));r.obligationsMet=Object.values(r.obligations).every(Boolean);if(mode==='guarded')assert(r.obligationsMet,JSON.stringify(r.obligations));}
|
||||
catch(e){r.status='fail';r.error=e.message;}
|
||||
r.final=await inspect(p).catch(()=>null);r.events=await p.evaluate(()=>window.probe).catch(()=>[]);r.completions=await p.evaluate(()=>window.raceLog).catch(()=>[]);r.requests=[...requests];results.push(r);
|
||||
for(const b of barriers.values())if(b.release){b.release();b.release=null;}await context.close();await writeFile(output,JSON.stringify({status:'incomplete',results},null,2)+'\n');
|
||||
}
|
||||
async function backMiss(p){await activate(p,'beta');await selected(p,'beta');await p.evaluate(()=>sessionStorage.removeItem('htmx-history-cache'));const b=barrier('history');await p.goBack();await held(b);return b;}
|
||||
await scenario('O01-history-focus-moved',async p=>{const b=await backMiss(p);await tabTo(p,'search');await p.keyboard.type('new task');const before=await inspect(p);await released(p,b,true);const after=await inspect(p);return {before,after,obligations:{selection:after.topic==='alpha'&&new URL(base+after.url).searchParams.get('topic')==='alpha',title:after.title==='alpha — Catalog',focus:after.focus==='search',typedText:after.search==='new task'}};});
|
||||
await scenario('O02-history-superseded',async p=>{const b=await backMiss(p);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b,true);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noFeedbackOrFocusTheft:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O03-old-success',async p=>{const b=barrier('old');await activate(p,'old');await held(b);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noFeedbackOrFocusTheft:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O04-old-error',async p=>{const b=barrier('old');await activate(p,'oldfail');await held(b);await activate(p,'new');await selected(p,'gamma');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{newerSelection:same(before,after,['topic','url','title']),noObsoleteFeedback:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O05-newer-fails',async p=>{const b=barrier('old');await activate(p,'old');await held(b);await activate(p,'newfail');await p.waitForFunction(()=>document.querySelector('#feedback').textContent.includes('Navigation failed'));const before=await inspect(p);assert.equal(before.topic,'alpha');await released(p,b);const after=await inspect(p);return {before,after,obligations:{committedView:same(before,after,['topic','url','title']),newestFailureRetained:same(before,after,['feedback','focus'])}};});
|
||||
await scenario('O06-history-newer-than-request',async p=>{await activate(p,'beta');await selected(p,'beta');const b=barrier('old');await activate(p,'old');await held(b);await p.goBack();await selected(p,'alpha');const before=await inspect(p);await released(p,b);const after=await inspect(p);return {before,after,obligations:{historySelection:same(before,after,['topic','url','title']),historyContinuation:same(before,after,['feedback','focus'])}};});
|
||||
}}finally{await browser.close();}
|
||||
}
|
||||
}finally{for(const b of barriers.values())if(b.release)b.release();await new Promise(resolve=>server.close(resolve));}
|
||||
const files={};for(const f of ['ownership.mjs','ownership-policy.js','focus-policy.js','package.json','package-lock.json','node_modules/htmx.org/dist/htmx.js'])files[f]=createHash('sha256').update(await readFile(new URL(f,dir))).digest('hex');
|
||||
const r={date:new Date().toISOString(),node:process.version,kind:'ordered-navigation-completion-races-in-authored-htmx-fixture',files,browsers,results,completed:results.filter(x=>x.status==='pass').length,total:results.length,obligationsMet:results.filter(x=>x.obligationsMet).length};await writeFile(output,JSON.stringify(r,null,2)+'\n');console.log(JSON.stringify({output,completed:r.completed,total:r.total,obligationsMet:r.obligationsMet,failures:results.filter(x=>x.status==='fail')},null,2));if(r.completed!==r.total)process.exitCode=1;
|
||||
Loading…
Add table
Add a link
Reference in a new issue