const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'); function fixture(){ const space={style:{}},surface={style:{}},label={},frames=new Map();let next=0,observer; const viewport={clientWidth:1550,clientHeight:601,rect:{width:1550,height:600.5234375},scrollLeft:0,scrollTop:0, addEventListener(){},getBoundingClientRect(){return this.rect;}, querySelector(s){return s==='.family-map-space'?space:surface;},replaceChildren(){}, scrollTo(x,y){this.scrollLeft=x;this.scrollTop=y;}}; const scope=vm.createContext({$:()=>label,ResizeObserver:class{constructor(fn){observer=fn;}observe(){}}, requestAnimationFrame(fn){frames.set(++next,fn);return next;},cancelAnimationFrame(id){frames.delete(id);}}); vm.runInContext(fs.readFileSync('web/family-map.js','utf8')+';this.MapClass=FamilyMap',scope); const map=new scope.MapClass(viewport,()=>{});map.graph={width:400,height:264,nodes:[]}; return {map,viewport,space,surface,frames,resize:()=>observer(),flush(){const jobs=[...frames.values()];frames.clear();jobs.forEach(fn=>fn());}}; } test('fractional fullscreen size never creates a too-tall scroll surface',()=>{ const f=fixture();for(let i=0;i<20;i++){f.map.fit();assert.equal(f.space.style.height,'600px');assert.ok(parseFloat(f.space.style.height)<=f.viewport.rect.height);} assert.equal(f.surface.style.transform,'translate(575px,168px) scale(1)'); }); test('resize notifications coalesce and unchanged dimensions do not refit',()=>{ const f=fixture();let calls=0;const fit=f.map.fit.bind(f.map);f.map.fit=()=>{calls++;fit();}; f.resize();f.resize();assert.equal(f.frames.size,1);f.flush();assert.equal(calls,1); f.resize();f.flush();assert.equal(calls,1); f.viewport.rect.height=700.7;f.viewport.clientHeight=701;f.resize();f.flush();assert.equal(calls,2); }); test('manual zoom and scroll survive a resize notification',()=>{ const f=fixture();f.map.autoFit=false;f.map.scale=1.5;f.viewport.scrollLeft=15;f.viewport.scrollTop=30; f.resize();f.flush();assert.equal(f.map.scale,1.5);assert.equal(f.map.autoFit,false); assert.equal(f.viewport.scrollLeft,15);assert.equal(f.viewport.scrollTop,30); }); test('hidden canvas does not resize and pending work is cancelled on reset',()=>{ const f=fixture();f.viewport.clientWidth=0;f.resize();f.flush();assert.equal(f.space.style.width,undefined); f.resize();f.map.reset();assert.equal(f.frames.size,0);assert.equal(f.map.graph,null); }); test('scaled map clips unscaled overflow and fullscreen fallback clears the sidebar',()=>{ const css=fs.readFileSync('web/family-map.css','utf8'); assert.match(css,/\.family-map-space\{min-width:0;min-height:0;overflow:clip\}/); assert.match(css,/scrollbar-gutter:stable/);assert.match(css,/\.family-workspace\.family-expanded\{z-index:1100\}/); });