const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'); process.env.TZ='Europe/Copenhagen'; const source=fs.readFileSync('static/js/planner.js','utf8'),host={innerHTML:''}; const state={meta:{users:[{id:1,name:'One'},{id:2,name:'Two'}]},data:{allocations:[],external_busy:[],absences:[ {id:1,user_id:1,kind_label:'Ferie',starts_at:'2026-10-24T22:00:00Z',ends_at:'2026-10-26T23:00:00Z'}, {id:2,user_id:2,kind_label:'Fravær',starts_at:'2026-10-26T09:00:00+01:00',ends_at:'2026-10-26T12:00:00+01:00'} ]}}; const context={Date,Math,Map,Number,String,state,$:()=>host,selectedUsers:()=>[1,2], dateKey:d=>d.toLocaleDateString('en-CA'),esc:s=>String(s),fmtMin:m=>String(m||0),pad:n=>String(n).padStart(2,'0'), cap:()=>({allocated_minutes:0,available_minutes:450,state:'green',label:'Ledig'}),integrationWarning:()=>'', document:{querySelectorAll:()=>[]},bindCalendar:()=>{}}; vm.createContext(context); vm.runInContext(source.slice(source.indexOf(' function blockStyle('),source.indexOf(' function bindCalendar(')),context); const day=new Date('2026-10-25T12:00:00+01:00'); assert.equal(context.absencesOnDay(day,1).length,1); assert.equal(context.absencesOnDay(new Date('2026-10-27T12:00:00+01:00'),1).length,0); assert.match(context.absenceHtml(state.data.absences[0],day),/height:660px/); context.renderTimeline(day,new Date('2026-10-26T12:00:00+01:00')); assert.match(host.innerHTML,/planner-absence/);assert.match(host.innerHTML,/Ferie/); assert.ok(host.innerHTML.indexOf('planner-absence-overview')One<\/strong> · Ferie/); assert.match(context.absenceHtml(state.data.absences[1],new Date('2026-10-26T12:00:00+01:00')),/top:120px;height:180px/); context.renderMonth(new Date('2026-10-01T00:00:00+02:00')); assert.equal((host.innerHTML.match(/planner-absence-month/g)||[]).length,3); assert.match(host.innerHTML,/Ferie · One/);assert.match(host.innerHTML,/Fravær · Two/); console.log('Team calendar absence: day/week/month, multiple days, partial days, exclusive end and DST OK'); // Jumping to an employee scrolls the calendar pane, keeping the outer page in place. const button={dataset:{absenceUser:'1',absenceDate:'2026-10-25'}}; let scroll=null; host.scrollTop=200;host.getBoundingClientRect=()=>({top:100});host.scrollTo=value=>{scroll=value;}; context.document={querySelectorAll:selector=>selector==='[data-absence-user]'?[button]:[],querySelector:()=>({getBoundingClientRect:()=>({top:900})})}; vm.runInContext(source.slice(source.indexOf(' function bindCalendar('),source.indexOf(' async function drop(')),context); context.bindCalendar();button.onclick();assert.equal(scroll.top,952);assert.equal(scroll.behavior,'smooth');