// 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.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); });