Files
safetynet/tests/history-navigation.test.mjs
T

32 lines
6.0 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {EventEmitter} from 'node:events';
import {createRequire} from 'node:module';
const {performHistory}=createRequire(import.meta.url)('../apps/browser/history-navigation.cjs');
function fixture(entries=['https://example.test/one','https://example.test/two'],index=1){
const c=new EventEmitter();c.url=entries[index];c.stops=0;c.calls=0;c.index=index;c.isDestroyed=()=>false;c.getURL=()=>c.url;c.stop=()=>c.stops++;c.navigationHistory={getActiveIndex:()=>c.index,getEntryAtIndex:i=>entries[i]?{url:entries[i]}:null,goToIndex:i=>{c.calls++;c.index=i;c.url=entries[i];}};c.reload=()=>c.calls++;return c;
}
const run=(contents,extra={})=>performHistory({contents,action:'back',allowed:()=>true,current:()=>true,timeoutMs:100,...extra});
const dispatched=async c=>{for(let i=0;i<20&&!c.calls;i++)await new Promise(r=>setImmediate(r));assert.equal(c.calls,1);};
test('native back/forward target and main-frame in-page completion',async()=>{const c=fixture();let p=run(c);await dispatched(c);c.emit('did-navigate-in-page',{},c.url,true);assert.deepEqual(await p,{completed:true,url:'https://example.test/one'});c.calls=0;p=run(c,{action:'forward'});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).url,'https://example.test/two');assert.equal(c.eventNames().length,0);});
test('missing history is a no-op',async()=>{const c=fixture(['https://example.test/'],0);assert.deepEqual(await run(c),{completed:false,url:c.url});assert.equal(c.calls,0);});
test('blocked protocols, credentials and policy reject before side effects',async()=>{for(const url of ['file:///secret','https://user:[email protected]/','https://example.test/']){const c=fixture([url,'https://example.test/two']);await assert.rejects(run(c,{allowed:()=>false}),/denied/);assert.equal(c.calls,0);}});
test('history races while awaiting policy cannot change inspected destination',async()=>{const c=fixture();await assert.rejects(run(c,{allowed:async()=>{c.index=0;return true;}}),/destination changed/);assert.equal(c.calls,0);});
test('stale context and policy revocation after dispatch stop navigation',async()=>{for(const mode of ['context','policy']){const c=fixture();let valid=true;const p=run(c,{current:()=>mode==='context'?valid:true,allowed:()=>mode==='policy'?valid:true});await dispatched(c);valid=false;c.emit('did-finish-load');await assert.rejects(p,/context changed|destination denied/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);}});
test('abort before and after dispatch has bounded cleanup',async()=>{const c=fixture();const controller=new AbortController();controller.abort();await assert.rejects(run(c,{signal:controller.signal}),/cancelled/);assert.equal(c.calls,0);const next=new AbortController();const p=run(c,{signal:next.signal});await dispatched(c);next.abort();await assert.rejects(p,/cancelled/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);});
test('same-URL reload awaits actual event, subframe events ignored',async()=>{const c=fixture();let done=false;const p=run(c,{action:'reload'}).then(v=>{done=true;return v;});await dispatched(c);c.emit('did-navigate-in-page',{},c.url,false);c.emit('did-fail-load',{},-1,'private error',c.url,false);await new Promise(r=>setImmediate(r));assert.equal(done,false);c.emit('did-finish-load');assert.equal((await p).completed,true);});
test('unexpected final URL and mainframe load failure reject; timeout stops',async()=>{for(const mode of ['redirect','failure','timeout']){const c=fixture();const p=run(c,{timeoutMs:30});await dispatched(c);if(mode==='redirect'){c.url='https://other.test/';c.emit('did-finish-load');}if(mode==='failure')c.emit('did-fail-load',{},-1,'secret',c.url,true);await assert.rejects(p,/destination changed|navigation failed|timed out/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);}});
test('missing positive native context proof denies history before any side effect',async()=>{const c=fixture();await assert.rejects(run(c,{current:async()=>undefined}),/context changed/);assert.equal(c.calls,0);});
test('context guards distinguish captured state before dispatch from intentional navigation after',async()=>{const c=fixture(),phases=[];const p=run(c,{current:phase=>{phases.push(phase);return phase==='before'?c.calls===0:c.calls===1;}});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).completed,true);assert.ok(phases.includes('before')&&phases.includes('after'));});
test('indexed history compares exact snapshot immediately before native dispatch and never retries',async()=>{
const c=fixture(),snapshot=()=>({activeIndex:c.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title:'Two'}]}),binding=JSON.stringify(snapshot());
const p=run(c,{action:'index',index:0,getSnapshot:snapshot,historyBinding:binding});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).url,'https://example.test/one');assert.equal(c.calls,1);
const changed=fixture();let title='Two';const getSnapshot=()=>({activeIndex:changed.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title}]});
await assert.rejects(run(changed,{action:'index',index:0,getSnapshot,historyBinding:JSON.stringify(getSnapshot()),allowed:()=>{title='Replaced';return true;}}),/snapshot changed/);assert.equal(changed.calls,0);assert.equal(changed.eventNames().length,0);
});
test('indexed current-entry selection is a guarded no-op and raw indexes lack a snapshot authority',async()=>{
const c=fixture(),snapshot=()=>({activeIndex:c.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title:'Two'}]});assert.equal((await run(c,{action:'index',index:1,getSnapshot:snapshot,historyBinding:JSON.stringify(snapshot())})).completed,true);assert.equal(c.calls,0);
for(const extra of [{index:-1},{index:0},{index:0,getSnapshot:snapshot},{index:0,getSnapshot:snapshot,historyBinding:'{}'}])await assert.rejects(run(c,{action:'index',...extra}));assert.equal(c.calls,0);
});