pediatric-ai-scribe-v3/test/clinical-notes-entrypoints.test.js
2026-05-08 07:17:29 +02:00

149 lines
7 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..');
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function scriptTagSource(indexHtml, src) {
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = indexHtml.match(new RegExp('<script\\b[^>]*\\bsrc="' + escaped + '(?:\\?v=[^"]+)?"[^>]*><\\/script>'));
return match ? match[0] : '';
}
const clinicalScripts = [
'/js/hospitalCourse.js',
'/js/chartReview.js',
'/js/soap.js',
'/js/milestones.js',
'/js/sickVisit.js',
'/js/ed-encounters.js',
'/js/encounters.js'
];
test('clinical note-generation scripts are loaded once from index', () => {
const indexHtml = read('public/index.html');
clinicalScripts.forEach((src) => {
const matches = indexHtml.match(new RegExp('src="' + src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:\\?v=[^"]+)?"', 'g')) || [];
assert.equal(matches.length, 1, `${src} should be loaded exactly once`);
});
});
test('clinical note generators are wired to their expected tabs', () => {
const expectations = {
'public/js/hospitalCourse.js': 'hospital',
'public/js/chartReview.js': 'chart',
'public/js/soap.js': 'soap',
'public/js/milestones.js': 'wellvisit',
'public/js/sickVisit.js': 'sickvisit',
'public/js/ed-encounters.js': 'ed'
};
for (const [file, tab] of Object.entries(expectations)) {
const source = read(file);
assert.match(source, new RegExp("e\\.detail\\.tab !== '" + tab + "'|e\\.detail\\.tab === '" + tab + "'"), `${file} should listen for ${tab} tab activation`);
}
});
test('saved encounter infrastructure exposes required globals', () => {
const encounters = read('public/js/encounters.js');
['wirePauseButton', 'registerEncounterLoadHandler', 'saveEncounter', 'loadSavedEncountersList'].forEach((name) => {
assert.match(encounters, new RegExp('window\\.' + name + '\\s*='), `encounters.js should expose window.${name}`);
});
assert.match(encounters, /sessionStorage\.setItem\('_savedEncId_' \+ type, data\.id\)/, 'saved encounter IDs should survive refresh within the tab');
});
test('clinical note generators preserve save/load integration points', () => {
const loadHandlers = {
'public/js/hospitalCourse.js': { type: 'hospital', note: 'hc-course-text' },
'public/js/chartReview.js': { type: 'chart', note: 'cr-review-text' },
'public/js/soap.js': { type: 'soap', note: 'soap-text' },
'public/js/sickVisit.js': { type: 'sickvisit', note: 'sick-note-text' },
'public/js/ed-encounters.js': { type: 'ed', note: 'composeFinalNoteForSave' }
};
for (const [file, expected] of Object.entries(loadHandlers)) {
const source = read(file);
assert.match(source, new RegExp("registerEncounterLoadHandler\\('" + expected.type + "'"), `${file} should register a load handler for ${expected.type}`);
assert.match(source, new RegExp(expected.note.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `${file} should reference its generated note target`);
}
const encounters = read('public/js/encounters.js');
['hospital', 'chart', 'soap', 'sickvisit', 'wellvisit', 'encounter', 'dictation'].forEach((type) => {
assert.match(encounters, new RegExp("saveFromTab\\('" + type + "'|enc_type:\\s*type"), `encounters.js should support saving ${type}`);
});
assert.match(read('public/js/sickVisit.js'), /enc_type:\s*'sickvisit'/, 'sick visit has an inline save flow');
assert.match(read('public/js/ed-encounters.js'), /enc_type:\s*'ed'/, 'ED has an inline draft/final save flow');
});
test('clinical note scripts avoid browser-native prompt and confirm UI', () => {
const files = clinicalScripts.map((src) => 'public' + src);
files.forEach((file) => {
const source = read(file);
assert.doesNotMatch(source, /\bprompt\s*\(/, `${file} should not use prompt()`);
assert.doesNotMatch(source, /\balert\s*\(/, `${file} should not use alert()`);
assert.doesNotMatch(source, /\bconfirm\s*\(/, `${file} should not use confirm()`);
});
});
test('clinical note scripts avoid inline DOM event handlers', () => {
const files = clinicalScripts.map((src) => 'public' + src);
files.forEach((file) => {
const source = read(file);
assert.doesNotMatch(source, /\son[a-z]+\s*=/i, `${file} should wire events with addEventListener instead of inline handlers`);
});
});
test('live speech transcript rendering escapes interim browser text', () => {
assert.match(
read('public/js/sickVisit.js'),
/transcriptEl\.innerHTML = esc\(_sickTranscript\) \+ \(interim \? '<span[^']*>' \+ esc\(interim\)/,
'sick visit live transcript should escape browser speech text before innerHTML'
);
assert.match(
read('public/js/ed-encounters.js'),
/transcriptEl\.innerHTML = escHtml\(_liveTranscript\) \+ \(interim \? '<span[^']*>' \+ escHtml\(interim\)/,
'ED live transcript should escape browser speech text before innerHTML'
);
});
test('clinical AI outputs are inserted as text or sanitized output helpers', () => {
const expectations = {
'public/js/hospitalCourse.js': /setOutputText\(courseText, data\.hospitalCourse\)/,
'public/js/chartReview.js': /setOutputText\(reviewText, data\.review\)/,
'public/js/soap.js': /setOutputText\(soapText, data\.soap\)/,
'public/js/sickVisit.js': /setOutputText\(noteEl, data\.note\)/,
'public/js/ed-encounters.js': /textEl\.textContent = stage\.note|t\.textContent = note/
};
for (const [file, pattern] of Object.entries(expectations)) {
assert.match(read(file), pattern, `${file} should not inject generated clinical text as raw HTML`);
}
});
test('chart review preserves pasted note coverage cues', () => {
const route = read('src/routes/chartReview.js');
const script = read('public/js/chartReview.js');
const prompts = read('src/utils/prompts.js');
assert.match(script, /function getVisitContent\(card\)/);
assert.match(script, /el\.innerText \|\| el\.textContent/);
assert.match(route, /SOURCE COVERAGE REQUIREMENT/);
assert.match(route, /Account for every numbered source note/);
assert.match(route, /keep the output short/);
assert.match(route, /guides the next\/current visit/);
assert.match(route, /SOURCE NOTE \$\{sourceIndex\} of \$\{noteCount\}/);
assert.match(prompts, /Purpose: create a short guide for the next\/current visit/);
assert.match(prompts, /avoid copying whole notes/);
assert.match(prompts, /Pending tests, labs, referrals, medication changes, or action items/);
});
test('settings scripts expose loaders without rendering account values as HTML', () => {
const nextcloud = read('public/js/nextcloud.js');
const documents = read('public/js/documents.js');
assert.match(nextcloud, /window\.loadNextcloudStatus = loadNextcloudStatus/);
assert.match(documents, /window\.loadDocuments = loadDocuments/);
assert.match(nextcloud, /function setNextcloudConnectedStatus\(url, user\)/);
assert.match(nextcloud, /strong\.textContent = url \|\| ''/);
assert.doesNotMatch(nextcloud, /nc-status'\)\.innerHTML/);
});