Make report coverage calendar expandable

This commit is contained in:
Wyndham ARR committed 2026-10-09 13:55:41 +08:00
1 parent 4356160cc9
commit 6d1a98743f
11 files changed
+283 -57

No files matched your search

+10 -6
View File
@@ -9,10 +9,10 @@ const boot = ' boot();\n})();';
assert(source.endsWith(boot+'\n') || source.endsWith(boot));
function harness(respond) {
const elements=new Map(), storage=new Map(), storageReads=[], calls=[], timers=[];
const elements=new Map(), storage=new Map(), storageReads=[], calls=[], timers=[], documentListeners=new Map();
const element=selector => {
if (!elements.has(selector)) {
const attributes=new Map(),classes=new Set();
const attributes=new Map(),classes=new Set(),listeners=new Map();
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;
@@ -20,19 +20,21 @@ function harness(respond) {
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,
querySelector:selector=>element(selector),querySelectorAll(){return [];},scrollIntoView(){},focus(){context.document.activeElement=this;}});
querySelector:selector=>element(selector),querySelectorAll(){return [];},scrollIntoView(){},focus(){context.document.activeElement=this;},
addEventListener(type,callback){if(!listeners.has(type)) listeners.set(type,[]);listeners.get(type).push(callback);},
dispatchEvent(event){for(const callback of listeners.get(event.type)||[]) callback(event);},contains(node){return node===this;}});
}
return elements.get(selector);
};
const context=vm.createContext({Headers, console, Date, Intl, Uint8Array, TextEncoder, btoa,
document:{hidden:false,querySelector:element},
document:{hidden:false,querySelector:element,addEventListener(type,callback){if(!documentListeners.has(type)) documentListeners.set(type,[]);documentListeners.get(type).push(callback);}},
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,formatDate:value=>value,formatDurationSeconds:value=>`${value}s`},crypto:webcrypto,
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);},
});
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview, strictReviewPrice, reviewPriceInput, renderDailyPriceReview, saveDailyPriceReviewItem, loadDailyPriceReview, openDailyPriceReview, dailyPriceReviewCanFinalize, dailyPriceReviewHasUnsavedChanges, finalizeDailyPriceReview, confirmDailyPriceReviewCancel, handleUpload, renderJobs, renderDailyOverview, syncDailyOverviewDate, loadDailyOverview, selectDailyReportDate, loadJobs, loadDailyHistory, openDailyHistory, closeDailyHistory, openDailyDelete, closeDailyDelete, confirmDailyDelete, localMonth, localDate, shiftMonth, renderReportCalendar, loadReportCalendar, changeReportCalendarMonth, selectReportCalendarDate, moveReportCalendarFocus, reportCalendarReady, currentReportGenerated';
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview, strictReviewPrice, reviewPriceInput, renderDailyPriceReview, saveDailyPriceReviewItem, loadDailyPriceReview, openDailyPriceReview, dailyPriceReviewCanFinalize, dailyPriceReviewHasUnsavedChanges, finalizeDailyPriceReview, confirmDailyPriceReviewCancel, handleUpload, renderJobs, renderDailyOverview, syncDailyOverviewDate, loadDailyOverview, selectDailyReportDate, loadJobs, loadDailyHistory, openDailyHistory, closeDailyHistory, openDailyDelete, closeDailyDelete, confirmDailyDelete, localMonth, localDate, shiftMonth, renderReportCalendar, loadReportCalendar, changeReportCalendarMonth, selectReportCalendarDate, moveReportCalendarFocus, reportCalendarReady, currentReportGenerated, openReportCalendar, closeReportCalendar, bindReportCalendar, openARRCalendar, closeARRCalendar, arrCalendar';
vm.runInContext(source.replace(boot,` globalThis.subject = {${exports}};\n})();`),context);
const subject=context.subject;
Object.assign(subject.state,{arrDownloadReady:true,arrDownloadLoaded:true,arrDownloadStorageKey:'test',arrDownloadUsername:'operator',csrf:'fixture'});
@@ -42,7 +44,9 @@ function harness(respond) {
const node={dataset:{arrDataReviewItemId:itemId},querySelector:()=>input};
return {input,button:{closest:()=>node}};
};
return {...subject,calls,storage,storageReads,timers,element,row,i18n:context.window.ARRI18n,submit:()=>subject.submitARRDownload({preventDefault(){}})};
return {...subject,calls,storage,storageReads,timers,element,row,i18n:context.window.ARRI18n,document:context.document,
emitDocumentEvent(event){for(const callback of documentListeners.get(event.type)||[]) callback(event);},
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)}`;
+129 -2
View File
@@ -76,7 +76,7 @@ test('loading and failed reads never label days ungenerated and a retry can rest
resolve(response(503,null,'DATABASE_QUERY_FAILED'));
await reading;
assert.equal(h.state.reportCalendarError,true);
assert.match(block(h,`${month}-01`),/report_calendar.day_unknown/);
assert.match(block(h,`${month}-01`),/report_calendar.unknown/);
assert.doesNotMatch(h.element('#report-calendar-days').innerHTML,/is-ungenerated/);
assert.equal(h.element('#report-calendar-retry').hidden,false);
fail=false;
@@ -272,7 +272,7 @@ test('calendar labels, status legends and read failures are translated in all th
const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}});
vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context);
const catalog=context.window.ARRI18n.CATALOG;
for(const key of ['title','widget_aria','generated','not_generated','unknown','loading_day',
for(const key of ['title','widget_aria','open','loading_summary','failed_summary','generated','not_generated','unknown','loading_day',
'day_generated','day_not_generated','day_unknown','day_loading_day','loading','failed','coverage','help','locked','day_group','legend','retry']) {
const entry=catalog[`report_calendar.${key}`];
assert.equal(entry?.length,3,key);
@@ -324,3 +324,130 @@ test('same-date selections across an environment A to B to A transition reject t
assert.equal(h.state.dailyOverviewDate,date);
assert.equal(h.state.jobsMonth,month);
});
test('the compact calendar starts closed and its popup and ARR date picker are mutually exclusive',async()=>{
const h=setup(()=>page([row(16)]));
await h.loadReportCalendar();
assert.equal(h.element('#report-calendar-popup').hidden,true);
assert.equal(h.element('#report-calendar-toggle').getAttribute('aria-expanded'),'false');
assert.equal(h.element('#report-calendar-summary').textContent,'report_calendar.coverage');
const reads=h.calls.length;
h.openARRCalendar();
assert.equal(h.arrCalendar.open,true);
h.openReportCalendar();
assert.equal(h.arrCalendar.open,false);
assert.equal(h.state.reportCalendarOpen,true);
assert.equal(h.element('#report-calendar-popup').hidden,false);
assert.equal(h.element('#report-calendar-toggle').getAttribute('aria-expanded'),'true');
assert.match(block(h,`${month}-16`),/report-calendar-day-check/,'a generated day has a non-color check mark');
assert.doesNotMatch(block(h,`${month}-17`),/report-calendar-day-check|report-calendar-day-state/);
h.openARRCalendar();
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.element('#report-calendar-popup').hidden,true);
assert.equal(h.arrCalendar.open,true);
assert.equal(h.calls.length,reads,'opening either calendar is a view action');
});
test('Escape and close return focus to the trigger while outside click or keyboard focus preserve the destination',()=>{
const h=setup(()=>{throw new Error('Closing must not request data');});
h.bindReportCalendar();
h.openReportCalendar();
let prevented=false;
h.emitDocumentEvent({type:'keydown',key:'Escape',preventDefault(){prevented=true;}});
assert.equal(prevented,true);
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.document.activeElement,h.element('#report-calendar-toggle'));
h.openReportCalendar();
h.element('#report-calendar-close').dispatchEvent({type:'click'});
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.document.activeElement,h.element('#report-calendar-toggle'));
for(const type of ['click','focusin']) {
h.openReportCalendar();
const destination=h.element('#xml-file');
destination.focus();
h.emitDocumentEvent({type,target:destination});
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.document.activeElement,destination,'dismissal cannot steal focus from the next operation');
}
});
test('successful read-only selection closes the popup but busy or unsaved prices keep it open without discarding input',async()=>{
const date=`${month}-17`;
const h=setup(url=>url===`/api/daily-reports/${date}`?response(200,{business_date:date,current_job:null,latest_job:null}):page());
await h.loadReportCalendar();
h.openReportCalendar();
h.state.uploadInFlight=true;
assert.equal(await h.selectReportCalendarDate(date),false);
assert.equal(h.state.reportCalendarOpen,true);
h.state.uploadInFlight=false;
h.state.dailyPriceReview={case_status:'open',items:[{item_id:1,real_price:null}]};
const input={value:'0',closest:()=>({dataset:{dailyPriceReviewItemId:'1'}})};
h.element('#daily-price-review-body').querySelectorAll=()=>[input];
assert.equal(await h.selectReportCalendarDate(date),false);
assert.equal(h.state.reportCalendarOpen,true);
assert.equal(input.value,'0');
assert.equal(h.element('#arr-download-date').value,`${month}-16`);
h.state.dailyPriceReview=null;
assert.equal(await h.selectReportCalendarDate(date),true);
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.element('#report-calendar-popup').hidden,true);
assert.equal(h.document.activeElement,h.element('#report-calendar-toggle'));
assert.equal(h.calls.every(call=>call.method==='GET'),true);
assert.equal(h.calls.some(call=>call.url.includes('/arr-downloads')),false);
});
test('a hidden popup never captures focus during a late month refresh or keyboard-render request',async()=>{
let resolveRead;
const h=setup(()=>new Promise(resolve=>{resolveRead=resolve;}));
h.openReportCalendar();
const pending=h.loadReportCalendar();
const hiddenDay=h.element(`[data-report-calendar-day="${h.state.reportCalendarFocusDate}"]`);
hiddenDay.closest=()=>hiddenDay;
h.closeReportCalendar();
let focusAttempts=0;
hiddenDay.focus=()=>{focusAttempts+=1;};
h.document.activeElement=hiddenDay;
resolveRead(page());
await pending;
h.renderReportCalendar(true);
assert.equal(focusAttempts,0);
assert.equal(h.element('#report-calendar-popup').hidden,true);
});
test('a late day selection cannot close a popup opened again after the original popup was dismissed',async()=>{
let resolveSelection;
const date=`${month}-17`;
const h=setup(url=>url===`/api/daily-reports/${date}`?new Promise(resolve=>{resolveSelection=resolve;}):page());
await h.loadReportCalendar();
h.openReportCalendar();
const selecting=h.selectReportCalendarDate(date);
h.closeReportCalendar();
h.openReportCalendar();
resolveSelection(response(200,{business_date:date,current_job:null,latest_job:null}));
assert.equal(await selecting,true);
assert.equal(h.state.reportCalendarOpen,true);
assert.equal(h.element('#report-calendar-popup').hidden,false);
});
test('a delegated day click stays internal after synchronous rendering removes its target and restores trigger focus on success',async()=>{
let resolveSelection;
const date=`${month}-17`;
const h=setup(url=>url===`/api/daily-reports/${date}`?new Promise(resolve=>{resolveSelection=resolve;}):page());
await h.loadReportCalendar();
h.bindReportCalendar();
h.openReportCalendar();
const originalButton={dataset:{reportCalendarDay:date},closest(){return this;}};
const control=h.element('#report-calendar');
const event={type:'click',target:originalButton,composedPath:()=>[
originalButton,h.element('#report-calendar-days'),h.element('#report-calendar-popup'),control]};
h.element('#report-calendar-days').dispatchEvent(event);
assert.equal(h.state.dailyOverviewDate,date,'the delegated handler has already synchronously rerendered the days');
assert.equal(control.contains(originalButton),false,'the original click target is no longer a current DOM descendant');
h.emitDocumentEvent(event);
assert.equal(h.state.reportCalendarOpen,true,'document must honor the event origin instead of the removed target');
resolveSelection(response(200,{business_date:date,current_job:null,latest_job:null}));
await new Promise(resolve=>setImmediate(resolve));
assert.equal(h.state.reportCalendarOpen,false);
assert.equal(h.document.activeElement,h.element('#report-calendar-toggle'));
assert.equal(h.calls.every(call=>call.method==='GET'),true);
});