103 lines
7.3 KiB
JavaScript
103 lines
7.3 KiB
JavaScript
|
|
const {test} = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
const vm = require('node:vm');
|
||
|
|
const fs = require('node:fs');
|
||
|
|
const source = fs.readFileSync('static/js/work-desk.js','utf8');
|
||
|
|
function extract(start,end) {const a=source.indexOf(start),b=source.indexOf(end,a);assert.ok(a>0&&b>a);return source.slice(a,b);}
|
||
|
|
|
||
|
|
test('allocated and fixed/actual time have independent calendar columns with overlap lanes',()=>{
|
||
|
|
const context={Date,workHours:[{weekday:2,start_time:'08:30:00',end_time:'16:30:00'}]};vm.createContext(context);
|
||
|
|
vm.runInContext(extract(' function calendarColumns(', ' async function events('),context);
|
||
|
|
const event=(id,start,end,column)=>({id,start:'2026-10-21T'+start+':00',end:'2026-10-21T'+end+':00',extendedProps:{deskColumn:column}});
|
||
|
|
const events=context.calendarColumns([event(1,'09:00','10:00',0),event(2,'09:30','10:30',0),event(3,'09:00','10:00',1),event(4,'11:00','12:00',0)]);
|
||
|
|
assert.equal(events[0].extendedProps.deskLeft,0);assert.equal(events[0].extendedProps.deskRight,75);
|
||
|
|
assert.equal(events[1].extendedProps.deskLeft,25);
|
||
|
|
assert.equal(events[2].extendedProps.deskLeft,50);assert.equal(events[2].extendedProps.deskRight,0);
|
||
|
|
assert.equal(events[3].extendedProps.deskLeft,0);assert.equal(events[3].extendedProps.deskRight,50);
|
||
|
|
assert.equal(context.workingWindow('2026-10-21').start,'08:30');
|
||
|
|
assert.equal(context.workingWindow('2026-10-21').end,'16:30');
|
||
|
|
assert.equal(context.workingWindow('2026-10-22').start,'07:00');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('dropping Planlæg saves at the chosen time without opening a dialog and supports undo',async()=>{
|
||
|
|
const calls=[];let undo,refreshes=0;
|
||
|
|
const context={busy:false,desk:{can_edit:true},metadata:{settings:{default_allocation_minutes:60}},uid:4,planner:'/api/v1/planner',
|
||
|
|
mutation:async(path,method,body)=>{calls.push({path,method,body:JSON.parse(JSON.stringify(body))});return{event:{id:8,version:2}};},
|
||
|
|
api:async(path,options)=>calls.push({path,...options}),clock:()=>'',status:(text,fn)=>{undo=fn;},refresh:async()=>{refreshes++;},error:e=>{throw e;},Date};
|
||
|
|
vm.createContext(context);vm.runInContext(extract(' function reservationDuration(', ' function plan('),context);
|
||
|
|
await context.dropReservation({sag_id:12,titel:'Test',remaining_work_minutes:100,missing_minutes:40},new Date('2026-10-21T08:15:00Z'));
|
||
|
|
assert.equal(calls[0].body.starts_at,'2026-10-21T08:15:00.000Z');
|
||
|
|
assert.equal(calls[0].body.ends_at,'2026-10-21T09:00:00.000Z');
|
||
|
|
assert.deepEqual(calls[0].body.user_ids,[4]);assert.equal(refreshes,1);
|
||
|
|
await undo();assert.equal(calls[1].path,'/api/v1/planner/events/8?version=2');assert.equal(calls[1].method,'DELETE');
|
||
|
|
assert.equal(context.reservationDuration({remaining_work_minutes:null}),60);
|
||
|
|
assert.equal(context.reservationDuration({remaining_work_minutes:180}),180);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('quick meeting records the chosen duration for multiple employees without a case or customer',()=>{
|
||
|
|
const context={Date};vm.createContext(context);
|
||
|
|
vm.runInContext(extract(' function meetingPayload(', ' async function meeting('),context);
|
||
|
|
const form={elements:{description:{value:' Internt møde '}},querySelectorAll:()=>[{value:'4'},{value:'7'}]};
|
||
|
|
for(const minutes of [15,30,45,60]) {
|
||
|
|
const before=Date.now(),payload=context.meetingPayload(form,'request-1',minutes,null,null);
|
||
|
|
assert.equal(payload.mode,'registered');assert.equal(payload.minutes,minutes);
|
||
|
|
assert.deepEqual(Array.from(payload.user_ids),[4,7]);
|
||
|
|
assert.equal(payload.customer_id,null);assert.equal(payload.contact_id,null);
|
||
|
|
assert.equal(payload.description,'Internt møde');assert.equal(payload.sag_id,undefined);
|
||
|
|
assert.ok(Math.abs(new Date(payload.starts_at).getTime()-(before-minutes*60000))<100);
|
||
|
|
}
|
||
|
|
const customer=context.meetingPayload(form,'request-2',30,9,12);
|
||
|
|
assert.equal(customer.customer_id,9);assert.equal(customer.contact_id,12);
|
||
|
|
assert.throws(()=>context.meetingPayload(form,'x',20,null,null));
|
||
|
|
assert.throws(()=>context.meetingPayload({...form,querySelectorAll:()=>[]},'x',30,null,null));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('only a scheduling conflict can be confirmed and retried',async()=>{
|
||
|
|
const calls=[];
|
||
|
|
let responses=[{status:409,detail:{confirm_conflicts:true},message:'Overlap'},{}];
|
||
|
|
const context={api:async(path,options)=>{calls.push(JSON.parse(options.body));const next=responses.shift();if(next.status)throw next;return next;},confirm:()=>true,JSON};
|
||
|
|
vm.createContext(context);vm.runInContext(extract(' async function mutation(', ' function error('),context);
|
||
|
|
await context.mutation('/events/5','PATCH',{version:3});
|
||
|
|
assert.deepEqual(calls,[{version:3},{version:3,confirm_conflicts:true}]);
|
||
|
|
responses=[{status:409,detail:{current:{version:4}},message:'Stale'}];calls.length=0;
|
||
|
|
await assert.rejects(context.mutation('/events/5','PATCH',{version:3}));
|
||
|
|
assert.equal(calls.length,1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('reordering retains future-day tasks and undo is versioned',async()=>{
|
||
|
|
const calls=[];let undo;
|
||
|
|
const context={desk:{version:9,can_edit:true,manual_order:false,items:[{sag_id:1},{sag_id:2},{sag_id:3,in_day:false}]},
|
||
|
|
planner:'/api/v1/planner',mutation:async(path,method,body)=>{calls.push(JSON.parse(JSON.stringify(body)));return{version:10};},
|
||
|
|
load:async()=>{},status:(message,action)=>{undo=action;}};
|
||
|
|
vm.createContext(context);vm.runInContext(extract(' async function saveOrder(', ' function eventBody('),context);
|
||
|
|
await context.reorder(2,1);
|
||
|
|
assert.deepEqual(calls[0],{ids:[2,1,3],mode:'manual',version:9});
|
||
|
|
await undo();
|
||
|
|
assert.deepEqual(calls[1],{ids:[1,2,3],mode:'recommended',version:10});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('moving shared meetings preserves all participants and undo uses new event version',async()=>{
|
||
|
|
const calls=[];let undo,reverted=false;
|
||
|
|
const booking={id:7,event_id:5,event_version:3,title:'Meeting',kind:'meeting',participants:[1,2],starts_at:'2026-10-21T08:00:00Z',ends_at:'2026-10-21T09:00:00Z'};
|
||
|
|
const context={uid:1,metadata:{editable_user_ids:[1,2]},planner:'/api/v1/planner',
|
||
|
|
mutation:async(path,method,body)=>{calls.push({path,method,body:JSON.parse(JSON.stringify(body))});return{event:{version:4}};},
|
||
|
|
status:(message,action)=>{undo=action;},refresh:async()=>{},error:()=>{},calendar:{refetchEvents(){}}};
|
||
|
|
vm.createContext(context);vm.runInContext(extract(' function eventBody(', ' function plan('),context);
|
||
|
|
await context.changeBooking({event:{extendedProps:{booking},start:new Date('2026-10-21T10:00:00Z'),end:new Date('2026-10-21T11:00:00Z')},revert:()=>{reverted=true;}});
|
||
|
|
assert.equal(reverted,false);
|
||
|
|
assert.deepEqual(calls[0].body.user_ids,[1,2]);
|
||
|
|
assert.equal(calls[0].body.version,3);
|
||
|
|
await undo();
|
||
|
|
assert.equal(calls[1].body.version,4);
|
||
|
|
assert.equal(calls[1].body.starts_at,booking.starts_at);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('locked or unauthorized booking is reverted without a write',async()=>{
|
||
|
|
let writes=0,reverted=0;
|
||
|
|
const context={uid:1,metadata:{editable_user_ids:[1]},planner:'/api/v1/planner',mutation:async()=>{writes++;}};
|
||
|
|
vm.createContext(context);vm.runInContext(extract(' function eventBody(', ' function plan('),context);
|
||
|
|
for(const booking of [{participants:[1,2]},{participants:[1],locked:true}])
|
||
|
|
await context.changeBooking({event:{extendedProps:{booking}},revert:()=>{reverted++;}});
|
||
|
|
assert.equal(writes,0);assert.equal(reverted,2);
|
||
|
|
});
|