const fs = require('fs'); const vm = require('vm'); const assert = require('assert'); const path = require('path'); const root = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '..'); const source = require('./reader_source_contract').readReaderSource(root); const helper = source.match(/function createHeadingLocationCache\(root, entries\) \{[\s\S]*?\n\}/)[0]; const observers = [], listeners = new Map(); class Observer { constructor(callback) { this.callback = callback; this.records = []; this.closed = false; observers.push(this); } observe() {} disconnect() { this.closed = true; } takeRecords() { return this.records.splice(0); } } const add=(name,fn)=>{if(!listeners.has(name))listeners.set(name,new Set());listeners.get(name).add(fn);}; const remove=(name,fn)=>{listeners.get(name)?.delete(fn);if(!listeners.get(name)?.size)listeners.delete(name);}; const region = {clientWidth:400, scrollHeight:10000, addEventListener:add,removeEventListener:remove, ownerDocument:{defaultView:{getComputedStyle:node=>({position:node.position||'static'})}, fonts:{addEventListener:add,removeEventListener:remove}}}; const context = {MutationObserver:Observer, ResizeObserver:Observer, documentState:{zoom:100}}; vm.runInNewContext(helper, context); let reads = 0, scroll = 0; const entries = Array.from({length:1000}, (_,index)=>({target:{isConnected:true, getBoundingClientRect:()=>{reads++;return {top:index*10-scroll};}}})); const cache = context.createHeadingLocationCache(region, entries); assert.equal(cache.at(8,scroll),0); assert.equal(reads,1000); reads=0; for(let index=1;index<100;index++){scroll=index*10;assert.equal(cache.at(8,scroll),index);} assert.equal(reads,0); entries[99].target={isConnected:true,getBoundingClientRect:()=>{reads++;return {top:1005-scroll};}}; assert.equal(cache.at(8,scroll),98); assert.equal(reads,1000); reads=0;entries[98].target.isConnected=false; assert.equal(cache.at(8,scroll),97); assert.equal(reads,999); const unordered=[entries[500],entries[10],entries[200],entries[200]]; const alternative=context.createHeadingLocationCache(region,unordered); scroll=2000; assert.equal(alternative.at(0,scroll),3); scroll=0;assert.equal(alternative.at(0,scroll),1); for(const change of [()=>observers[0].records.push({}),()=>listeners.get('loadingdone').forEach(fn=>fn()), ()=>region.clientWidth++,()=>context.documentState.zoom++]) { reads=0;change();cache.at(8,scroll);assert.equal(reads,999); } cache.dispose();alternative.dispose(); const pinned=context.createHeadingLocationCache(region,[ {target:{isConnected:true,position:'fixed',getBoundingClientRect:()=>({top:0})}}, {target:{isConnected:true,getBoundingClientRect:()=>({top:500-scroll})}}]); scroll=0;assert.equal(pinned.at(8,scroll),0); scroll=510;assert.equal(pinned.at(8,scroll),0); pinned.dispose(); assert.ok(observers.every(observer=>observer.closed)); assert.equal(listeners.size,0); console.log('heading geometry reuse, unordered ties, mutation/layout invalidation and cleanup passed');