Files
Endoleap/tests/headless/element-sources.test.mjs

45 lines
6.4 KiB
JavaScript

import {test} from 'node:test';import assert from 'node:assert/strict';
import {createElementSources} from '../../src/lib/canvas-bindings/element-sources.ts';
import {createSelectorStore} from '../../src/lib/canvas-bindings/selectors.ts';
function fixture(count=3){
const element=id=>({id,incarnation:1,ownerCanvasId:'root',data:{title:id}}),document={elements:new Map(Array.from({length:count},(_,index)=>{const id=String(index);return[id,element(id)];}))};let root={document,runtime:{acknowledged:document}},throwSubscribe=false;const listeners=new Set(),lookups=[];
const ports={getSnapshot:()=>root,subscribe(fn){if(throwSubscribe)throw Error('subscribe failed');listeners.add(fn);return()=>listeners.delete(fn);},lookup(snapshot,id){lookups.push(id);const element=snapshot.document.elements.get(id);return element?{status:'present',element}:{status:snapshot.runtime.loading?'unloaded':'absent'};}};
return{ports,lookups,element,get size(){return listeners.size;},root:()=>root,throwSubscribe(value){throwSubscribe=value;},emit(next,changes=[]){root=next;for(const fn of [...listeners])fn({changes},root);},edit(id,title){const elements=new Map(root.document.elements);elements.set(id,{...elements.get(id),data:{title}});this.emit({...root,document:{elements}},[{kind:'field',target:{id,incarnation:1},field:'data'}]);}};
}
test('one ID edit among 1000 subscriptions evaluates only that target; runtime-only cuts do no element work',()=>{
const f=fixture(1000),sources=createElementSources(f.ports),calls=new Map(),notified=new Set(),stops=[];
for(let index=0;index<1000;index++){const id=String(index),store=createSelectorStore(sources.forElement(id),view=>{calls.set(id,(calls.get(id)??0)+1);return view.element.data.title;});stops.push(store.subscribe(()=>notified.add(id)));}
assert.equal(f.size,1);const before=new Map(calls);f.lookups.length=0;f.edit('300','changed');assert.deepEqual([...new Set(f.lookups)],['300']);assert.deepEqual([...notified],['300']);for(const[id,count]of calls)assert.equal(count,before.get(id)+(id==='300'?1:0));
f.lookups.length=0;f.emit({...f.root(),runtime:{...f.root().runtime,metadata:1}});assert.equal(f.lookups.length,0);for(const stop of stops)stop();assert.equal(f.size,0);
});
test('ACK replacement checks active targets, ignores equal cloned records and delivers unlisted changes',()=>{
const f=fixture(),sources=createElementSources(f.ports),seen=[];const a=sources.forElement('0'),b=sources.forElement('1'),stopA=a.subscribe(()=>seen.push('0')),stopB=b.subscribe(()=>seen.push('1')),old=a.getSnapshot();
const elements=new Map([...f.root().document.elements].map(([id,row])=>[id,structuredClone(row)]));elements.get('1').data.title='canonical';f.emit({...f.root(),document:{elements}});assert.deepEqual(seen,['1']);assert.strictEqual(a.getSnapshot(),old);assert.equal(b.getSnapshot().element.data.title,'canonical');stopA();stopB();
});
test('loading coverage invalidates missing lookups at EOF without waking present targets',()=>{
const f=fixture(),sources=createElementSources(f.ports);f.emit({...f.root(),runtime:{...f.root().runtime,loading:{version:6}}});const missing=sources.forElement('later'),present=sources.forElement('0'),seen=[];const a=missing.subscribe(()=>seen.push('missing')),b=present.subscribe(()=>seen.push('present'));assert.equal(missing.getSnapshot().status,'unloaded');f.emit({...f.root(),runtime:{...f.root().runtime,loading:undefined}});assert.deepEqual(seen,['missing']);assert.equal(missing.getSnapshot().status,'absent');a();b();
});
test('getSnapshot stays fresh while unsubscribed, repeated cleanup is safe and late subscriptions do not reuse old baselines',()=>{
const f=fixture(),sources=createElementSources(f.ports),channel=sources.forElement('0');let calls=0;const stop=channel.subscribe(()=>calls++);stop();stop();f.edit('0','unsubscribed');assert.equal(calls,0);assert.equal(channel.getSnapshot().element.data.title,'unsubscribed');const again=channel.subscribe(()=>calls++);f.edit('0','current');assert.equal(calls,1);again();assert.equal(f.size,0);
});
test('observer failures do not starve other buckets and failed upstream subscription retains no listener',()=>{
const f=fixture(),sources=createElementSources(f.ports);f.throwSubscribe(true);assert.throws(()=>sources.forElement('0').subscribe(()=>{}),/subscribe failed/);f.throwSubscribe(false);let b=0;const a=sources.forElement('0').subscribe(()=>{throw Error('observer failed');}),stopB=sources.forElement('1').subscribe(()=>b++),elements=new Map(f.root().document.elements);for(const id of ['0','1'])elements.set(id,{...elements.get(id),data:{title:'new'}});assert.throws(()=>f.emit({...f.root(),document:{elements}},['0','1'].map(id=>({kind:'element',id}))),/observer failed/);assert.equal(b,1);a();stopB();assert.equal(f.size,0);
});
test('last unsubscribe before the captured upstream callback runs does not retain or dereference a dead baseline',()=>{
const f=fixture();let stop;const earlier=f.ports.subscribe(()=>stop()),sources=createElementSources(f.ports);let notified=0;stop=sources.forElement('0').subscribe(()=>notified++);assert.doesNotThrow(()=>f.edit('0','after detach'));assert.equal(notified,0);assert.equal(f.size,1);earlier();assert.equal(f.size,0);
});
test('lookup failure notifies readers and clearing the failure re-notifies even when the prior value is restored',()=>{
const f=fixture(),lookup=f.ports.lookup;let fail=false;f.ports.lookup=(...args)=>{if(fail)throw Error('lookup failed');return lookup(...args);};const channel=createElementSources(f.ports).forElement('0');let calls=0;const stop=channel.subscribe(()=>calls++),before=channel.getSnapshot();fail=true;f.emit({...f.root(),document:{...f.root().document}});assert.equal(calls,1);assert.throws(channel.getSnapshot,/lookup failed/);fail=false;f.emit({...f.root(),document:{...f.root().document}});assert.equal(calls,2);assert.strictEqual(channel.getSnapshot(),before);stop();
});
test('an advanced ACK does not recheck missing IDs against an unchanged captured loading baseline',()=>{
const f=fixture(),base=f.root().document;f.emit({...f.root(),runtime:{...f.root().runtime,loading:{version:6},loadingRead:{document:base}}});const sources=createElementSources(f.ports),stop=sources.forElement('missing').subscribe(()=>{});f.lookups.length=0;f.emit({...f.root(),runtime:{...f.root().runtime,acknowledged:{...base}}});assert.deepEqual(f.lookups,[]);f.emit({...f.root(),runtime:{...f.root().runtime,loadingRead:{document:{...base}}}});assert.deepEqual(f.lookups,['missing']);stop();
});