56 lines
5.6 KiB
JavaScript
56 lines
5.6 KiB
JavaScript
(async () => {
|
|
const report={ok:false,checks:[],benchmark:null};
|
|
const check=(name,passed)=>{report.checks.push({name,ok:Boolean(passed)});if(!passed)throw Error(name);};
|
|
const wait=async predicate=>{const deadline=performance.now()+2000;while(!predicate()){if(performance.now()>deadline)throw Error('Editor did not settle within two seconds.');await new Promise(resolve=>setTimeout(resolve,10));}};
|
|
const focused=document.activeElement,scroll=[scrollX,scrollY],root=document.createElement('div');
|
|
root.style.cssText='position:fixed;left:-10000px;width:640px';document.body.append(root);
|
|
let editor,bench,releaseLoad,releaseSave;
|
|
try {
|
|
const abort=new AbortController(),timer=setTimeout(()=>abort.abort(),2000);
|
|
let schema;
|
|
try{const response=await fetch('?api=schema',{signal:abort.signal});check('Schema loads',response.ok);schema=(await response.json()).data;}finally{clearTimeout(timer);}
|
|
const fields=schema.map_fields,grid=schema.map_grids.T21;
|
|
const values=(field,g=grid)=>new Float32Array(g.lat.length*g.lon.length).fill(fields[field].default);
|
|
let mode='normal';
|
|
const callbacks={fields,onDirty:()=>{},onSaved:()=>{},sources:async()=>[{id:'fixture',name:'Local test fixture'}],
|
|
load:async field=>{if(mode==='fail')throw Error('Test load failure');if(mode==='hold')await new Promise(resolve=>{releaseLoad=resolve;});return{values:values(field),grid,name:'Test map'};},
|
|
save:async field=>{if(mode==='save-fail')throw Error('Test save failure');if(mode==='save-hold')await new Promise(resolve=>{releaseSave=resolve;});return{id:'test-only',field};}};
|
|
editor=new ExoMapEditor(root,callbacks);await editor.start();
|
|
const el=selector=>root.querySelector(selector),canvas=el('canvas');
|
|
const paint=()=>canvas.dispatchEvent(new KeyboardEvent('keydown',{key:' ',bubbles:true,cancelable:true}));
|
|
const change=async field=>{el('[data-map-field]').value=field;el('[data-map-field]').dispatchEvent(new Event('change'));await new Promise(resolve=>setTimeout(resolve,0));await wait(()=>!el('[data-map-field]').disabled);};
|
|
check('Initial map is clean',el('[data-map-save]').disabled);
|
|
paint();check('Keyboard paint is dirty',!el('[data-map-save]').disabled);
|
|
el('[data-map-undo]').click();check('Undo returns to original',el('[data-map-save]').disabled);
|
|
el('[data-map-redo]').click();check('Redo restores edit',!el('[data-map-save]').disabled);
|
|
el('[data-map-undo]').click();await change('landmask');
|
|
check('Categorical controls are visible',!el('[data-map-ocean]').hidden&&!el('[data-map-land]').hidden);
|
|
el('[data-map-ocean]').click();check('Ocean selects zero',Number(el('[data-map-value]').value)===fields.landmask.min);
|
|
el('[data-map-land]').click();check('Land selects one',Number(el('[data-map-value]').value)===fields.landmask.max);
|
|
el('[data-map-value]').value='.5';paint();check('Fractional mask is not painted',el('[data-map-save]').disabled&&el('[data-map-status]').textContent==='Choose Ocean or Land.');
|
|
el('[data-map-land]').click();paint();check('Land brush paints',!el('[data-map-save]').disabled);
|
|
el('[data-map-undo]').click();await change('albedo');
|
|
el('[data-map-tool]').value='set';el('[data-map-value]').value=String(fields.albedo.max+1);paint();
|
|
check('Out-of-range albedo is not painted',el('[data-map-save]').disabled&&el('[data-map-status]').textContent.startsWith('Value must be'));
|
|
el('[data-map-value]').value='.15';paint();mode='save-fail';el('[data-map-save]').click();await wait(()=>el('[data-map-status]').textContent==='Test save failure');
|
|
check('Failed save retains changes',!el('[data-map-save]').disabled);
|
|
mode='save-hold';el('[data-map-save]').click();await wait(()=>Boolean(releaseSave));
|
|
check('Saving cannot close',!editor.canClose());releaseSave();await wait(()=>el('[data-map-status]').textContent.startsWith('Map saved'));
|
|
check('Successful save becomes clean',el('[data-map-save]').disabled);
|
|
mode='fail';await change('height');check('Load failure clears old values',el('[data-map-position]').textContent===''&&el('[data-map-min]').textContent===''&&el('[data-map-save]').disabled);
|
|
mode='normal';await change('albedo');check('Load recovers',Boolean(el('[data-map-position]').textContent));
|
|
mode='hold';el('[data-map-field]').value='height';el('[data-map-field]').dispatchEvent(new Event('change'));await wait(()=>Boolean(releaseLoad));
|
|
check('Loading can close',editor.canClose());editor.destroy();releaseLoad();await new Promise(resolve=>setTimeout(resolve,0));
|
|
check('Destroyed editor ignores delayed load',root.childElementCount===0);
|
|
const benchRoot=document.createElement('div');root.append(benchRoot);const g=schema.map_grids.T85;
|
|
bench=new ExoMapEditor(benchRoot,{...callbacks,fields:{height:fields.height},load:async()=>({values:values('height',g),grid:g})});await bench.start();
|
|
const times=[],c=benchRoot.querySelector('canvas');
|
|
for(let i=0;i<40;i++){const start=performance.now();c.dispatchEvent(new KeyboardEvent('keydown',{key:' ',bubbles:true,cancelable:true}));times.push(performance.now()-start);}
|
|
times.sort((a,b)=>a-b);report.benchmark={resolution:'T85',cells:g.nlat*g.nlon,repetitions:times.length,meanMs:times.reduce((a,b)=>a+b)/times.length,p95Ms:times[37],maxMs:times.at(-1),compositorIncluded:false};
|
|
check('T85 benchmark uses its actual grid',g.lat.length===g.nlat&&g.lon.length===g.nlon&&g.nlat>grid.nlat);
|
|
report.ok=true;
|
|
}catch(error){report.error=String(error.stack||error);}
|
|
finally{releaseLoad?.();releaseSave?.();editor?.destroy();bench?.destroy();root.remove();if(focused?.isConnected)focused.focus({preventScroll:true});window.scrollTo(...scroll);}
|
|
return report;
|
|
})()
|