'use strict'; const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'); const source=fs.readFileSync(require('node:path').join(__dirname,'../js/app.js'),'utf8'); const helpers=source.slice(source.indexOf(' var SCAN_WAIT_TIMEOUT_MS'),source.indexOf(' function restartScanCycle(')); const submit=source.slice(source.indexOf(' function submitPhotos('),source.indexOf(' function shutter()')); function setup() { let now=0, next=0, done, resolveAnimation, aborted=0, stopped=0, rendered=0; const timers=new Map(),messages=[],events=[]; const nodes={'#f-age':{value:30},'#f-gender':{value:'女'},'#p-analyzing':{show:false},'#s-report':{classList:{contains:()=>ctx.currentScreen==='s-report'}}}; const ctx={Date:{now:()=>now},Promise,console,camSeq:1,camStep:2,photos:['a','b','c'],captureDiagnostics:[],shutterLock:true,currentScreen:'s-cam', setTimeout(fn,ms){const id=++next;timers.set(id,{fn,ms});return id;},clearTimeout(id){timers.delete(id);}, $:s=>nodes[s],photosByView:p=>p,openOverlay(){nodes['#p-analyzing'].show=true;ctx.scanCycle={promise:new Promise(r=>{resolveAnimation=r;})};}, closeOverlay(){nodes['#p-analyzing'].show=false;ctx.stopAnalysisWait();resolveAnimation();}, voice:{play(){},stop(){}},VOICE:{analyzing:'a',fail:'f'},regionSel:{value:'上海'},SKINS:[{name:'中性'}],skinIndex:0, trackCapture:s=>events.push(s),stopCamera(){stopped++;},toast:s=>messages.push(s),showScreen:s=>{ctx.currentScreen=s;},popHistorySilently(){}, requestAnalyze(p,s,profile,cb){done=cb;return ()=>{aborted++;};}, SkinScore:{buildViewModel:()=>({})},renderReport(){rendered++;},IS_MOCK:false,AccountClient:{user:()=>({kind:'wechat'})}}; vm.createContext(ctx);vm.runInContext(helpers+submit,ctx);ctx.submitPhotos(1); return {ctx,timers,messages,events,advance(ms,fire=true){now=ms;if(fire){const jobs=[...timers.values()];jobs.forEach(j=>j.fn());}},respond(error=null){done(error,{vm:{},report_saved:true});},animation(){resolveAnimation();},get aborted(){return aborted;},get rendered(){return rendered;},get stopped(){return stopped;}}; } const flush=()=>new Promise(r=>setImmediate(r)); (async()=>{ const pending=setup();assert.equal([...pending.timers.values()][0].ms,30000); pending.advance(30000);await flush();assert.equal(pending.ctx.currentScreen,'s-profile');assert.equal(pending.messages[0],'检测服务开小差了,请重试');assert.equal(pending.ctx.shutterLock,false);assert.equal(pending.ctx.photos.length,0);assert.equal(pending.aborted,1);assert.equal(pending.timers.size,0); pending.respond();await flush();assert.equal(pending.rendered,0);assert.equal(pending.messages.length,1); const stuck=setup();stuck.respond();await flush();assert.equal(stuck.ctx.currentScreen,'s-cam');stuck.advance(30000);await flush();assert.equal(stuck.ctx.currentScreen,'s-profile');assert.equal(stuck.rendered,0); const success=setup();success.advance(29999,false);success.respond();success.animation();await flush();assert.equal(success.ctx.currentScreen,'s-report');assert.equal(success.timers.size,0);assert.equal(success.messages.length,0);assert.equal(success.rendered,1); const late=setup();late.advance(31000,false);late.respond();await flush();assert.equal(late.ctx.currentScreen,'s-profile');assert.equal(late.rendered,0,'deadline checked even before delayed timer executes'); const cancel=setup();cancel.ctx.camSeq++;cancel.ctx.closeOverlay();cancel.advance(30000);cancel.respond();await flush();assert.equal(cancel.messages.length,0);assert.equal(cancel.timers.size,0);assert.equal(cancel.rendered,0); console.log('PASS: scan page 30s watchdog covers pending network and stuck animation, success cleanup, late callbacks, legacy timing and cancellation'); })().catch(e=>{console.error(e);process.exitCode=1;});