const fs = require('fs'); const path = require('path'); const vm = require('vm'); const assert = require('assert'); const root = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname,'..'); const script = fs.readFileSync(path.join(root,'static/reader/pdf/reader-engine-preload.js'),'utf8'); const resources = require(path.join(root,'static/reader-resources.js')); const tick=()=>new Promise(resolve=>setImmediate(resolve)); function fixture(prefix,query,{saveData=false,held=false,httpFailure=false,readFailure=false}={}){ const links=[],fetches=[],events=new Map(),timers=new Map(); let reads=0; vm.runInNewContext(script,{URL,URLSearchParams,AbortController,Promise, navigator:{connection:{saveData}},location:{search:query},VoiceOfMLReaderResources:resources, VoiceOfMLReaderSecurity:{async readBytes(_response,limit){assert.equal(limit,1024*1024);reads++; if(readFailure)throw Error('optional bytes failed');return new Uint8Array();}}, setTimeout(fn,ms){assert.equal(ms,8000);timers.set(1,fn);return 1;},clearTimeout:id=>timers.delete(id), addEventListener:(name,fn)=>events.set(name,fn),removeEventListener:name=>events.delete(name), fetch(url,options){ fetches.push({url,options}); assert.ok(/\/static\/(?:reader\/pdf\/pdf-worker-wrapper\.[a-f0-9.]*mjs|reader\/pdf\/pdf-worker-wrapper\.mjs|vendor\/wasm\/qcms_bg\.wasm)$/.test(url),'byte warming is restricted to component paths'); assert.equal(options.mode,'same-origin');assert.equal(options.credentials,'same-origin'); if(held)return new Promise((_,reject)=>options.signal.addEventListener('abort',()=>reject(options.signal.reason),{once:true})); return Promise.resolve({ok:!httpFailure,body:{cancel:async()=>{}}}); }, document:{currentScript:{src:'https://site.test'+prefix+'reader/pdf/reader-engine-preload.abcdef123456.js'}, createElement:()=>({}),head:{appendChild:node=>links.push(node)}}}); return {links,fetches,events,timers,get reads(){return reads;}}; } (async()=>{ for(const prefix of ['/static/','/search/static/']){ for(const [query,pdf] of [['?ext=pdf',true],['?ext=PDF',true],['?ext=txt',false],['?id=abc',false], ['?url='+encodeURIComponent('https://example.test/Book.PDF'),true],['?url=invalid&ext=txt',false]]){ const f=fixture(prefix,query),base='https://site.test'+prefix; assert.deepStrictEqual(f.links.map(link=>link.href),pdf?['pdf','pdfWorker'].map(name=>resources.vendorUrl(name,base)):[]); assert.ok(f.links.every(link=>link.rel==='modulepreload')); assert.deepStrictEqual(f.fetches.map(row=>row.url),pdf?[base+'reader/pdf/pdf-worker-wrapper.mjs',...resources.pdfSupportAssets(base)]:[]); await tick();assert.equal(f.reads,pdf?2:0);assert.equal(f.timers.size,0);assert.equal(f.events.size,0); } for(const extension of ['md','markdown','html','htm','epub']){ const f=fixture(prefix,'?ext='+extension),base='https://site.test'+prefix,suffix=prefix==='/search/static/'?'?reader-v1':''; assert.deepStrictEqual(f.links.map(link=>link.href),resources.engineAssets(extension,base,suffix)); assert.equal(f.fetches.length,0); if(extension==='epub'){ assert.ok(f.links.some(link=>link.href===base+'foliate-reader/vendor/zip.js')); assert.ok(f.links.some(link=>link.href===base+'foliate-reader/epub.js')); assert.ok(f.links.every(link=>link.rel==='modulepreload')); }else{ assert.ok(f.links.every(link=>link.rel==='preload'&&link.as==='script')); if(extension==='html'||extension==='htm')assert.equal(f.links.length,1); } } } // Every static view.js import must be warmed with view.js, or it is only // discovered after view.js arrives (an extra round trip on first open). const view=path.join(root,'static/foliate-reader/view.js'); if(fs.existsSync(view)){ const imports=[...fs.readFileSync(view,'utf8').matchAll(/^import\s[^;]*?from\s+['"]\.\/([^'"]+)['"]/gm)].map(m=>m[1]); assert.ok(imports.length>=4,'expected view.js static imports'); for(const extension of ['epub','mobi','azw','azw3','fb2','fbz']){ const hrefs=resources.engineAssets(extension,'/static/'); for(const file of imports)assert.ok(hrefs.includes('/static/foliate-reader/'+file),extension+' must warm '+file); } }else console.log('foliate-reader not prepared; static import coverage skipped (run copy_reader_vendor.mjs)'); const lean=fixture('/static/','?ext=pdf',{saveData:true});await tick();assert.equal(lean.fetches.length,1); assert.ok(lean.fetches[0].url.endsWith('pdf-worker-wrapper.mjs')); for(const event of ['pagehide','voice-reader-dispose','timeout']){ const f=fixture('/static/','?ext=pdf',{held:true}); if(event==='timeout')f.timers.get(1)();else f.events.get(event)(); await tick();assert.ok(f.fetches.every(row=>row.options.signal.aborted));assert.equal(f.events.size,0);assert.equal(f.timers.size,0); } for(const options of [{httpFailure:true},{readFailure:true}]){ const f=fixture('/static/','?ext=pdf',options);await tick(); assert.equal(f.timers.size,0);assert.equal(f.events.size,0,'optional failure must release warming ownership'); } assert.ok(resources.readerFiles.indexOf('reader/pdf/pdf-worker-wrapper.mjs'){console.error(error);process.exitCode=1;});