// Exercise the actual UI functions with a synthetic API, without booting a browser. const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const {webcrypto} = require('node:crypto'); const source = fs.readFileSync(path.resolve(__dirname, '../../../arr_web/static/app.js'), 'utf8'); 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 element=selector => { 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, querySelector:selector=>element(selector),querySelectorAll(){return [];},scrollIntoView(){},focus(){context.document.activeElement=this;}}); } return elements.get(selector); }; const context=vm.createContext({Headers, console, Date, Intl, Uint8Array, TextEncoder, btoa, document:{hidden:false,querySelector:element}, 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'; 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'}); element('#arr-download-date').value='2026-09-16'; const row=(itemId,value='')=>{ const input={value,focus(){},closest:()=>node}; 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(){}})}; } 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)}`; module.exports={harness,response,scopedKey};