// 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(`]+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.day_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','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); });