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/case-slot-resize.js', 'utf8'); function setup(responses = [{ok: true}]) { const handlers = {}, calls = [], messages = []; let reloads = 0; const label = {textContent: ''}; const button = { dataset: {allocationId: '42', version: '3', start: '2026-10-21T06:00:00Z', end: '2026-10-21T06:30:00Z'}, closest: selector => selector === '.case-slot-resize' ? button : {querySelector: () => label}, classList: {add() {}, remove() {}}, setPointerCapture() {}, hasPointerCapture: () => true, releasePointerCapture() {}, }; vm.runInNewContext(source, { document: {getElementById: () => ({addEventListener: (type, fn) => {handlers[type] = fn;}})}, window: {confirm: () => true, showCaseFeedback: (...args) => messages.push(args), loadEmbeddedCasePlanner: async () => {reloads++;}}, fetch: async (url, options) => {calls.push({url, payload: JSON.parse(options.body)});const response = responses.shift();return {...response, json: async () => response.body || {}};}, }); const event = values => ({target: button, pointerId: 1, button: 0, clientY: 100, preventDefault() {}, stopPropagation() {}, ...values}); return {handlers, calls, messages, label, event, reloads: () => reloads}; } const settle = () => new Promise(resolve => setImmediate(resolve)); test('drag changes end time in 15 minute steps and sends current version', async () => { const s = setup(); s.handlers.pointerdown(s.event()); s.handlers.pointermove(s.event({clientY: 124})); assert.match(s.label.textContent, /60 min/); s.handlers.pointerup(s.event()); await settle(); assert.deepEqual(s.calls, [{url: '/api/v1/planner/allocations/42', payload: {version: 3, ends_at: '2026-10-21T07:00:00.000Z'}}]); assert.equal(s.reloads(), 1); }); test('cancel and an unchanged drag do not write', async () => { const s = setup(); s.handlers.pointerdown(s.event()); s.handlers.pointermove(s.event({clientY: 124})); s.handlers.pointercancel(s.event()); assert.match(s.label.textContent, /30 min/); s.handlers.pointerdown(s.event()); s.handlers.pointerup(s.event()); await settle(); assert.equal(s.calls.length, 0); }); test('keyboard clamps duration to 15 minutes', async () => { const s = setup(); s.handlers.keydown(s.event({key: 'ArrowUp'})); await settle(); assert.equal(s.calls[0].payload.ends_at, '2026-10-21T06:15:00.000Z'); }); test('overlap requires confirmation before retry; stale version never retries', async () => { const overlap = setup([{ok: false, status: 409, body: {detail: {message: 'Overlap', conflict_status: 'overlap'}}}, {ok: true}]); overlap.handlers.keydown(overlap.event({key: 'ArrowDown'})); await settle(); assert.equal(overlap.calls.length, 2); assert.equal(overlap.calls[1].payload.confirm_conflicts, true); const stale = setup([{ok: false, status: 409, body: {detail: {message: 'Ændret af en anden bruger', current: {version: 4}}}}]); stale.handlers.keydown(stale.event({key: 'ArrowDown'})); await settle(); assert.equal(stale.calls.length, 1); assert.equal(stale.messages[0][0], 'Ændret af en anden bruger'); assert.equal(stale.reloads(), 1); });