PixText-Studio / tests /test_examples.mjs
lsmpp's picture
Show detection and refinement progress in a centered activity card with visible stages and completion feedback
60d86b6 verified
Raw History Blame Contribute Delete
6.77 kB
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import test from 'node:test';
import vm from 'node:vm';
const script = readFileSync(new URL('../static/app.js', import.meta.url), 'utf8');
const presets = Object.fromEntries(['english', 'chinese'].map(name => [name,
JSON.parse(readFileSync(new URL(`../examples/${name}.json`, import.meta.url), 'utf8'))]));
// Run the real editor against a small DOM/fetch adapter; no models or browser packages needed.
function editor() {
const elements = new Map(), requests = [], decoded = [];
let nextId = 0, failExample = null;
const element = id => {
if(!elements.has(id)) elements.set(id, {value:'', dataset:{}, style:{setProperty(){}},
classList:{toggle(){},add(){},remove(){}}, setAttribute(name,value){this.attributes??={};this.attributes[name]=value;}, removeAttribute(name){delete this.attributes?.[name];}, addEventListener(){},
getContext(){return {drawImage(){}};}, children:[], focus(){}});
return elements.get(id);
};
const examples = ['english','chinese'].map(example => ({...element(example),dataset:{example}}));
const models = ['meanflow','teacher'].map(model => ({...element(model),dataset:{model}}));
const context = vm.createContext({structuredClone, URLSearchParams, console, setTimeout:(fn,ms)=>setTimeout(fn,ms).unref(), clearTimeout,
document:{getElementById:element,addEventListener(){},querySelectorAll(selector){
if(selector==='[data-example]') return examples;
if(selector==='[data-model]') return models;
return [];
}},
Image:class {async decode(){decoded.push(this.src);this.complete=true;}},
location:{pathname:'/',hash:''}, history:{replaceState(){}},
fetch:async(url,options) => {
if(url==='/api/config') return {ok:true,json:async()=>({transport:'http'})};
assert.equal(url,'/api/session');
const {example}=JSON.parse(options.body);requests.push(example);
if(example===failExample){failExample=null;throw new Error('Network unavailable');}
return {ok:true,json:async()=>({id:`session-${++nextId}`,example,width:1024,height:1024,
image:`/examples/${example}-preview.webp?v=release`,
regions:presets[example].map((r,i)=>({...r,id:`example-${i}`,enabled:true,origin:'example'})),
settings:{variant:'meanflow',steps:4,seed:42,guidance:2.6,text_threshold:.5}})};
}});
vm.runInContext(script,context);
const run=code=>vm.runInContext(code,context);
return {run,requests,decoded,element,failNext(example){failExample=example;}};
}
async function ready(app) {
await app.run("Promise.all([prepareExample('english'),prepareExample('chinese')])");
await new Promise(resolve=>setImmediate(resolve));
}
test('switching and restoring example edits reuse prepared sessions and decoded images', async()=>{
const app=editor();await ready(app);
assert.equal(app.requests.length,2);
assert.equal(app.decoded.length,2);
for(const example of ['chinese','english','chinese','english']) {
await app.run(`loadExample('${example}')`);
assert.equal(app.run('state.regions.length'),presets[example].length);
assert.equal(app.element('guidance').value,2.6);
assert.equal(app.element('refineButton').disabled,false);
app.run("state.regions[0].text='edited'; state.regions[0].bbox[0]=0; state.regions[0].enabled=false;");
}
await app.run("loadExample('english')");
assert.equal(app.run('state.regions[0].text'),presets.english[0].text);
assert.equal(app.run('state.regions[0].bbox[0]'),presets.english[0].bbox[0]);
assert.equal(app.run('state.regions[0].enabled'),true);
assert.equal(app.requests.length,2);
assert.equal(app.decoded.length,2);
});
test('concurrent preparation shares one request and an expired session is renewed',async()=>{
const app=editor();await ready(app);
app.run("exampleCache.get('chinese').expires=0");
await app.run("Promise.all([prepareExample('chinese'),prepareExample('chinese')])");
assert.equal(app.requests.filter(x=>x==='chinese').length,2);
});
test('failed preparation is retryable without replacing the current editor',async()=>{
const app=editor();await ready(app);
await app.run("loadExample('english')");
const current=app.run('state.session.id');
app.run("exampleCache.get('chinese').expires=0");app.failNext('chinese');
await app.run("loadExample('chinese')");
assert.equal(app.run('state.session.id'),current);
assert.equal(app.run('state.busy'),false);
await app.run("loadExample('chinese')");
assert.equal(app.run('state.regions.length'),13);
});
test('detection immediately shows central activity and keeps unknown progress indeterminate',async()=>{
const app=editor();await ready(app);
app.run("setBusy(true,'Preparing detection','detect'); updateActivity('Detecting characters and words',.3,true)");
assert.equal(app.element('jobProgress').hidden,false);
assert.equal(app.element('jobTitle').textContent,'Detecting text');
assert.equal(app.element('jobPercent').hidden,true);
assert.equal(app.element('progressTrack').attributes['aria-valuenow'],undefined);
assert.equal(app.element('detectButton').attributes['aria-busy'],'true');
});
test('refinement displays reported progress, then leaves a nonblocking completion state',async()=>{
const app=editor();await ready(app);
app.run("setBusy(true,'Waiting for GPU','refine');updateActivity('Refining · step 2 of 4',.55,false)");
assert.equal(app.element('jobTitle').textContent,'Refining text');
assert.equal(app.element('jobPercent').textContent,'55%');
assert.equal(app.element('jobPercent').hidden,false);
assert.equal(app.element('progressTrack').attributes['aria-valuenow'],'55');
app.run("finishActivity('done','4 regions · 2.7s');setBusy(false)");
assert.equal(app.element('jobProgress').dataset.state,'done');
assert.equal(app.element('jobProgress').hidden,false);
assert.equal(app.element('refineButton').attributes['aria-busy'],'false');
app.run("setBusy(true,'Preparing detection','detect')");
assert.equal(app.element('jobProgress').dataset.state,'running');
assert.equal(app.element('jobTitle').textContent,'Detecting text');
});
test('a job error remains visible after controls are released and can be dismissed',async()=>{
const app=editor();await ready(app);
app.run("setBusy(true,'Waiting for GPU','refine');finishActivity('error','GPU quota exceeded');setBusy(false)");
assert.equal(app.element('jobProgress').hidden,false);
assert.equal(app.element('jobProgress').dataset.state,'error');
assert.equal(app.element('jobMessage').textContent,'GPU quota exceeded');
assert.equal(app.element('closeProgress').hidden,false);
app.run('dismissActivity()');assert.equal(app.element('jobProgress').hidden,true);
});