48 lines
5.0 KiB
JavaScript
48 lines
5.0 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFile} from 'node:fs/promises';
|
|
import {createRequire} from 'node:module';
|
|
import {runInNewContext} from 'node:vm';
|
|
import {createRetainedProjection} from '../../src/components/canvas/retained-projection.ts';
|
|
const require=createRequire(import.meta.url),transform=require('../../scripts/loaders/xyflow-retained.cjs');
|
|
|
|
test('retention keeps the complete native graph, preserves business data and releases flags after the owner ends',()=>{
|
|
const project=createRetainedProjection(),a=Object.freeze({id:'a',dragging:false,data:Object.freeze({text:'draft'}),position:{x:2000,y:0}}),b={id:'b'},edge={id:'edge',source:'a',target:'b'},nodes=[a,b],edges=[edge];
|
|
const retained=project(nodes,edges,new Set(['a','edge']));
|
|
assert.equal(retained.nodes.length,2);assert.equal(retained.edges.length,1);assert.equal(retained.nodes[0].__endoleapRetain,true);assert.equal(retained.edges[0].__endoleapRetain,true);
|
|
assert.equal(retained.nodes[1],b);assert.equal(retained.nodes[0].data,a.data);assert.equal(retained.nodes[0].position,a.position);assert.equal(retained.nodes[0].dragging,false);assert.equal(a.__endoleapRetain,undefined);
|
|
assert.equal(project(nodes,edges,new Set(['a','edge'])),retained);
|
|
const released=project(nodes,edges,new Set());assert.equal(released.nodes,nodes);assert.equal(released.edges,edges);
|
|
const restored={...a,data:{text:'new incarnation'}},next=project([restored,b],edges,new Set(['a']));assert.notEqual(next.nodes[0],retained.nodes[0]);assert.equal(next.nodes[0].data,restored.data);
|
|
});
|
|
|
|
test('pinned xyflow compiler extension retains only explicit offscreen owners and keeps native visibility',async()=>{
|
|
for(const file of ['index.js','index.mjs']){
|
|
const source=await readFile(new URL('../../node_modules/@xyflow/react/dist/esm/'+file,import.meta.url),'utf8'),code=transform(source);
|
|
const start=code.indexOf('const selector$c = '),end=code.indexOf('\n};',start)+3;
|
|
const select=runInNewContext(code.slice(start,end)+'; selector$c', {getNodesInside:()=>[{id:'visible'}]});
|
|
const nodes=[{id:'visible'},{id:'pinned',__endoleapRetain:true},{id:'other'},{id:'hidden',__endoleapRetain:true,hidden:true}],state={nodes,nodeLookup:new Map(nodes.map(node=>[node.id,node])),width:100,height:100,transform:[0,0,1]};
|
|
assert.deepEqual([...select(true)(state)],['visible','pinned']);assert.deepEqual([...select(false)(state)],nodes.map(node=>node.id));assert.equal(state.nodeLookup.size,4);
|
|
const edgeStart=code.indexOf('function useVisibleEdgeIds('),edgeEnd=code.indexOf('\n}',edgeStart)+2;
|
|
const edges=[{id:'near',source:'visible',target:'other'},{id:'editing',source:'pinned',target:'other',__endoleapRetain:true},{id:'offscreen',source:'other',target:'pinned'}];
|
|
const result=runInNewContext(code.slice(edgeStart,edgeEnd)+'; useVisibleEdgeIds(true)',{useCallback:fn=>fn,useStore:fn=>fn({...state,edges}),shallow:()=>{},isEdgeVisible:({sourceNode})=>sourceNode.id==='visible'});
|
|
assert.deepEqual([...result],['near','editing']);
|
|
assert.throws(()=>transform(source.replace('const selector$c =','const changedSelector =').replace('getNodesInside(s.nodeLookup,','changedVisibility(s.nodeLookup,')),/Unsupported xyflow/);
|
|
}
|
|
});
|
|
|
|
|
|
test('native selection scanning stays dormant without listeners and still serves hooks and the callback prop',async()=>{
|
|
for(const file of ['index.js','index.mjs']){
|
|
const source=await readFile(new URL('../../node_modules/@xyflow/react/dist/esm/'+file,import.meta.url),'utf8'),code=transform(source);
|
|
const start=code.indexOf('const selector$l = '),end=code.indexOf('\n}',code.indexOf('function SelectionListener({',start))+2;
|
|
let scans=0,effects=0;const callbacks=[],userNode={id:'selected',position:{x:0,y:0},data:{title:'original payload'},selected:true},node={...userNode,internals:{userNode}},edge={id:'edge',source:'selected',target:'other',selected:true};
|
|
const lookup=value=>({*[Symbol.iterator](){scans++;yield[value.id,value];}}),state={nodeLookup:lookup(node),edgeLookup:lookup(edge),onSelectionChangeHandlers:[]};
|
|
const render=runInNewContext(code.slice(start,end)+'; SelectionListener',{useStore:selector=>selector(state),useStoreApi:()=>({getState:()=>state}),useEffect:callback=>{effects++;callback();},jsx:(Component,props)=>Component(props),shallow:(a,b)=>JSON.stringify(a)===JSON.stringify(b)});
|
|
assert.equal(render({}),null);assert.equal(scans,0);assert.equal(effects,0);
|
|
state.onSelectionChangeHandlers=[value=>callbacks.push(value)];render({});assert.equal(scans,2);assert.deepEqual([...callbacks[0].nodes].map(node=>node.id),['selected']);assert.deepEqual([...callbacks[0].edges].map(edge=>edge.id),['edge']);assert.equal(callbacks[0].nodes[0],userNode);assert.equal(callbacks[0].edges[0],edge);
|
|
state.onSelectionChangeHandlers=[];render({});assert.equal(scans,2,'unsubscribed hook kept scanning');
|
|
let prop;render({onSelectionChange:value=>{prop=value;}});assert.equal(scans,4);assert.deepEqual([...prop.nodes].map(node=>node.id),['selected']);assert.equal(prop.nodes[0],userNode);
|
|
}
|
|
});
|