454 lines
24 KiB
JavaScript
454 lines
24 KiB
JavaScript
// Read-only calendar coverage and date selection through actual UI functions.
|
|
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}=require('./helpers/arr_ui_harness.cjs');
|
|
const month='2026-09';
|
|
const page=(items=[],total=items.length)=>({status:200,ok:true,json:async()=>({ok:true,data:items,pagination:{total,limit:200,offset:0}})});
|
|
const row=(day,current={})=>({business_date:`${month}-${String(day).padStart(2,'0')}`,current_job:current===null?null:{
|
|
job_id:`fixture-${day}`,status:'succeeded',is_current:true,is_retired:false,no_of_rooms:0,...current},latest_job:null});
|
|
const block=(h,date)=>h.element('#report-calendar-days').innerHTML.match(new RegExp(`<button[^>]+data-report-calendar-day="${date}"[^>]*>[\\s\\S]*?<\\/button>`))?.[0];
|
|
const priceReview=()=>({case_id:'xml-price-case',case_status:'open',revision:1,business_date:`${month}-16`,
|
|
completed_items:1,total_items:1,items:[{item_id:1,company_key:'Fixture XML',rate_code:'FIXTURE',
|
|
effective_rate_amount:0,real_price:0,affected_records:1,affected_rooms:1,affected_room_nights:1,candidate_prices:[]}],
|
|
pagination:{offset:0,limit:50,total:1}});
|
|
function setup(respond=()=>page()) {
|
|
const h=harness(respond);
|
|
h.state.reportCalendarInitialized=true;
|
|
h.state.reportCalendarMonth=month;
|
|
h.state.arrDownloadContextId='hotel-fixture';
|
|
h.state.jobsMonth='2026-10';
|
|
h.state.selectedJobId='fixture-selected';
|
|
return h;
|
|
}
|
|
|
|
test('calendar defaults to the business current month and reads the whole month independently of daily pagination',async()=>{
|
|
const h=setup((url)=>{
|
|
assert.equal(url,`/api/daily-reports?month=${h.localMonth()}&limit=200&offset=0`);
|
|
return page();
|
|
});
|
|
h.state.reportCalendarMonth='';
|
|
h.state.jobsMonth='2026-01';
|
|
h.state.jobsOffset=100;
|
|
h.renderReportCalendar();
|
|
assert.equal(h.state.reportCalendarMonth,h.localMonth());
|
|
await h.loadReportCalendar();
|
|
assert.equal(h.state.reportCalendarDataMonth,h.localMonth());
|
|
assert.equal(h.state.jobsMonth,'2026-01');
|
|
assert.equal(h.state.jobsOffset,100);
|
|
assert.equal(h.calls.every(c=>c.method==='GET'),true);
|
|
});
|
|
|
|
test('only effective current successful reports are generated, including zero-room and pending-replacement days',async()=>{
|
|
const data=[row(1),{...row(2),latest_job:{status:'needs_review',is_current:false}},row(3,null),
|
|
{...row(4,null),latest_job:{status:'succeeded',is_current:false}},row(5,{is_retired:true}),
|
|
row(6,{is_current:false}),row(7,{status:'failed'}),row(8,{status:'needs_review'}),row(9,{is_current:undefined})];
|
|
const h=setup(()=>page(data));
|
|
h.state.dailyReports=[row(20)];
|
|
await h.loadReportCalendar();
|
|
for(const number of [1,2]) assert.match(block(h,`${month}-0${number}`),/is-generated/);
|
|
for(const number of [3,4,5,6,7,8,9]) assert.match(block(h,`${month}-0${number}`),/is-ungenerated/);
|
|
assert.match(block(h,`${month}-20`),/is-ungenerated/,'the current daily-list page cannot supply calendar coverage');
|
|
assert.equal(h.element('#report-calendar-status').textContent,'report_calendar.coverage');
|
|
});
|
|
|
|
test('a complete empty month marks every actual day ungenerated and renders no other-month buttons',async()=>{
|
|
const h=setup(()=>page());
|
|
await h.changeReportCalendarMonth('2026-02');
|
|
const html=h.element('#report-calendar-days').innerHTML;
|
|
assert.equal((html.match(/data-report-calendar-day=/g)||[]).length,28);
|
|
assert.equal((html.match(/is-ungenerated/g)||[]).length,28);
|
|
assert.doesNotMatch(html,/data-report-calendar-day="2026-01|data-report-calendar-day="2026-03/);
|
|
await h.changeReportCalendarMonth('2028-02');
|
|
assert.equal((h.element('#report-calendar-days').innerHTML.match(/data-report-calendar-day=/g)||[]).length,29);
|
|
});
|
|
|
|
test('loading and failed reads never label days ungenerated and a retry can restore a confirmed snapshot',async()=>{
|
|
let resolve;
|
|
let fail=true;
|
|
const h=setup(()=>fail?new Promise(r=>{resolve=r;}):page());
|
|
const reading=h.loadReportCalendar();
|
|
assert.match(block(h,`${month}-01`),/is-unknown/);
|
|
assert.doesNotMatch(h.element('#report-calendar-days').innerHTML,/is-ungenerated/);
|
|
assert.equal(h.element('#report-calendar').getAttribute('aria-busy'),'true');
|
|
resolve(response(503,null,'DATABASE_QUERY_FAILED'));
|
|
await reading;
|
|
assert.equal(h.state.reportCalendarError,true);
|
|
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;
|
|
await h.loadReportCalendar();
|
|
assert.equal(h.state.reportCalendarError,false);
|
|
assert.match(block(h,`${month}-01`),/is-ungenerated/);
|
|
assert.equal(h.element('#report-calendar-retry').hidden,true);
|
|
});
|
|
|
|
for(const data of [{data:[row(1)],pagination:{total:2}},
|
|
{data:[row(1),row(1)],pagination:{total:2}},
|
|
{data:[{business_date:'2026-10-01',current_job:null}],pagination:{total:1}},
|
|
{data:[{business_date:'2026-09-01'}],pagination:{total:1}},
|
|
{data:{},pagination:{total:0}}]) {
|
|
test(`an incomplete or invalid month snapshot remains unknown (${JSON.stringify(data).slice(0,58)})`,async()=>{
|
|
const h=setup(()=>({status:200,ok:true,json:async()=>({ok:true,...data})}));
|
|
await h.loadReportCalendar();
|
|
assert.equal(h.reportCalendarReady(),false);
|
|
assert.equal(h.state.reportCalendarError,true);
|
|
assert.doesNotMatch(h.element('#report-calendar-days').innerHTML,/is-ungenerated|is-generated/);
|
|
});
|
|
}
|
|
|
|
test('late previous-month responses and failures cannot overwrite the newer month',async()=>{
|
|
let resolveOld;
|
|
const h=setup((url,options,calls)=>calls.length===1?new Promise(resolve=>{resolveOld=resolve;}):page());
|
|
const old=h.loadReportCalendar();
|
|
await h.changeReportCalendarMonth('2026-10');
|
|
resolveOld(response(503,null,'DATABASE_QUERY_FAILED'));
|
|
await old;
|
|
assert.equal(h.state.reportCalendarMonth,'2026-10');
|
|
assert.equal(h.state.reportCalendarDataMonth,'2026-10');
|
|
assert.equal(h.state.reportCalendarError,false);
|
|
assert.equal(h.state.reportCalendarLoading,false);
|
|
assert.equal(h.reportCalendarReady(),true);
|
|
});
|
|
|
|
test('late replies from the former environment cannot replace the new environment snapshot',async()=>{
|
|
let resolveOld;
|
|
const h=setup((url,options,calls)=>calls.length===1?new Promise(resolve=>{resolveOld=resolve;}):page([row(2)]));
|
|
const old=h.loadReportCalendar();
|
|
h.state.arrDownloadContextId='new-hotel-fixture';
|
|
await h.loadReportCalendar();
|
|
resolveOld(page([row(1)]));
|
|
await old;
|
|
assert.equal(h.state.reportCalendarContext,'new-hotel-fixture');
|
|
assert.match(block(h,`${month}-02`),/is-generated/);
|
|
assert.match(block(h,`${month}-01`),/is-ungenerated/);
|
|
});
|
|
|
|
test('selecting a calendar day only reads its overview and synchronizes the daily viewed month',async()=>{
|
|
const date=`${month}-17`;
|
|
const h=setup((url)=>{
|
|
if(url===`/api/daily-reports/${date}`) return response(200,{business_date:date,current_job:null,latest_job:null});
|
|
if(url.startsWith('/api/daily-reports?')) return page();
|
|
throw new Error('Unexpected request '+url);
|
|
});
|
|
const savedIntent={request_id:'a'.repeat(32),report_date:'2026-10-07'};
|
|
h.rememberARRIntent(savedIntent);
|
|
h.state.arrDataReviewDraftsByRequest={fixture:{room:'ROOM-DRAFT'}};
|
|
const selected=await h.selectReportCalendarDate(date);
|
|
assert.equal(selected,true);
|
|
assert.equal(h.state.dailyOverviewDate,date);
|
|
assert.equal(h.element('#arr-download-date').value,date);
|
|
assert.equal(h.state.jobsMonth,month);
|
|
assert.equal(h.state.jobsOffset,0);
|
|
assert.equal(h.element('#jobs-history-month').value,month);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,savedIntent.request_id);
|
|
assert.equal(h.state.arrDataReviewDraftsByRequest.fixture.room,'ROOM-DRAFT');
|
|
assert.equal(h.calls.every(c=>c.method==='GET'),true);
|
|
assert.equal(h.calls.some(c=>c.url.includes('/arr-downloads')),false);
|
|
assert.match(block(h,date),/aria-pressed="true"/);
|
|
});
|
|
|
|
test('running, submission and price-edit guards preserve the old selected day and human input',async()=>{
|
|
for(const flag of ['arrDownloadBusy','arrDownloadLoading','arrDataReviewLoading','arrDataReviewMutating','arrDataReviewFinalizing',
|
|
'dailyPriceReviewLoading','dailyPriceReviewMutating','uploadInFlight','dailyDeleting']) {
|
|
const h=setup(()=>{throw new Error('No request while '+flag);});
|
|
h.state[flag]=true;
|
|
const old=h.element('#arr-download-date').value;
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),false);
|
|
assert.equal(h.element('#arr-download-date').value,old);
|
|
}
|
|
const h=setup(()=>{throw new Error('No request while running or editing');});
|
|
h.state.arrDownloadTask={status:'downloading',report_date:'2026-10-07',request_id:'a'.repeat(32)};
|
|
h.renderReportCalendar();
|
|
assert.match(block(h,`${month}-17`),/disabled/);
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),false);
|
|
h.state.arrDownloadTask=null;
|
|
h.state.dailyPriceReview={case_status:'open',items:[{item_id:1,real_price:null}]};
|
|
h.element('#daily-price-review-body').querySelectorAll=()=>[{value:'0',closest:()=>({dataset:{dailyPriceReviewItemId:'1'}})}];
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),false);
|
|
assert.equal(h.element('#toast').textContent,'daily_review.save_before_switch');
|
|
assert.equal(h.calls.length,0);
|
|
});
|
|
|
|
for(const outcome of ['succeeded','needs_review','failed','rejected']) {
|
|
test(`XML upload ${outcome} restores calendar selection without an OHIP pending task`,async()=>{
|
|
let resolveUpload;
|
|
const h=setup((url,options)=>{
|
|
if(url==='/api/jobs' && options.method==='POST') return new Promise(resolve=>{resolveUpload=resolve;});
|
|
if(url.startsWith('/api/daily-reports?')) return page();
|
|
if(url==='/api/jobs/fixture-xml/trace') return response(200,{job:{job_id:'fixture-xml',status:outcome,business_date:`${month}-16`}});
|
|
if(url.startsWith('/api/jobs/fixture-xml/review?')) return response(200,priceReview());
|
|
if(url===`/api/daily-reports/${month}-17`) return response(200,{business_date:`${month}-17`,current_job:null,latest_job:null});
|
|
throw new Error('Unexpected request '+url);
|
|
});
|
|
h.state.health={processing_ready:true};
|
|
h.state.selectedFile={name:'fixture.XML',arrayBuffer:async()=>new Uint8Array([60,47,62]).buffer};
|
|
await h.loadReportCalendar();
|
|
assert.doesNotMatch(block(h,`${month}-17`),/disabled/);
|
|
const uploading=h.handleUpload();
|
|
await new Promise(resolve=>setImmediate(resolve));
|
|
assert.equal(h.state.uploadInFlight,true);
|
|
assert.match(block(h,`${month}-17`),/disabled/);
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),false);
|
|
resolveUpload(outcome==='rejected'?response(503,null,'UPLOAD_UNAVAILABLE'):
|
|
response(200,{job_id:'fixture-xml',business_date:`${month}-16`,status:outcome}));
|
|
await uploading;
|
|
assert.equal(h.state.uploadInFlight,false);
|
|
assert.equal(h.state.dailyPriceReviewLoading,false);
|
|
assert.equal(h.state.arrDownloadPendingReviews.length,0);
|
|
assert.doesNotMatch(block(h,`${month}-17`),/disabled/,'the upload finally must refresh disabled day buttons');
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),true);
|
|
assert.equal(h.calls.filter(call=>call.method!=='GET').length,1,'only the explicitly started XML upload can write');
|
|
assert.equal(h.calls.some(call=>call.url.includes('/arr-downloads')),false);
|
|
});
|
|
}
|
|
|
|
for(const action of ['finalize','cancel']) for(const outcome of action==='finalize'?['succeeded','failed','rejected']:['succeeded','rejected']) {
|
|
test(`XML price ${action} ${outcome} releases the calendar after review completion`,async()=>{
|
|
let resolveReview;
|
|
const h=setup((url,options)=>{
|
|
if(url===`/api/jobs/fixture-xml/review/${action}` && options.method==='POST') return new Promise(resolve=>{resolveReview=resolve;});
|
|
if(url.startsWith('/api/jobs/fixture-xml/review?')) return response(200,priceReview());
|
|
if(url.startsWith('/api/daily-reports?')) return page();
|
|
if(url===`/api/daily-reports/${month}-17`) return response(200,{business_date:`${month}-17`,current_job:null,latest_job:null});
|
|
throw new Error('Unexpected request '+url);
|
|
});
|
|
h.state.dailyPriceReview=priceReview();
|
|
h.state.dailyPriceReviewJobId='fixture-xml';
|
|
await h.loadReportCalendar();
|
|
assert.doesNotMatch(block(h,`${month}-17`),/disabled/);
|
|
const mutating=action==='finalize'?h.finalizeDailyPriceReview():h.confirmDailyPriceReviewCancel();
|
|
assert.equal(h.state.dailyPriceReviewMutating,true);
|
|
assert.match(block(h,`${month}-17`),/disabled/);
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),false);
|
|
resolveReview(outcome==='rejected'?response(503,null,'REVIEW_UNAVAILABLE'):response(200,{status:outcome}));
|
|
await mutating;
|
|
assert.equal(h.state.dailyPriceReviewMutating,false);
|
|
assert.equal(h.state.dailyPriceReviewLoading,false);
|
|
assert.equal(h.state.arrDownloadPendingReviews.length,0);
|
|
assert.doesNotMatch(block(h,`${month}-17`),/disabled/,'an XML review has no OHIP task refresh to re-enable the calendar');
|
|
assert.equal(await h.selectReportCalendarDate(`${month}-17`),true);
|
|
assert.equal(h.calls.filter(call=>call.method!=='GET').length,1,'calendar selection never replays a review decision');
|
|
assert.equal(h.calls.some(call=>call.url.includes('/arr-downloads')),false);
|
|
});
|
|
}
|
|
|
|
test('month browsing crosses years and keyboard focus crosses months without selecting or submitting',async()=>{
|
|
const h=setup(()=>page());
|
|
const oldSelection=h.element('#arr-download-date').value;
|
|
await h.changeReportCalendarMonth('2026-12');
|
|
await h.changeReportCalendarMonth(h.shiftMonth(h.state.reportCalendarMonth,1));
|
|
assert.equal(h.state.reportCalendarMonth,'2027-01');
|
|
await h.moveReportCalendarFocus('PageDown','2027-01-31');
|
|
assert.equal(h.state.reportCalendarMonth,'2027-02');
|
|
assert.equal(h.state.reportCalendarFocusDate,'2027-02-28');
|
|
await h.moveReportCalendarFocus('ArrowRight','2027-02-28');
|
|
assert.equal(h.state.reportCalendarMonth,'2027-03');
|
|
assert.equal(h.state.reportCalendarFocusDate,'2027-03-01');
|
|
await h.moveReportCalendarFocus('Home','2027-03-04');
|
|
assert.equal(h.state.reportCalendarFocusDate,'2027-03-01');
|
|
assert.equal(h.element('#arr-download-date').value,oldSelection);
|
|
assert.equal(h.calls.every(c=>c.method==='GET'),true);
|
|
assert.equal(h.calls.every(c=>c.url.includes('limit=200')),true);
|
|
});
|
|
|
|
test('refreshing daily results synchronizes effective calendar coverage after a generation or deletion',async()=>{
|
|
let generated=true;
|
|
const h=setup(url=>url.includes('limit=200')?page([row(16,generated?{}:null)]):page());
|
|
h.state.jobsMonth=month;
|
|
await h.loadJobs(false,false);
|
|
assert.match(block(h,`${month}-16`),/is-generated/);
|
|
generated=false;
|
|
await h.loadJobs(false,false);
|
|
assert.match(block(h,`${month}-16`),/is-ungenerated/);
|
|
assert.equal(h.calls.filter(c=>c.url.includes('limit=200')).length,2);
|
|
assert.equal(h.calls.every(c=>c.method==='GET'),true);
|
|
});
|
|
|
|
test('calendar labels, status legends and read failures are translated in all three locales',()=>{
|
|
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','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);
|
|
assert.equal(entry.every(Boolean),true,key);
|
|
assert.match(entry[2],/[\u0e00-\u0e7f]/,key);
|
|
}
|
|
});
|
|
|
|
test('a slow earlier cross-month selection cannot roll the daily list back after a newer selection completes',async()=>{
|
|
let resolveOld;
|
|
const oldDate='2026-09-16',newDate='2026-10-07';
|
|
const h=setup(url=>{
|
|
if(url===`/api/daily-reports/${oldDate}`) return new Promise(resolve=>{resolveOld=resolve;});
|
|
if(url===`/api/daily-reports/${newDate}`) return response(200,{business_date:newDate,current_job:null,latest_job:null});
|
|
throw new Error('No stale list read expected '+url);
|
|
});
|
|
const earlier=h.selectDailyReportDate(oldDate,{syncHistory:true});
|
|
assert.equal(await h.selectDailyReportDate(newDate,{syncHistory:true}),true);
|
|
resolveOld(response(200,{business_date:oldDate,current_job:null,latest_job:null}));
|
|
assert.equal(await earlier,false);
|
|
assert.equal(h.state.dailyOverviewDate,newDate);
|
|
assert.equal(h.state.jobsMonth,'2026-10');
|
|
assert.equal(h.calls.every(c=>c.method==='GET'),true);
|
|
assert.equal(h.calls.length,2);
|
|
});
|
|
|
|
test('same-date selections across an environment A to B to A transition reject the first A response',async()=>{
|
|
let resolveOld;
|
|
const date='2026-09-16';
|
|
const h=setup((url,options,calls)=>{
|
|
if(url===`/api/daily-reports/${date}`) {
|
|
if(calls.length===1) return new Promise(resolve=>{resolveOld=resolve;});
|
|
return response(200,{business_date:date,current_job:null,latest_job:null});
|
|
}
|
|
if(url.startsWith('/api/daily-reports?')) return page();
|
|
throw new Error('Unexpected request '+url);
|
|
});
|
|
h.state.jobsMonth=month;
|
|
const earlier=h.selectDailyReportDate(date,{syncHistory:true});
|
|
h.state.arrDownloadContextId='environment-B';
|
|
h.state.dailyOverview=null;
|
|
assert.equal(await h.selectDailyReportDate(date,{syncHistory:true}),true);
|
|
h.state.arrDownloadContextId='hotel-fixture';
|
|
h.state.dailyOverview=null;
|
|
assert.equal(await h.selectDailyReportDate(date,{syncHistory:true}),true);
|
|
resolveOld(response(200,{business_date:date,current_job:row(16).current_job,latest_job:null}));
|
|
assert.equal(await earlier,false);
|
|
assert.equal(h.state.dailyOverview.current_job,null);
|
|
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);
|
|
});
|