112 lines
8.5 KiB
JavaScript
112 lines
8.5 KiB
JavaScript
/* 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(/<li>/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;});
|