pediatric-ai-scribe-v3/test/assistant-citation-modal.test.js

112 lines
5.9 KiB
JavaScript

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 ui(t) {
const dom = new JSDOM('<div id="assistant-tab">' + read('public/components/assistant.html') + '</div>', { 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 toasts = [];
const context = { window, document: window.document, console, URL, Blob, TextDecoder, AbortController,
setTimeout() {}, clearTimeout() {}, showToast: (...args) => toasts.push(args), EMPTY_PROMPT_SETS: [[]],
createAssistantImageStore: () => ({ clear() {}, renderGeneratedImage: () => '' }),
fetchSavedAssistantChats: async () => ({ success: true, chats: [] }),
saveAssistantChat: async () => ({ success: true }) };
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);
}
appBinding(t, context);
t.after(() => window.close());
return { context, document: window.document, window, toasts };
}
// The production page binds delegated clicks in bindEvents; the vm harness mirrors it.
function appBinding(t, context) {
context.bindEvents();
}
const longExcerpt = 'Synthetic excerpt. ' + 'x'.repeat(15000);
function openModal(app, n = 1) {
const cite = app.document.querySelector('.assistant-cite[data-source-number="' + n + '"]');
assert.ok(cite, 'citation chip exists');
cite.click();
return app.document.querySelector('.assistant-source-modal');
}
test('citation chips open an OWUI-style source modal with title, page and 10k excerpt preview', t => {
const app = ui(t);
const sources = [{ number: 1, title: 'Nelson Textbook of Pediatrics', page: 7, excerpt: longExcerpt }];
app.context.restoreSavedChat({ version: 2, messages: [
{ role: 'user', content: 'Question' },
{ role: 'assistant', content: 'Answer [1].', sources }
], lastAnswer: 'Answer [1].', sources });
const modal = openModal(app);
assert.ok(modal, 'modal opens');
assert.equal(modal.getAttribute('role'), 'dialog');
assert.match(modal.textContent, /Nelson Textbook of Pediatrics/);
assert.match(modal.textContent, /page 7/);
const excerpt = modal.querySelector('[data-assistant-source-excerpt]');
assert.ok(excerpt);
assert.ok(excerpt.textContent.length >= 10000 && excerpt.textContent.length <= 10200, '10k preview shown');
const showAll = modal.querySelector('[data-assistant-source-show-all]');
assert.ok(showAll, 'Show all offered for long excerpts');
showAll.click();
assert.ok(excerpt.textContent.length >= 15000, 'Show all reveals the full excerpt');
assert.equal(showAll.hidden, true, 'toggle hides once expanded');
// Sidebar switching is preserved alongside the modal.
assert.match(app.document.querySelector('#assistant-source-1').textContent, /Nelson Textbook of Pediatrics.*page 7/is);
});
test('source modal closes via button, backdrop and Escape', t => {
const app = ui(t);
const sources = [{ number: 1, title: 'Synthetic source', page: 3, excerpt: 'Short excerpt.' }];
app.context.restoreSavedChat({ version: 2, messages: [
{ role: 'user', content: 'Question' },
{ role: 'assistant', content: 'Answer [1].', sources }
], lastAnswer: 'Answer [1].', sources });
let modal = openModal(app);
assert.equal(modal.querySelector('[data-assistant-source-show-all]'), null, 'no Show all for short excerpts');
modal.querySelector('.modal-close').click();
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
modal = openModal(app);
modal.click(); // backdrop is the modal element itself
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
modal = openModal(app);
app.window.document.dispatchEvent(new app.window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
});
test('per-turn source maps select the correct source for reused citation numbers', t => {
const app = ui(t);
const first = [{ number: 1, title: 'First turn reference', page: 11, excerpt: 'First turn context.' }];
const second = [{ number: 1, title: 'Second turn reference', page: 23, excerpt: 'Second turn context.' }];
app.context.restoreSavedChat({ version: 2, messages: [
{ role: 'user', content: 'First question' },
{ role: 'assistant', content: 'First answer. [1]', sources: first },
{ role: 'user', content: 'Second question' },
{ role: 'assistant', content: 'Second answer. [1]', sources: second }
], lastAnswer: 'Second answer. [1]', sources: second });
const citations = app.document.querySelectorAll('#assistant-messages .assistant-cite');
assert.equal(citations.length, 2);
citations[0].click();
assert.match(app.document.querySelector('.assistant-source-modal').textContent, /First turn reference/);
assert.match(app.document.querySelector('.assistant-source-modal').textContent, /page 11/);
app.document.querySelector('.assistant-source-modal .modal-close').click();
citations[1].click();
const modal = app.document.querySelector('.assistant-source-modal');
assert.match(modal.textContent, /Second turn reference/);
assert.match(modal.textContent, /page 23/);
assert.doesNotMatch(modal.textContent, /First turn reference/);
});