Files
exoplasim/tests/web-gui-composite.js

26 lines
4.1 KiB
JavaScript

(async()=>{
'use strict';
const check=(condition,message)=>{if(!condition)throw Error(message);},wait=async condition=>{const limit=performance.now()+3000;while(!condition()){if(performance.now()>limit)throw Error('Composite rendering timed out.');await new Promise(resolve=>setTimeout(resolve,10));}};
const schema=(await(await fetch(window.EXOPLASIM.api.schema)).json()).data,grid=schema.map_grids.T85;
const root=document.createElement('div');root.style.cssText='position:fixed;left:-10000px;top:0;width:640px;height:480px';document.body.append(root);
const original=window.fetch,frames=new Map(),rows=grid.lat.length,cols=grid.lon.length,gravity=schema.fields.find(field=>field.name==='gravity').implicit_default;
const index={lat:grid.lat,lon:grid.lon,time:[0,1],time_unit:'timesteps',fields:[{name:'ts',label:'surface_temperature',unit:'K',shape:[2,1,rows,cols],min:200,max:300},{name:'pr',label:'total_precipitation',unit:'m s-1',shape:[2,1,rows,cols],min:0,max:1e-6},{name:'sg',label:'surface_geopotential',unit:'m2 s-2',shape:[1,1,rows,cols],min:-1000,max:1000}]};
for(const [i,field] of index.fields.entries()){const values=new Float32Array(rows*cols);for(let cell=0;cell<values.length;cell++)values[cell]=field.min+(field.max-field.min)*(cell%cols)/(cols-1);frames.set(field.name,values.buffer);}
let visual,fail=false;
try{
window.fetch=(url,options)=>{const name=String(url);if(!name.startsWith('exo-composite-probe:'))return original(url,options);const field=name.split(':')[1];if(field==='series')return Promise.resolve(new Response(JSON.stringify({ok:true,data:{mean:[250,251],unit:'K',time:index.time,time_unit:index.time_unit}})));return Promise.resolve(new Response(frames.get(field),{status:fail&&field==='pr'?500:200}));};
visual=new ExoVisualization(root);visual.setDataset(index,field=>'exo-composite-probe:'+field,()=> 'exo-composite-probe:series',{gravity});
await wait(()=>root.querySelector('.viz-empty').hidden);
const view=root.querySelector('[data-view]'),time=root.querySelector('[data-time]');view.value='rgb';view.dispatchEvent(new Event('change'));await wait(()=>root.querySelector('.viz-empty').hidden);
check(root.querySelectorAll('.viz-rgb-legend>div').length===3,'Three channel legends are required.');
const legend=root.querySelector('.viz-rgb-legend').textContent,timings=[];
for(let trial=0;trial<42;trial++){const start=performance.now();await new Promise((resolve,reject)=>{const status=root.querySelector('.viz-empty'),observer=new MutationObserver(()=>{if(status.hidden){observer.disconnect();clearTimeout(timeout);resolve();}}),timeout=setTimeout(()=>{observer.disconnect();reject(Error('Composite timing sample timed out.'));},3000);observer.observe(status,{attributes:true,attributeFilter:['hidden']});time.value=String(trial%2);time.dispatchEvent(new Event('input'));});if(trial>=2)timings.push(performance.now()-start);}
check(root.querySelector('.viz-rgb-legend').textContent===legend,'Playback must not change the channel scales.');
const map=root.querySelector('.viz-map');map.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));check(root.querySelector('[data-inspector]').textContent.includes('mm/day'),'Inspection must show converted precipitation.');
fail=true;time.dispatchEvent(new Event('input'));await wait(()=>root.querySelector('.viz-empty').textContent.includes('500'));check(root.querySelector('[data-inspector]').textContent.includes('500'),'A channel failure must replace stale inspection.');
fail=false;time.dispatchEvent(new Event('input'));await wait(()=>root.querySelector('.viz-empty').hidden);
view.value='field';view.dispatchEvent(new Event('change'));await wait(()=>root.querySelector('.viz-empty').hidden);check(root.querySelector('.viz-rgb-legend').hidden,'Single-field mode must recover.');
timings.sort((a,b)=>a-b);return{passed:true,cells:rows*cols,repetitions:timings.length,mean_ms:timings.reduce((a,b)=>a+b)/timings.length,p95_ms:timings[Math.ceil(timings.length*.95)-1],max_ms:timings.at(-1),timing_scope:'Synthetic fetch, decoding and synchronous rendering, timed to the completion mutation; not a compositor measurement.'};
}finally{window.fetch=original;visual?.destroy();root.remove();}
})()