const assert = require('node:assert/strict'); const test = require('node:test'); const { withBrowserEnv } = require('./support/load-browser-modules'); const { duplicateNameEmployees, shift } = require('./support/fixtures'); // These tests focus on the hospital-specific adaptation of the shared timeline widget. test('employee view renders empty employee lanes and an explicit unassigned lane', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderEmployeeView } = await importModule('static/app/schedule/employee-view.mjs'); const container = document.createElement('div'); renderEmployeeView({ sf: window.SF, container, view: { id: 'by-employee', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [shift({ employeeIdx: null })], }, }); assert.ok(container.querySelector('.sf-rail-timeline')); assert.equal(container.querySelectorAll('.sf-rail-timeline-row').length, 3); assert.ok(container.textContent.includes('Unassigned shifts')); assert.ok(container.textContent.includes('employee-1')); assert.ok(container.textContent.includes('employee-2')); }); }); test('location view uses the canonical detailed timeline surface', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderLocationView } = await importModule('static/app/schedule/location-view.mjs'); const container = document.createElement('div'); renderLocationView({ sf: window.SF, container, view: { id: 'by-location', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [shift({ employeeIdx: 0 })], }, }); const block = container.querySelector('.sf-rail-timeline-item'); const row = container.querySelector('.sf-rail-timeline-row'); assert.ok(block); assert.ok(row); assert.equal(row.dataset.mode, 'detailed'); assert.ok(!container.textContent.includes('All shifts are assigned in the current snapshot.')); }); }); test('timeline render does not assign read-only layout geometry', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderLocationView } = await importModule('static/app/schedule/location-view.mjs'); const geometry = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(document.createElement('div')), 'clientWidth'); const container = document.createElement('div'); assert.equal(typeof geometry.get, 'function'); assert.equal(geometry.set, undefined); renderLocationView({ sf: window.SF, container, view: { id: 'by-location', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [shift({ employeeIdx: 0 })], }, }); assert.ok(container.querySelector('.sf-rail-timeline')); }); }); test('location view renders individual assignments without overview summaries', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderLocationView } = await importModule('static/app/schedule/location-view.mjs'); const container = document.createElement('div'); renderLocationView({ sf: window.SF, container, view: { id: 'by-location', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [ shift({ id: 'shift-1', employeeIdx: 0, start: '2024-01-01T08:00:00', end: '2024-01-01T12:00:00' }), shift({ id: 'shift-2', employeeIdx: 1, start: '2024-01-01T09:00:00', end: '2024-01-01T17:00:00' }), shift({ id: 'shift-3', employeeIdx: null, start: '2024-01-01T10:00:00', end: '2024-01-01T18:00:00' }), ], }, }); const detailBlocks = container.querySelectorAll('.sf-rail-timeline-item--detail'); assert.equal(detailBlocks.length, 3); assert.equal(container.querySelectorAll('.sf-rail-timeline-summary-pill').length, 0); assert.equal(Array.from(container.querySelectorAll('.sf-block-label')) .filter((node) => node.textContent.trim() === 'Alex').length, 2); assert.ok(container.textContent.includes('Unassigned')); }); }); test('timeline header drag scrolls the schedule viewport horizontally', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderLocationView } = await importModule('static/app/schedule/location-view.mjs'); const container = document.createElement('div'); renderLocationView({ sf: window.SF, container, view: { id: 'by-location', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [ shift({ id: 'shift-1', employeeIdx: 0, start: '2024-01-01T08:00:00', end: '2024-01-01T16:00:00' }), shift({ id: 'shift-2', employeeIdx: 1, start: '2024-01-20T08:00:00', end: '2024-01-20T16:00:00' }), ], }, }); const headerViewport = container.querySelector('.sf-rail-timeline-header-viewport'); const bodyViewport = container.querySelector('.sf-rail-timeline-body-viewport'); assert.ok(headerViewport); assert.ok(bodyViewport); bodyViewport.scrollLeft = 120; headerViewport.scrollLeft = 120; headerViewport.eventListeners.mousedown[0]({ button: 0, clientX: 200, preventDefault() {}, }); headerViewport.eventListeners.mousemove[0]({ clientX: 150, preventDefault() {}, }); assert.equal(bodyViewport.scrollLeft, 170); headerViewport.eventListeners.mouseup[0]({}); }); }); test('detailed timeline items use packed positioning from the shared timeline surface', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderEmployeeView } = await importModule('static/app/schedule/employee-view.mjs'); const container = document.createElement('div'); renderEmployeeView({ sf: window.SF, container, view: { id: 'by-employee', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: duplicateNameEmployees(), shifts: [shift({ employeeIdx: 0 })], }, }); const block = container.querySelector('.sf-rail-timeline-item--detail'); assert.ok(block); assert.ok(String(block.style.top || '').length > 0); assert.equal(block.style.bottom, 'auto'); }); }); test('employee view renders availability overlays from employee day preferences', async () => { await withBrowserEnv({}, async ({ document, importModule, window }) => { const { renderEmployeeView } = await importModule('static/app/schedule/employee-view.mjs'); const container = document.createElement('div'); renderEmployeeView({ sf: window.SF, container, view: { id: 'by-employee', entityPlural: 'shifts', sourcePlural: 'employees', variableField: 'employeeIdx', }, data: { employees: [ { id: 'employee-1', name: 'Alex', unavailableDates: ['2024-01-01'], undesiredDates: ['2024-01-02'], desiredDates: ['2024-01-03'] }, ], shifts: [shift({ employeeIdx: 0, start: '2024-01-01T08:00:00', end: '2024-01-03T16:00:00' })], }, }); assert.equal(container.querySelectorAll('.sf-rail-timeline-overlay').length, 3); }); });