const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const { JSDOM } = require('jsdom'); const { marked } = require('marked'); const read = file => fs.readFileSync(path.join(__dirname, '..', file), 'utf8'); function workspace(t) { const dom = new JSDOM('
' + read('public/components/assistant.html') + '
', { url: 'https://example.test', runScripts: 'outside-only' }); const window = dom.window; window.eval(read('public/js/accountBoundary.js')); assert.equal(window.AccountBoundary.enter({ id: 'synthetic-rendering-owner' }, true), true); window.marked = marked; window.DOMPurify = require('dompurify')(window); window.matchMedia = () => ({ matches: true }); const fetched = []; const activated = []; const events = []; const context = { window, document: window.document, console, URL, Blob, TextDecoder, AbortController, setTimeout() {}, clearTimeout() {}, showToast() {}, EMPTY_PROMPT_SETS: [[]], createAssistantImageStore: () => ({ clear() {}, renderGeneratedImage: () => '' }), fetchAssistantStatus: async () => ({ success: true }), fetchAssistantExamples: async () => ({ success: true, examples: [] }), fetchSavedAssistantChats: async () => ({ success: true, chats: [] }), saveAssistantChat: async () => ({ success: true }), fetch: async url => { fetched.push(url); return new Response(read('public/components/learning.html')); } }; vm.createContext(context); for (const file of ['assistant/citations.js', 'assistant/sources.js', 'assistant/sharing.js', 'generatedImages.js', 'assistant/export.js', 'clinicalAssistant.js']) { vm.runInContext(read('public/js/' + file).replace(/^import[\s\S]*?from ['"][^'"]+['"];\s*/gm, '').replace(/^export /gm, ''), context); } context.bindEvents(); window.activateTab = tab => activated.push(tab); window.document.addEventListener('tabChanged', e => events.push(e.detail.tab)); t.after(() => window.close()); return { context, document: window.document, window, fetched, activated, events }; } test('assistant area is an OWUI-style three-column workspace with a slim go-back top bar', t => { const app = workspace(t); const layout = app.document.querySelector('.assistant-layout'); assert.ok(layout); const columns = [...layout.children].map(el => el.className.split(' ')[0]); assert.ok(columns.includes('assistant-history'), 'left history rail'); assert.ok(columns.includes('assistant-main'), 'center chat column'); assert.ok(columns.includes('assistant-side'), 'right image/sources column'); const left = app.document.querySelector('.assistant-history'); assert.ok(left.querySelector('#assistant-saved-chats'), 'saved chats live in the left rail'); assert.equal(left.querySelector('#btn-assistant-clear') !== null, true, 'New chat sits at the top of the rail'); assert.equal(left.firstElementChild.id, 'btn-assistant-goback', 'Go back is the first rail element (ChatGPT-style), then Create image, then New chat'); const right = app.document.querySelector('.assistant-side'); assert.ok(right.querySelector('#assistant-visual-output'), 'image controls in the right column'); assert.ok(right.querySelector('#assistant-sources'), 'sources panel in the right column'); assert.equal(right.querySelector('#assistant-saved-chats'), null, 'saved chats moved out of the right column'); const topbar = app.document.querySelector('.assistant-topbar'); assert.ok(topbar); assert.ok(app.document.querySelector('.assistant-history #btn-assistant-goback'), 'Go back lives at the top of the left rail (ChatGPT-style sidebar)'); assert.ok(app.document.querySelector('.assistant-history #btn-assistant-create-image'), 'Create image entry sits at the rail top'); const actions = app.document.querySelector('.assistant-toolbar-actions'); const buttons = [...actions.querySelectorAll('button')].map(b => b.id); assert.deepEqual(buttons, ['btn-assistant-export-pdf', 'btn-assistant-download-chat', 'btn-assistant-takehome'], 'top bar keeps Export PDF, Download transcript and Patient take home'); const examples = app.document.querySelectorAll('.assistant-empty .assistant-examples button, [data-assistant-example]'); assert.ok(examples.length >= 3, 'the generated example questions stay on the empty chat screen'); }); test('opening the assistant replaces the main menu with the saved chats; Go back restores the app', async t => { const app = workspace(t); const c = app.context; app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'assistant' } })); assert.ok(app.document.body.classList.contains('assistant-workspace'), 'fullscreen workspace class applied'); app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'encounter' } })); assert.ok(!app.document.body.classList.contains('assistant-workspace'), 'leaving the assistant restores the app chrome'); app.document.body.classList.add('assistant-workspace'); app.document.getElementById('btn-assistant-goback').click(); assert.ok(!app.document.body.classList.contains('assistant-workspace'), 'Go back removes the workspace class'); }); test('learning hub opens inside the assistant center column and keeps the right tools column visible', async t => { const app = workspace(t); const c = app.context; app.document.getElementById('btn-assistant-learning-view').click(); await new Promise(r => setImmediate(r)); assert.ok(app.fetched.includes('/components/learning.html'), 'learning component fetched once'); assert.equal(app.document.getElementById('assistant-chat-view').hidden, true, 'chat view hidden'); assert.equal(app.document.getElementById('assistant-learning-view').hidden, false, 'learning view visible'); assert.ok(app.document.querySelector('#assistant-learning-root #lh-search'), 'learning markup injected'); assert.deepEqual(app.events, ['learning'], 'learning tabChanged dispatched for learningHub init'); const right = app.document.querySelector('.assistant-side'); assert.equal(right.hidden, false, 'right tools column stays visible in the learning view'); assert.equal(app.document.getElementById('assistant-visual-output').closest('.assistant-layout') !== null, true, 'image controls remain mounted'); app.document.getElementById('btn-assistant-chat-view').click(); assert.equal(app.document.getElementById('assistant-chat-view').hidden, false); assert.equal(app.document.getElementById('assistant-learning-view').hidden, true); // Reopening reuses the cached fetch and re-dispatches init. app.document.getElementById('btn-assistant-learning-view').click(); await new Promise(r => setImmediate(r)); assert.equal(app.fetched.filter(url => url === '/components/learning.html').length, 1, 'component fetched exactly once'); assert.equal(app.events.filter(e => e === 'learning').length, 2); }); test('go back leaves the assistant workspace for the last non-assistant tab', t => { const app = workspace(t); app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'notes' } })); app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'assistant' } })); app.document.getElementById('btn-assistant-goback').click(); assert.deepEqual(app.activated, ['notes'], 'returns to the previous main-menu tab'); }); test('learning hub has no top-level menu entry and its component section is gone from index.html', () => { const indexHtml = read('public/index.html'); assert.doesNotMatch(indexHtml, /