/* Execute the production drag handlers with isolated API/DOM adapters. */ const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const source = fs.readFileSync('static/js/planning-work.js', 'utf8'); const start = source.indexOf(' let taskDrag=null'); const end = source.indexOf(' async function tab(requested)', start); assert.ok(start > 0 && end > start); const elements = new Map(), calls = [], errors = []; function element(id) { if (!elements.has(id)) elements.set(id, { value: '', dataset: {}, handlers: {}, hidden: false, textContent: '', append() {}, setAttribute() {}, querySelectorAll() { return []; }, addEventListener(type, handler) { this.handlers[type] = handler; }, classList: { add() {}, remove() {}, toggle() {} }, }); return elements.get(id); } const root = element('root'); const queue = { version: 7, items: [{ sag_id: 14, titel: 'Allocated', case_updated_at: '2026-10-17T10:00:00Z' }] }; const context = {window:{}, console, Intl, Date, Number, JSON, Promise, root, queue, target: 1, uid: 1, unassignedRows: [{ id: 12, titel: 'Inbox', case_updated_at: '2026-10-17T09:00:00Z' }], unassignedOffset: 0, metadata: { permissions: ['cases.edit','planner.allocate'], users: [{id:1,name:'One'},{id:2,name:'Two'}], editable_user_ids: [1,2] }, byId: element, esc: s=>String(s??''), loadQueue: async()=>{}, loadUnassigned: async()=>{}, error: err=>errors.push(err.message),canModifyEvent:()=>true, calendar: { refetchEvents() {}, view: {type:'dayGridMonth'}, getDate:()=>new Date('2026-10-19T12:00:00Z') }, api: async (path,options={})=>{ calls.push({path,body:options.body?JSON.parse(options.body):null});return path.startsWith('/queue?')?{version:9}:{}; }, }; vm.createContext(context); vm.runInContext(source.slice(start,end),context); vm.runInContext(source.slice(source.indexOf(' function taskControls('),source.indexOf(' function renderQueue()')),context); function drag(id,origin,kind='reorder') { const task={dataset:{taskId:String(id),taskSource:origin},classList:{add(){}}}; root.handlers.dragstart({target:{closest:selector=>selector==='[data-task-kind]'?{dataset:{taskKind:kind}}:task},dataTransfer:{setData(){}},preventDefault(){throw Error('Unexpected rejected drag');}}); } function employeeDrop(id) { const button={dataset:{dropUser:String(id)},hasAttribute:()=>false,classList:{remove(){}}}; return element('workEmployeeDrops').handlers.drop({preventDefault(){},stopPropagation(){},target:{closest:()=>button}}); } function dateDrop(day) { return element('workCalendar').handlers.drop({preventDefault(){},stopPropagation(){},target:{closest:()=>({dataset:{date:day}})}}); } (async()=>{ const reserved=context.reservationEvent({id:3,event_id:2,kind:'work',title:'Task',starts_at:'2026-10-19T08:00:00Z',ends_at:'2026-10-19T09:00:00Z'},'timeGridWeek'); assert.equal(reserved.display,'background');assert.equal(reserved.editable,false);assert.equal(reserved.title,'Allokeret: Task'); const meeting=context.reservationEvent({id:4,kind:'meeting',title:'Meeting'},'timeGridWeek'); assert.equal(meeting.display,'auto');assert.equal(meeting.extendedProps.reservation,undefined); const list=context.reservationEvent({id:3,kind:'work',title:'Task'},'listWeek'); assert.equal(list.display,'auto');assert.equal(list.extendedProps.reservation,true); const card=context.taskControls({sag_id:14,start_date:'2026-10-19',start_time:'09:00:00',deadline:'2026-10-20T15:00:00Z',planned_slots:[{starts_at:'2026-11-16T09:00:00Z',ends_at:'2026-11-16T10:00:00Z'},{starts_at:'2026-11-17T11:00:00Z',ends_at:'2026-11-17T12:00:00Z'}]}); for(const kind of ['start','deadline','slot'])assert.ok(card.includes('data-task-kind="'+kind+'"')); assert.ok(card.includes('19.10.2026 kl. 09:00'));assert.ok(card.includes('2 tidsrum'));assert.equal((card.match(/
  • /g)||[]).length,2); drag(12,'inbox');await employeeDrop(2); assert.equal(calls.at(-1).path,'/cases/12/assignees');assert.equal(calls.at(-1).body.user_id,2); drag(14,'queue');await employeeDrop(2); assert.equal(calls.at(-1).path,'/cases/14/transfer'); assert.deepEqual(calls.at(-1).body,{source_user_id:1,target_user_id:2,source_version:7,target_version:9}); drag(12,'inbox','start');await dateDrop('2026-10-19'); assert.deepEqual(calls.at(-1).body,{updated_at:'2026-10-17T09:00:00Z',start_date:'2026-10-19'}); drag(14,'queue','deadline');await dateDrop('2026-11-02'); assert.deepEqual(calls.at(-1).body,{updated_at:'2026-10-17T10:00:00Z',deadline_date:'2026-11-02'}); const before=calls.length; root.handlers.dragend();await dateDrop('2026-11-03');assert.equal(calls.length,before); await assert.rejects(context.setTaskDate({id:14},'2026-11-03','start'),/Opgavelisten skal genindlæses/); assert.equal(calls.length,before); const slotDialogs=[];context.planTaskSlot=(task,day,clock)=>slotDialogs.push({id:task.id,day,clock}); drag(14,'queue','slot');await dateDrop('2026-11-05');await dateDrop('2026-11-06'); assert.deepEqual(slotDialogs.map(s=>s.day),['2026-11-05','2026-11-06']);assert.equal(calls.length,before); // The actual time-grid drop target can be a lane spanning the whole week, // with no dated ancestor. Screen geometry must resolve the right column. const box=(date,left,right)=>({dataset:{date},getBoundingClientRect:()=>({left,right,top:100,bottom:700,width:right-left,height:600})}); const columns=[box('2026-10-19',100,200),box('2026-10-20',200,300)]; const lane={dataset:{time:'09:30:00'},getBoundingClientRect:()=>({top:220,bottom:240})}; const host=element('workCalendar');host.querySelectorAll=selector=>selector.includes('slot-lane')?[lane]:columns; context.calendar.view.type='timeGridWeek'; const overLane={clientX:250,clientY:225,target:{closest:()=>null},preventDefault(){},stopPropagation(){}}; const hit=context.calendarTaskTarget(overLane); assert.equal(hit.day,'2026-10-20');assert.equal(hit.clock,'09:30');assert.equal(hit.cell,columns[1]); drag(14,'queue','slot');await host.handlers.drop(overLane); assert.equal(slotDialogs.at(-1).day,'2026-10-20');assert.equal(slotDialogs.at(-1).clock,'09:30'); assert.equal(context.calendarTaskTarget({...overLane,clientX:350}).day,null); context.calendar.view.type='dayGridMonth';host.querySelectorAll=()=>columns; assert.equal(context.calendarTaskTarget(overLane).day,'2026-10-20'); host.querySelectorAll=()=>[]; context.api=async()=>{throw Error('Sagen er ændret. Genindlæs.');}; drag(14,'queue','deadline');await dateDrop('2026-11-04');assert.equal(errors.at(-1),'Sagen er ændret. Genindlæs.'); assert.ok(calls.every(call=>!call.path.includes('timer')&&!call.path.includes('/events'))); // The choice dialog sends the explicit action and the exact booking versions. const snapshot=[{id:71,version:3,event_version:5}]; for(const action of ['transfer','remove','cancel']) { let saved=null; context.api=async()=>{const err=new Error('Choose');err.detail={code:'future_bookings_choice',booking_count:1,bookings:snapshot};throw err;}; context.saveWithConflict=async(path,options)=>{saved=JSON.parse(options.body);}; context.document={createElement:()=>{ const buttons=['transfer','remove','cancel'].map(action=>({dataset:{action}})); return {querySelectorAll:()=>buttons,close(){},remove(){},showModal(){buttons.find(b=>b.dataset.action===action).onclick();}}; }}; const result=await context.transferWithBookings(14,{source_user_id:1,target_user_id:2,source_version:7,target_version:9}); assert.equal(result,action!=='cancel'); if(action==='cancel')assert.equal(saved,null); else {assert.equal(saved.future_bookings_action,action);assert.deepEqual(saved.future_bookings,snapshot);assert.equal(saved.source_version,7);} } vm.runInContext(source.slice(source.indexOf(' function absenceEvent('),source.indexOf(' let visibleAbsences=')),context); context.activityTypes=[{code:'vacation',label:'Ferie'}]; const absence=context.absenceEvent({id:9,kind:'vacation',all_day:true,display_start:'2026-10-15',display_end:'2026-10-17'}); assert.equal(absence.title,'Ferie');assert.equal(absence.allDay,true);assert.equal(absence.start,'2026-10-15');assert.equal(absence.end,'2026-10-17'); assert.equal(context.absenceEvent({id:10,kind:'unavailable'}).title,'Fravær'); console.log('Planning drag UI: assignment, transfer, start, deadline, external drag and stale changes OK'); })().catch(err=>{console.error(err);process.exitCode=1;});