bmc_hub/tests/js/work-desk.test.cjs

103 lines
7.3 KiB
JavaScript
Raw Normal View History

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);
});