Files
safetynet/tests/tab-registry.test.mjs
T

31 lines
4.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import registry from '../apps/browser/tab-registry.cjs';
function fixture(options={}) {
let id=0;const attached=[],detached=[],disposed=[];
const tabs=registry.createTabRegistry({createView:()=>{const wc={id:++id,url:'https://example.test/',destroyed:false,stops:0,history:[],getURL(){return this.url;},isDestroyed(){return this.destroyed;},stop(){this.stops++;},close(){this.destroyed=true;}};return {webContents:wc,visible:false,setVisible(v){this.visible=v;}};},attach:v=>attached.push(v),detach:v=>detached.push(v),dispose:t=>disposed.push(t),...options});
return {tabs,attached,detached,disposed};
}
test('native views retain separate DOM/history ownership when switching',()=>{const {tabs}=fixture();const a=tabs.create(),b=tabs.create();a.view.webContents.history.push('private history A');b.view.webContents.history.push('history B');tabs.activate(b.id);tabs.activate(a.id);assert.notEqual(a.view.webContents,b.view.webContents);assert.deepEqual(a.view.webContents.history,['private history A']);assert.deepEqual(b.view.webContents.history,['history B']);assert.equal(tabs.fromContents(b.view.webContents.id),b);});
test('foreground approval fails after switch-away-and-back even on identical URL',()=>{const {tabs}=fixture();const a=tabs.create(),b=tabs.create();tabs.resume(1);const token=tabs.context(a.id);assert.equal(tabs.current(token),true);tabs.activate(b.id);tabs.activate(a.id);assert.equal(tabs.current(token),false);});
test('background network contexts survive switches but not navigation or policy change',()=>{const {tabs}=fixture();const a=tabs.create(),b=tabs.create();tabs.resume(1);const token=tabs.context(a.id,{foreground:false});tabs.activate(b.id);assert.equal(tabs.current(token),true);tabs.navigation(a.id);assert.equal(tabs.current(token),false);const renewed=tabs.context(a.id,{foreground:false});tabs.resume(2);assert.equal(tabs.current(renewed),false);assert.throws(()=>tabs.resume(1));});
test('quarantine stops hidden and visible tabs, invalidates approvals and denies all old contexts',()=>{const {tabs}=fixture();const a=tabs.create(),b=tabs.create();tabs.resume(1);const tokens=[tabs.context(a.id,{foreground:false}),tabs.context(b.id,{foreground:false})];a.security.visualApproval={approved:true};b.security.visualApproval={approved:true};tabs.quarantine();for(const tab of [a,b]){assert.equal(tab.view.webContents.stops,1);assert.equal(tab.view.visible,false);assert.equal(tab.security.visualApproval,null);}tabs.resume(1);assert.equal(tokens.every(t=>!tabs.current(t)),true);});
test('close destroys native contents and cannot revive closed tab capabilities',()=>{const {tabs,disposed}=fixture();const a=tabs.create(),b=tabs.create();tabs.resume(1);const token=tabs.context();tabs.remove(a.id);assert.equal(a.view.webContents.destroyed,true);assert.equal(tabs.current(token),false);assert.equal(tabs.active(),b);assert.equal(tabs.fromContents(a.view.webContents.id),null);tabs.close();assert.equal(b.view.webContents.destroyed,true);assert.equal(disposed.length,2);assert.throws(()=>tabs.create());});
test('URL mutation and renderer-chosen identifiers cannot reuse native approval',()=>{const {tabs}=fixture();const a=tabs.create();tabs.resume(1);const token=tabs.context();a.view.webContents.url='https://other.test/';assert.equal(tabs.current(token),false);assert.throws(()=>tabs.activate('renderer-forged-id'));assert.equal(Object.isFrozen(token),true);assert.notEqual(a.id,'managed');});
test('capacity is bounded before another renderer process is created',()=>{const {tabs}=fixture({maxTabs:1});tabs.create();assert.throws(()=>tabs.create(),/capacity/);assert.equal(tabs.all().length,1);});
// Electron can remove view.webContents before pending startup rejects. The
// native reference must survive solely for deterministic cleanup/revocation.
test('destroyed native view remains removable after its live getter disappears',()=>{
const {tabs,disposed}=fixture();const a=tabs.create(),b=tabs.create();tabs.resume(1);const token=tabs.context(a.id),contents=a.contents;
assert.throws(()=>{a.contents=b.contents;},TypeError);
assert.equal(Object.keys(a).includes('contents'),false);
contents.destroyed=true;delete a.view.webContents;
assert.equal(tabs.current(token),false);assert.throws(()=>tabs.context(a.id),/Unknown native tab/);
assert.doesNotThrow(()=>tabs.remove(a.id));assert.equal(tabs.fromContents(contents.id),null);assert.equal(disposed.includes(a),true);assert.equal(tabs.active(),b);
});
test('shutdown cleans already destroyed views without reactivating surviving views',()=>{
const {tabs,disposed,attached}=fixture();const a=tabs.create(),b=tabs.create();a.contents.destroyed=true;delete a.view.webContents;
const attachments=attached.length;assert.doesNotThrow(()=>tabs.close());assert.equal(disposed.length,2);assert.equal(tabs.all().length,0);assert.equal(attached.length,attachments);assert.equal(b.contents.destroyed,true);assert.doesNotThrow(()=>tabs.close());
});