fix(camera): add 30-second scan waiting watchdog
This commit is contained in:
@@ -8,9 +8,9 @@ const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||
const start = source.indexOf(' var ANALYZE_TIMEOUT_MS =');
|
||||
const end = source.indexOf(' /* ─── SkinScore viewModel', start);
|
||||
assert(start >= 0 && end > start);
|
||||
function setup() {
|
||||
let timer, complete, signal, result;
|
||||
const ctx = { IS_MOCK: false, camSeq: 1, AbortController, CaptureQuality: require('../js/capture-quality'), captureDiagnostics: [],
|
||||
function setup(withAbort = true) {
|
||||
let timer, complete, signal, result, calls = 0;
|
||||
const ctx = { IS_MOCK: false, camSeq: 1, AbortController: withAbort ? AbortController : undefined, CaptureQuality: require('../js/capture-quality'), captureDiagnostics: [],
|
||||
setTimeout(fn, delay) { timer = { fn, delay }; return 1; },
|
||||
clearTimeout() { timer = null; },
|
||||
fetch(url, options) {
|
||||
@@ -18,21 +18,20 @@ function setup() {
|
||||
signal = options.signal;
|
||||
return new Promise((resolve, reject) => {
|
||||
complete = () => resolve({ json: async () => ({ ok: true, report_saved: true }) });
|
||||
signal.addEventListener('abort', () => { const e = new Error('aborted'); e.name = 'AbortError'; reject(e); });
|
||||
if (signal) signal.addEventListener('abort', () => { const e = new Error('aborted'); e.name = 'AbortError'; reject(e); });
|
||||
});
|
||||
}
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(source.slice(start, end), ctx);
|
||||
ctx.requestAnalyze(['front', 'left', 'right'], 1, {}, (...args) => { result = args; });
|
||||
const cancel = ctx.requestAnalyze(['front', 'left', 'right'], 1, {}, (...args) => { result = args; calls++; });
|
||||
return { ctx, get timer() { return timer; }, get signal() { return signal; },
|
||||
get result() { return result; }, finish() { complete(); } };
|
||||
get result() { return result; }, get calls() { return calls; }, cancel, finish() { complete(); } };
|
||||
}
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve));
|
||||
(async () => {
|
||||
const delayed = setup();
|
||||
assert.equal(delayed.timer.delay, 150000);
|
||||
assert(delayed.timer.delay > 60000 * 2 + 4000, 'Allow both backend attempts plus retry wait');
|
||||
assert.equal(delayed.timer.delay, 30000);
|
||||
delayed.finish();
|
||||
await flush();
|
||||
assert.equal(delayed.result[0], null);
|
||||
@@ -44,11 +43,18 @@ const flush = () => new Promise(resolve => setImmediate(resolve));
|
||||
await flush();
|
||||
assert(expired.signal.aborted);
|
||||
assert.match(expired.result[0], /检测服务开小差/);
|
||||
assert.equal(expired.calls, 1);
|
||||
const legacy = setup(false);
|
||||
legacy.timer.fn();
|
||||
assert.equal(legacy.result[0], '检测服务开小差了,请重试');
|
||||
legacy.finish();await flush();assert.equal(legacy.calls,1,'late success never replaces timeout');
|
||||
const aborted = setup();aborted.cancel();await flush();
|
||||
assert(aborted.signal.aborted);assert.equal(aborted.result,undefined);assert.equal(aborted.timer,null);
|
||||
const canceled = setup();
|
||||
canceled.ctx.camSeq++;
|
||||
canceled.finish();
|
||||
await flush();
|
||||
assert.equal(canceled.result, undefined);
|
||||
assert.equal(canceled.timer, null);
|
||||
console.log('PASS: frontend timeout covers backend retry budget; success, timeout, stale response, timer cleanup');
|
||||
console.log('PASS: 30s request timeout, legacy no-AbortController fallback, single completion, cancellation, late response suppression and timer cleanup');
|
||||
})().catch(e => { console.error(e); process.exitCode = 1; });
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
'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;});
|
||||
Reference in New Issue
Block a user