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); } });