Show durable reservation acquisition progress in download tasks

This commit is contained in:
Wyndham ARR committed 2026-10-08 23:42:31 +08:00
1 parent 698d91367c
commit db42a5e087
16 files changed
+757 -23

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
// Actual progress rendering against synthetic task snapshots; no Oracle requests.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const {harness,response,scopedKey} = require('./helpers/arr_ui_harness.cjs');
const localeContext=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}});
vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),localeContext);
const catalog=localeContext.window.ARRI18n.CATALOG;
const date='2026-09-16';
const task={request_id:'d'.repeat(32),report_date:date,status:'downloading',job_id:null,can_retry:false};
const progress=(phase,completed,total,percent)=>({phase,completed,total,percent,updated_at:'2026-10-08T05:00:00Z'});
function setup(respond=()=>{throw new Error('rendering must not send a request');}) {
const h=harness(respond);
h.i18n.t=(key,values={})=>(catalog[key]?.[0]||key).replace(/\{(\w+)\}/g,(_,name)=>String(values[name]??''));
return h;
}
async function render(h,snapshot) {
await h.acceptARRDownloadTask(snapshot,{sync:false});
}
test('unknown reservation totals remain indeterminate and never invent zero totals or percentages',async()=>{
const h=setup();
for(const phase of ['queued','searching','fetching']) {
await render(h,{...task,status:phase==='queued'?'queued':'downloading',acquisition_progress:progress(phase,0,null,null)});
assert.equal(h.element('#arr-acquisition-progress').hidden,false);
assert.equal(h.element('#arr-acquisition-track').getAttribute('aria-valuenow'),null);
assert.equal(h.element('#arr-acquisition-track').classList.contains('is-indeterminate'),true);
assert.equal(h.element('#arr-acquisition-percent').hidden,true);
assert.doesNotMatch(h.element('#arr-acquisition-detail').textContent,/0\s*\/\s*0|%/);
}
assert.equal(h.calls.length,0);
});
test('a real 58 of 85 snapshot renders server progress and accessible counts',async()=>{
const h=setup();
await render(h,{...task,acquisition_progress:progress('fetching',58,85,68)});
assert.equal(h.element('#arr-acquisition-status').textContent,'取数中');
assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 58 / 85 笔');
assert.equal(h.element('#arr-acquisition-percent').textContent,'68%');
assert.equal(h.element('#arr-acquisition-track').getAttribute('aria-valuenow'),'68');
assert.match(h.element('#arr-acquisition-track').getAttribute('aria-valuetext'),/58 \/ 85.*68%/);
assert.equal(h.element('#arr-acquisition-track').classList.contains('is-indeterminate'),false);
assert.equal(h.element('#arr-acquisition-bar').style.width,'68%');
});
test('all records still show 99 percent while completeness is being verified',async()=>{
const h=setup();
for(const percent of [99,100]) {
await render(h,{...task,acquisition_progress:progress('verifying',85,85,percent)});
assert.equal(h.element('#arr-acquisition-status').textContent,'取数中');
assert.equal(h.element('#arr-acquisition-percent').textContent,'99%');
assert.match(h.element('#arr-acquisition-detail').textContent,/85 \/ 85.*正在校验完整性/);
assert.equal(h.element('#arr-acquisition-track').getAttribute('aria-valuenow'),'99');
}
});
test('a verified empty result is a real completed zero total',async()=>{
const h=setup();
await render(h,{...task,status:'succeeded',acquisition_progress:progress('completed',0,0,100)});
assert.equal(h.element('#arr-acquisition-status').textContent,'已完成');
assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 0 / 0 笔');
assert.equal(h.element('#arr-acquisition-percent').textContent,'100%');
});
test('completed retrieval remains distinct from pending field and price review',async()=>{
for(const status of ['needs_data_review','needs_review','processing','succeeded']) {
const h=setup(url=>{
assert.equal(url,`/api/arr-downloads/${task.request_id}/data-review`);
return response(200,{request_id:task.request_id,report_date:date,status:'editing',items:[],pending_count:0,total_count:0});
});
await render(h,{...task,status,job_id:status==='needs_data_review'?null:'fixture-job',acquisition_progress:progress('completed',85,85,100)});
assert.equal(h.element('#arr-acquisition-status').textContent,'已完成');
assert.equal(h.element('#arr-acquisition-percent').textContent,'100%');
assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 85 / 85 笔');
if(status==='needs_data_review') assert.equal(h.element('#arr-download-data-review').hidden,false);
if(status==='needs_review') assert.equal(h.element('#arr-download-review').hidden,false);
assert.doesNotMatch(h.element('#arr-acquisition-detail').textContent,/生成|报表已完成/);
}
});
test('failed and interrupted retrieval never appear as 100 percent complete',async()=>{
const h=setup();
for(const phase of ['failed','interrupted']) {
await render(h,{...task,status:phase,acquisition_progress:progress(phase,85,85,100)});
assert.equal(h.element('#arr-acquisition-status').textContent,phase==='failed'?'取数失败':'取数中断');
assert.equal(h.element('#arr-acquisition-progress').classList.contains('is-completed'),false);
assert.equal(h.element('#arr-acquisition-progress').classList.contains('is-error'),true);
assert.equal(h.element('#arr-acquisition-percent').textContent,'99%');
assert.equal(h.element('#arr-acquisition-track').classList.contains('is-indeterminate'),false);
}
});
test('legacy metadata absence shows truthful status without fabricated counts',async()=>{
const h=setup();
await render(h,task);
assert.equal(h.element('#arr-acquisition-status').textContent,'取数中');
assert.equal(h.element('#arr-acquisition-track').classList.contains('is-indeterminate'),true);
assert.equal(h.element('#arr-acquisition-detail').textContent,'正在获取预订数据…');
await render(h,{...task,status:'needs_review',job_id:'fixture-job'});
assert.equal(h.element('#arr-acquisition-status').textContent,'已完成');
assert.equal(h.element('#arr-acquisition-percent').hidden,true);
assert.equal(h.element('#arr-acquisition-track').hidden,true);
assert.equal(h.element('#arr-acquisition-detail').textContent,'该任务未记录取数笔数。');
for(const status of ['failed','interrupted']) {
await render(h,{...task,status});
assert.equal(h.element('#arr-acquisition-percent').hidden,true);
assert.equal(h.element('#arr-acquisition-track').hidden,true);
assert.notEqual(h.element('#arr-acquisition-status').textContent,'已完成');
}
});
test('progress hides for another selected date and returns for the original running task',async()=>{
const h=setup();
await render(h,{...task,acquisition_progress:progress('fetching',58,85,68)});
h.element('#arr-download-date').value='2026-09-17';
h.handleARRDownloadDateChange();
assert.equal(h.element('#arr-acquisition-progress').hidden,true);
assert.equal(h.state.arrDownloadTask.report_date,date);
assert.match(h.element('#arr-download-status').textContent,/2026-09-16/);
h.element('#arr-download-date').value=date;
h.handleARRDownloadDateChange();
assert.equal(h.element('#arr-acquisition-progress').hidden,false);
assert.equal(h.element('#arr-acquisition-percent').textContent,'68%');
assert.equal(h.calls.length,0);
});
test('context changes hide the former task progress and empty selection has none',async()=>{
const h=setup(()=>response(200,{context_id:'new-context',ready:true,default_date:date,pending_reviews:[],latest_task:null}));
h.state.arrDownloadContextId='old-context';
h.state.arrDownloadStorageKey=scopedKey('old-context');
await render(h,{...task,acquisition_progress:progress('fetching',58,85,68)});
await h.initARRDownload();
assert.equal(h.state.arrDownloadTask,null);
assert.equal(h.element('#arr-acquisition-progress').hidden,true);
});
test('acquisition labels, counts and completeness guidance are available in all three languages',()=>{
for(const [key,values] of Object.entries(catalog).filter(([key])=>key.startsWith('arr_acquisition.'))) {
assert.equal(values.length,3,key);
assert.equal(values.every(value=>typeof value==='string'&&value.length>0),true,key);
assert.match(values[2],/[\u0e00-\u0e7f]/,key);
}
assert.match(catalog['arr_acquisition.verifying_counts'][0],/校验完整性/);
});
+12 -3
View File
@@ -11,8 +11,17 @@ assert(source.endsWith(boot+'\n') || source.endsWith(boot));
function harness(respond) {
const elements=new Map(), storage=new Map(), storageReads=[], calls=[];
const element=selector => {
if (!elements.has(selector)) elements.set(selector,{value:'', disabled:false, hidden:false, textContent:'', innerHTML:'',style:{},
classList:{add(){},remove(){},toggle(){},contains(){return false;}},setAttribute(){},removeAttribute(){},querySelectorAll(){return [];},scrollIntoView(){},focus(){}});
if (!elements.has(selector)) {
const attributes=new Map(),classes=new Set();
elements.set(selector,{value:'', disabled:false, hidden:false, textContent:'', innerHTML:'',style:{},
classList:{add:value=>classes.add(value),remove:value=>classes.delete(value),toggle(value,force){
const enabled=force===undefined?!classes.has(value):force;
if(enabled) classes.add(value); else classes.delete(value);
return enabled;
},contains:value=>classes.has(value)},setAttribute:(key,value)=>attributes.set(key,String(value)),
removeAttribute:key=>attributes.delete(key),getAttribute:key=>attributes.get(key)??null,
querySelectorAll(){return [];},scrollIntoView(){},focus(){}});
}
return elements.get(selector);
};
const context=vm.createContext({Headers, console, Date, Intl, Uint8Array,
@@ -32,7 +41,7 @@ function harness(respond) {
const node={dataset:{arrDataReviewItemId:itemId},querySelector:()=>input};
return {input,button:{closest:()=>node}};
};
return {...subject,calls,storage,storageReads,element,row,submit:()=>subject.submitARRDownload({preventDefault(){}})};
return {...subject,calls,storage,storageReads,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)}`;