From ed45822cb653ec4093a0adb7a8d774fc697b59bf Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 06:33:34 +0200 Subject: [PATCH] test clinical note entrypoints --- public/index.html | 2 +- public/js/chartReview.js | 3 +- public/js/ed-encounters.js | 2 +- public/js/hospitalCourse.js | 3 +- public/js/milestones.js | 2 - public/js/sickVisit.js | 2 +- test/clinical-notes-entrypoints.test.js | 123 ++++++++++++++++++++++++ test/module-entrypoints.test.js | 1 + 8 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 test/clinical-notes-entrypoints.test.js diff --git a/public/index.html b/public/index.html index f523b08..e8afd06 100644 --- a/public/index.html +++ b/public/index.html @@ -490,7 +490,7 @@ - + diff --git a/public/js/chartReview.js b/public/js/chartReview.js index 5a18ee0..9dae56d 100644 --- a/public/js/chartReview.js +++ b/public/js/chartReview.js @@ -80,7 +80,8 @@ var entry = document.createElement('div'); entry.className = 'lab-entry'; entry.innerHTML = '' + - ''; + ''; + entry.querySelector('.remove-lab-btn').addEventListener('click', function() { entry.remove(); }); labsContainer.appendChild(entry); }); diff --git a/public/js/ed-encounters.js b/public/js/ed-encounters.js index 147e9b7..7dd1a62 100644 --- a/public/js/ed-encounters.js +++ b/public/js/ed-encounters.js @@ -275,7 +275,7 @@ if (e.results[i].isFinal) _liveTranscript += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } - if (transcriptEl) transcriptEl.innerHTML = _liveTranscript + (interim ? '' + interim + '' : ''); + if (transcriptEl) transcriptEl.innerHTML = escHtml(_liveTranscript) + (interim ? '' + escHtml(interim) + '' : ''); }; _recognition.onend = function() { if (_recording && !_paused) try { _recognition.start(); } catch(e) {} }; } diff --git a/public/js/hospitalCourse.js b/public/js/hospitalCourse.js index a6ae360..4281568 100644 --- a/public/js/hospitalCourse.js +++ b/public/js/hospitalCourse.js @@ -44,7 +44,8 @@ var entry = document.createElement('div'); entry.className = 'lab-entry'; entry.innerHTML = '' + - ''; + ''; + entry.querySelector('.remove-lab-btn').addEventListener('click', function() { entry.remove(); }); labsContainer.appendChild(entry); }); diff --git a/public/js/milestones.js b/public/js/milestones.js index 341a993..fd63bb0 100644 --- a/public/js/milestones.js +++ b/public/js/milestones.js @@ -1,4 +1,3 @@ -(function() { var _inited = false; var MILESTONES_DATA = {}; // Will be loaded from API @@ -237,4 +236,3 @@ console.log('✅ Milestones module loaded'); } // end initMilestones -})(); diff --git a/public/js/sickVisit.js b/public/js/sickVisit.js index f5dbe64..9b96e7c 100644 --- a/public/js/sickVisit.js +++ b/public/js/sickVisit.js @@ -147,7 +147,7 @@ if (e.results[i].isFinal) _sickTranscript += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } - if (transcriptEl) transcriptEl.innerHTML = _sickTranscript + (interim ? '' + interim + '' : ''); + if (transcriptEl) transcriptEl.innerHTML = esc(_sickTranscript) + (interim ? '' + esc(interim) + '' : ''); }; _sickRecognition.onend = function() { if (_sickRecording && !_sickPaused) try { _sickRecognition.start(); } catch(e) {} }; } diff --git a/test/clinical-notes-entrypoints.test.js b/test/clinical-notes-entrypoints.test.js new file mode 100644 index 0000000..1869a61 --- /dev/null +++ b/test/clinical-notes-entrypoints.test.js @@ -0,0 +1,123 @@ +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(']*\\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 \? '' \+ 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 \? '' \+ 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`); + } +}); diff --git a/test/module-entrypoints.test.js b/test/module-entrypoints.test.js index 829b834..c863b6f 100644 --- a/test/module-entrypoints.test.js +++ b/test/module-entrypoints.test.js @@ -8,6 +8,7 @@ const moduleEntrypoints = [ 'public/js/clinicalAssistant.js', 'public/js/admin.js', 'public/js/diagrams.js', + 'public/js/milestones.js', 'public/js/notes.js', 'public/js/wellVisit.js' ];