test: coverage for learning-hub page mode, chat pin/rename menu, example auto-ask
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 2m4s

This commit is contained in:
Daniel 2026-09-09 01:33:57 +02:00
parent 050ec635d2
commit b9ac93ddc6
3 changed files with 55 additions and 0 deletions

View file

@ -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;

View file

@ -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' } }));

View file

@ -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;