const assert = require('node:assert/strict'); const test = require('node:test'); const { createJsonResponse, loadFullApp, withBrowserEnv } = require('./support/load-browser-modules'); const { duplicateNameEmployees, shift } = require('./support/fixtures'); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); const displayValue = (element) => element.style.display || ''; // This is the broad smoke test for the browser boot path. test('full app boot renders timeline blocks without runtime render errors', async () => { const { document, errors } = await loadFullApp({ demoData: { employees: duplicateNameEmployees(), shifts: [ shift({ id: 'shift-1', employeeIdx: 0 }), shift({ id: 'shift-2', start: '2024-01-01T12:00:00', end: '2024-01-01T20:00:00', requiredSkill: 'Nurse', employeeIdx: null, }), ], }, }); assert.deepEqual(errors, []); assert.ok(document.body.querySelectorAll('.sf-rail-timeline').length >= 1); assert.ok(document.body.querySelectorAll('.sf-rail-timeline-row').length >= 1); assert.ok(document.body.querySelectorAll('.sf-rail-timeline-item').length >= 1); assert.equal(document.body.querySelectorAll('.sf-content').length, 4); assert.ok(!document.body.textContent.includes('7860')); }); test('terminal Solve is exposed by the stock header and restarts through cleanup', async () => { const calls = []; const eventSources = []; let createCount = 0; class TestEventSource { constructor(url) { this.url = url; this.readyState = 1; eventSources.push(this); calls.push(['stream', url]); } close() { this.readyState = 2; calls.push(['close', this.url]); } } TestEventSource.CLOSED = 2; await withBrowserEnv({ EventSource: TestEventSource, fetch: async (url, options = {}) => { const method = options.method || 'GET'; calls.push([method, url]); if (url === '/sf-config.json') { return createJsonResponse({ title: 'SolverForge Hospital', subtitle: 'Test shell', defaultDemoId: 'LARGE', }); } if (url === '/generated/ui-model.json') { return createJsonResponse({ constraints: [], entities: [], facts: [], views: [{ id: 'schedule', label: 'Schedule', kind: 'timeline-by-location' }], }); } if (url === '/demo-data') return createJsonResponse(['LARGE']); if (url === '/demo-data/LARGE') return createJsonResponse({ employees: [], shifts: [] }); if (url === '/jobs' && method === 'POST') { createCount += 1; return createJsonResponse({ id: `job-${createCount}` }); } if (url.startsWith('/jobs/job-1/snapshot') && method === 'GET') { return createJsonResponse({ id: 'job-1', jobId: 'job-1', snapshotRevision: 3, lifecycleState: 'COMPLETED', currentScore: '0hard/0soft', bestScore: '0hard/0soft', solution: { employees: [], shifts: [], score: '0hard/0soft' }, }); } if (url.startsWith('/jobs/job-1/analysis') && method === 'GET') { return createJsonResponse({ id: 'job-1', jobId: 'job-1', snapshotRevision: 3, lifecycleState: 'COMPLETED', analysis: { score: '0hard/0soft', constraints: [] }, }); } if (url === '/jobs/job-1' && method === 'DELETE') { return createJsonResponse(''); } throw new Error(`unexpected ${method} ${url}`); }, }, async ({ importModule, document }) => { const { bootApp } = await importModule('static/app/main.mjs'); const boot = await bootApp(globalThis); await flush(); const solveButton = boot.shell.header.sfControls.solveBtn; assert.equal(solveButton.textContent, 'Solve'); assert.equal(displayValue(solveButton), ''); solveButton.eventListeners.click[0]({ type: 'click' }); await flush(); assert.equal(createCount, 1); assert.equal(solveButton.style.display, 'none'); eventSources[0].onmessage({ data: JSON.stringify({ eventType: 'completed', jobId: 'job-1', lifecycleState: 'COMPLETED', snapshotRevision: 3, currentScore: '0hard/0soft', bestScore: '0hard/0soft', }), }); await flush(); await flush(); assert.equal(document.getElementById('sf-app').dataset.lifecycleState, 'COMPLETED'); assert.equal(displayValue(solveButton), ''); solveButton.eventListeners.click[0]({ type: 'click' }); await flush(); await flush(); assert.equal(createCount, 2); assert.deepEqual(calls.filter(([method]) => method === 'DELETE' || method === 'POST'), [ ['POST', '/jobs'], ['DELETE', '/jobs/job-1'], ['POST', '/jobs'], ]); }); });