Automatically resume empty saved source reviews
This commit is contained in:
1 parent
cd362b3b81
commit
976a4fa7b2
11 files changed
+585
-29
No files matched your search
@@ -9,6 +9,7 @@ const task={request_id:requestId,report_date:'2026-10-07',status:'needs_data_rev
|
||||
const item=(extra={})=>({item_id:'1:BLOCK_CODE',source_sequence:1,confirmation_no:'SYNTHETIC-001',room_no:'SYNTHETIC-ROOM',company_name:'Synthetic Company',rate_code:'SYNTHETIC',field:'BLOCK_CODE',field_label:'团队代码',reason_code:'MISSING',can_be_empty:true,value:null,confirmed:false,...extra});
|
||||
const review=(items,revision=1)=>({request_id:requestId,report_date:task.report_date,status:'editing',revision,items,pending_count:items.filter(x=>!x.confirmed).length,total_count:items.length,can_finalize:items.every(x=>x.confirmed)});
|
||||
const plain=value=>JSON.parse(JSON.stringify(value));
|
||||
const activeTaskPoll=h=>h.timers.filter(timer=>!timer.cancelled&&timer.callback.name==='loadARRDownloadTask').at(-1);
|
||||
const priceTask={request_id:'c'.repeat(32),report_date:'2026-09-16',status:'needs_review',
|
||||
job_id:'fixture-price-september',can_retry:false};
|
||||
const priceReview=(price=null)=>({case_id:'fixture-price-case',case_status:'open',business_date:priceTask.report_date,revision:1,
|
||||
@@ -27,6 +28,77 @@ function pendingAPI(url,currentPrice=priceTask) {
|
||||
throw new Error('unexpected request '+url);
|
||||
}
|
||||
|
||||
test('an empty validated field review polls the same task and follows processing then price review by GET only',async()=>{
|
||||
let nextStatus='processing';
|
||||
const jobId='fixture-recovered-empty-review';
|
||||
const h=harness(url=>{
|
||||
if(url.endsWith('/data-review')) return response(200,review([]));
|
||||
if(url===`/api/arr-downloads/${requestId}`) return response(200,{...task,status:nextStatus,job_id:nextStatus==='processing'?null:jobId});
|
||||
if(url.startsWith('/api/jobs?')) return response(200,[]);
|
||||
if(url.endsWith('/trace')) return response(200,{job:{job_id:jobId,business_date:task.report_date,status:nextStatus,active:true},logs:[]});
|
||||
if(url.includes('/review?')) return response(200,{...priceReview(),business_date:task.report_date});
|
||||
throw new Error('unexpected request '+url);
|
||||
});
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
const emptyPoll=activeTaskPoll(h);
|
||||
assert.equal(emptyPoll?.delay,4000);
|
||||
await emptyPoll.callback();
|
||||
assert.equal(h.state.arrDownloadTask.status,'processing');
|
||||
assert.equal(h.state.arrDataReview,null);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
nextStatus='needs_review';
|
||||
await activeTaskPoll(h).callback();
|
||||
assert.equal(h.state.arrDownloadTask.status,'needs_review');
|
||||
assert.equal(h.element('#arr-download-review').hidden,false);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(h.state.arrDownloadPendingReviews[0].status,'needs_review');
|
||||
await h.openDailyPriceReview(jobId,false);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,false);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,false);
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
assert.equal(h.calls.some(call=>call.url.includes('/finalize')),false);
|
||||
});
|
||||
|
||||
for(const status of ['succeeded','failed']) {
|
||||
test(`empty-review recovery follows the real ${status} task status without submitting a decision`,async()=>{
|
||||
const h=harness(url=>{
|
||||
if(url.endsWith('/data-review')) return response(200,review([]));
|
||||
assert.equal(url,`/api/arr-downloads/${requestId}`);
|
||||
return response(200,{...task,status,error_code:status==='failed'?'ARR_DATA_EXECUTION_FAILED':null});
|
||||
});
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
await activeTaskPoll(h).callback();
|
||||
assert.equal(h.state.arrDownloadTask.status,status);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
assert.equal(h.element('#arr-download-pending').hidden,true);
|
||||
assert.match(h.element('#arr-download-status').textContent,new RegExp(`arr_download\\.${status}`));
|
||||
assert.equal(activeTaskPoll(h),undefined);
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
});
|
||||
}
|
||||
|
||||
test('filled real review items, inconsistent zero counts and unsaved drafts never trigger zero-item recovery polling',async()=>{
|
||||
for(const snapshot of [review([item({confirmed:true,value:'SAVED'})]),
|
||||
{...review([]),total_count:1}, {...review([]),pending_count:1}, {...review([]),can_finalize:false},
|
||||
{...review([]),items:[item()]}, {...review([]),request_id:'f'.repeat(32)},
|
||||
{...review([]),report_date:'2026-09-17'}]) {
|
||||
const h=harness(()=>response(200,snapshot));
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
assert.equal(activeTaskPoll(h),undefined);
|
||||
assert.equal(h.calls.length,1);
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
}
|
||||
const h=harness(()=>response(200,review([item({confirmed:true,value:'SAVED'})])));
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','UNSAVED').input);
|
||||
h.state.arrDataReview=review([]);
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
assert.equal(activeTaskPoll(h),undefined);
|
||||
assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'UNSAVED');
|
||||
assert.equal(h.calls.some(call=>call.url.includes('/finalize')),false);
|
||||
});
|
||||
|
||||
test('cross-month field and price dates restore, stay clickable after polling, and open their own pages by GET',async()=>{
|
||||
const h=harness(url=>url==='/api/arr-downloads'
|
||||
? response(200,{context_id:'production',ready:true,default_date:priceTask.report_date,
|
||||
|
||||
@@ -9,7 +9,7 @@ const boot = ' boot();\n})();';
|
||||
assert(source.endsWith(boot+'\n') || source.endsWith(boot));
|
||||
|
||||
function harness(respond) {
|
||||
const elements=new Map(), storage=new Map(), storageReads=[], calls=[];
|
||||
const elements=new Map(), storage=new Map(), storageReads=[], calls=[], timers=[];
|
||||
const element=selector => {
|
||||
if (!elements.has(selector)) {
|
||||
const attributes=new Map(),classes=new Set();
|
||||
@@ -27,7 +27,8 @@ function harness(respond) {
|
||||
const context=vm.createContext({Headers, console, Date, Intl, Uint8Array,
|
||||
document:{hidden:false,querySelector:element},
|
||||
window:{ARRI18n:{t:(key,values)=>key==='manual_review.context'?`${values.date} · ${values.type}`:key,text:value=>value,errorMessage:code=>code,formatInteger:value=>String(value),formatMonth:value=>value},crypto:webcrypto,
|
||||
setTimeout(){return 1;},clearTimeout(){},location:{replace(){}}},
|
||||
setTimeout(callback,delay){const timer={id:timers.length+1,callback,delay,cancelled:false};timers.push(timer);return timer.id;},
|
||||
clearTimeout(id){const timer=timers.find(entry=>entry.id===id);if(timer) timer.cancelled=true;},location:{replace(){}}},
|
||||
localStorage:{setItem:(k,v)=>storage.set(k,v),getItem:k=>{storageReads.push(k);return storage.get(k);},removeItem:k=>storage.delete(k)},
|
||||
fetch:async (url, options) => {calls.push({url,method:options.method||'GET',body:options.body,csrf:options.headers.get('X-ARR-CSRF')});return respond(url,options,calls);},
|
||||
});
|
||||
@@ -41,7 +42,7 @@ function harness(respond) {
|
||||
const node={dataset:{arrDataReviewItemId:itemId},querySelector:()=>input};
|
||||
return {input,button:{closest:()=>node}};
|
||||
};
|
||||
return {...subject,calls,storage,storageReads,element,row,i18n:context.window.ARRI18n,submit:()=>subject.submitARRDownload({preventDefault(){}})};
|
||||
return {...subject,calls,storage,storageReads,timers,element,row,i18n:context.window.ARRI18n,submit:()=>subject.submitARRDownload({preventDefault(){}})};
|
||||
}
|
||||
const response=(status,data,code) => ({status,ok:status<400,json:async()=>code?{ok:false,error:{code}}:{ok:true,data}});
|
||||
const scopedKey=(context='production',username='operator')=>`arr:last-download:v2:${encodeURIComponent(context)}:${encodeURIComponent(username)}`;
|
||||
|
||||
Reference in new issue
Block a user