From b9ac93ddc61bc4dbe8a8a0e5747f8f5e3913d9ec Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 9 Sep 2026 01:33:57 +0200 Subject: [PATCH] test: coverage for learning-hub page mode, chat pin/rename menu, example auto-ask --- test/assistant-message-actions.test.js | 32 +++++++++++++++++++++++++ test/assistant-workspace-layout.test.js | 9 +++++++ test/patient-takehome.test.js | 14 +++++++++++ 3 files changed, 55 insertions(+) diff --git a/test/assistant-message-actions.test.js b/test/assistant-message-actions.test.js index 13cf6e4..aefd357 100644 --- a/test/assistant-message-actions.test.js +++ b/test/assistant-message-actions.test.js @@ -23,6 +23,7 @@ function ui(t, options = {}) { var nextSaveId = 1; const context = { window, document: window.document, console, URL, Blob, TextDecoder, AbortController, crypto: webcrypto, navigator: { clipboard: { writeText: async text => { copies.push(text); } } }, + localStorage: { getItem: k => window.localStorage.getItem(k), setItem: (k,v) => window.localStorage.setItem(k,v) }, setTimeout() {}, clearTimeout() {}, showToast: (...args) => toasts.push(args), EMPTY_PROMPT_SETS: [[]], createAssistantImageStore: () => ({ clear() {}, renderGeneratedImage: () => '' }), fetchAssistantStatus: async () => ({ success: true }), @@ -111,6 +112,37 @@ test('saved-chat rows open on click; the hover trash deletes without opening', a assert.deepEqual(deleted, ['7'], 'menu delete removes the chat without opening it'); }); +test('chat menu pins a chat to the top and renames inline via the server', async t => { + const app = ui(t); + const c = app.context; + const older = { id: 2, title: 'Older chat', updated_at: '2026-09-01T00:00:00Z' }; + const newer = { id: 9, title: 'Newer chat', updated_at: '2026-09-08T00:00:00Z' }; + c.fetchSavedAssistantChats = async () => ({ success: true, chats: [older, newer] }); + const renames = []; + c.renameSavedAssistantChat = async (id, title) => { renames.push([String(id), title]); return { success: true }; }; + await c.loadSavedChats(); + const rows = () => [...app.document.querySelectorAll('[data-assistant-load-chat]')]; + assert.equal(rows()[0].getAttribute('data-assistant-load-chat'), '9', 'newest first before pinning'); + rows()[1].querySelector('[data-assistant-chat-menu]').click(); + await new Promise(r => setImmediate(r)); + const pop = app.document.querySelector('[data-assistant-chat-menu-pop]'); + pop.querySelector('[data-chat-action="pin"]').click(); + await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r)); + assert.equal(rows()[0].getAttribute('data-assistant-load-chat'), '2', 'pinned chat jumps to the top'); + assert.match(rows()[0].querySelector('.assistant-saved-chat-title').innerHTML, /fa-thumbtack/, 'pin icon shown'); + rows()[0].querySelector('[data-assistant-chat-menu]').click(); + await new Promise(r => setImmediate(r)); + const pop2 = app.document.querySelector('[data-assistant-chat-menu-pop]'); + pop2.querySelector('[data-chat-action="rename"]').click(); + await new Promise(r => setImmediate(r)); + const input = app.document.querySelector('.assistant-saved-chat-rename'); + assert.ok(input, 'inline rename input appears'); + input.value = 'Renamed chat'; + input.dispatchEvent(new app.window.Event('blur')); + await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r)); + assert.deepEqual(renames, [['2', 'Renamed chat']], 'rename PATCH sent'); +}); + test('regenerate on a non-latest answer refuses honestly without touching the chat', t => { const app = ui(t); const c = app.context; diff --git a/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js index b640bcd..098aecc 100644 --- a/test/assistant-workspace-layout.test.js +++ b/test/assistant-workspace-layout.test.js @@ -100,6 +100,15 @@ test('learning hub opens inside the assistant center column and keeps the right assert.equal(app.events.filter(e => e === 'learning').length, 2); }); +test('Learning Hub opens as a full page: saved chats, image column and sources are hidden', async t => { + const app = workspace(t); + app.document.getElementById('btn-assistant-learning-view').click(); + const layout = app.document.getElementById('assistant-layout'); + assert.ok(layout.classList.contains('learning-mode'), 'learning-mode class applied (CSS hides the rail, image column and sources — browser-verified)'); + app.document.getElementById('btn-assistant-chat-view').click(); + assert.ok(!layout.classList.contains('learning-mode'), 'back to chat restores the rail and sources'); +}); + 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' } })); diff --git a/test/patient-takehome.test.js b/test/patient-takehome.test.js index e506112..283ced9 100644 --- a/test/patient-takehome.test.js +++ b/test/patient-takehome.test.js @@ -159,6 +159,10 @@ function client(t, options = {}) { translateAssistantMessage: (message, target, provider) => apiFetch('/api/clinical-assistant/translate', { method: 'POST', headers: {}, body: JSON.stringify({ message, target, provider }) }).then(r => r.json()), getAuthHeaders: () => ({ 'X-Synthetic': '1' }), fetchSavedAssistantChat: async id => ({ success: true, chat: { id, payload: { version: 2, messages: [{ role: 'user', content: 'Old question' }] } } }), + openAssistantStream: async (payload) => { + calls.push({ url: 'openAssistantStream', payload }); + return new Response('event: done\ndata: ' + JSON.stringify({ success: true, answer: 'Answer.', sources: [] }) + '\n\n'); + }, startAssistantImageJob: async (prompt, history) => { calls.push({ url: 'image-job', prompt, history }); return { success: true, jobId: 'job-1' }; }, fetch: apiFetch }; vm.createContext(context); @@ -241,6 +245,16 @@ test('Create image dialog: describe it or pick a chat; the latest chat is one ta assert.equal(jobs[1].history[0].content, 'Old question', 'generates from the selected chat'); }); +test('tapping an example question asks it immediately (no empty sends)', async t => { + const app = client(t); + const btn = app.document.querySelector('[data-assistant-example]'); + btn.click(); + await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r)); + const asks = app.calls.filter(c => c.url === 'openAssistantStream'); + assert.equal(asks.length, 1, 'one request fired from the tap'); + assert.equal(asks[0].payload.message, btn.getAttribute('data-assistant-example'), 'the example text is the message'); +}); + test('translate picker filters languages by the local provider availability', async t => { const app = client(t); const c = app.context;