pediatric-ai-scribe-v3/test/learning-hub-editor.test.js
2026-05-08 16:54:48 +02:00

190 lines
12 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8');
}
const source = read('public/js/learningHub.js');
const apiModule = read('public/js/learningHub/api.js');
const aiPanelControllerModule = read('public/js/learningHub/aiPanelController.js');
const cmsControllerModule = read('public/js/learningHub/cmsController.js');
const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
const editorModule = read('public/js/learningHub/tiptapEditor.js');
const feedRendererModule = read('public/js/learningHub/feedRenderer.js');
const quizControllerModule = read('public/js/learningHub/quizController.js');
const quizRendererModule = read('public/js/learningHub/quizRenderer.js');
const sanitizeModule = read('public/js/learningHub/sanitize.js');
const slideControllerModule = read('public/js/learningHub/slideController.js');
const viewerRendererModule = read('public/js/learningHub/viewerRenderer.js');
const webdavControllerModule = read('public/js/learningHub/webdavController.js');
test('Learning Hub destroys embedded Tiptap editors before removing CMS rows', () => {
assert.match(source, /from '\.\/learningHub\/tiptapEditor\.js'/);
assert.match(editorModule, /export function destroyTpEditor\(editor\)/);
assert.match(editorModule, /export function destroyQuestionBlockEditors\(block\)/);
assert.match(editorModule, /export function clearQuestionBlocks\(container\)/);
assert.match(source, /destroyQuestionBlockEditors\(qb\); qb\.remove\(\)/);
assert.match(source, /destroyOptionEditor\(row\); row\.remove\(\)/);
assert.doesNotMatch(source, /document\.getElementById\('lh-cms-questions'\)\.innerHTML = ''/);
assert.doesNotMatch(source, /qContainer\.innerHTML = ''/);
});
test('Learning Hub sanitization helpers are isolated for renderer reuse', () => {
assert.match(source, /from '\.\/learningHub\/sanitize\.js'/);
assert.match(sanitizeModule, /export function esc\(str\)/);
assert.match(sanitizeModule, /export function sanitizeHtml\(html\)/);
assert.match(sanitizeModule, /DOMPurify\.sanitize/);
assert.doesNotMatch(sanitizeModule, /ai-generate|ai-refine|FormData|\/api\/admin\/learning/);
assert.doesNotMatch(source, /function sanitizeHtml\(html\)/);
});
test('Learning Hub API helpers centralize auth JSON fetches', () => {
assert.match(source, /from '\.\/learningHub\/api\.js'/);
assert.match(apiModule, /export function getJson\(url\)/);
assert.match(apiModule, /getAuthHeaders\(\)/);
assert.match(apiModule, /export function sendJson\(url, method, payload\)/);
assert.match(apiModule, /export function sendJsonBlob\(url, method, payload\)/);
assert.match(apiModule, /export function deleteJson\(url\)/);
});
test('Learning Hub Tiptap helpers are isolated from content generation flow', () => {
assert.match(editorModule, /export function makeTpEditor/);
assert.match(editorModule, /window\.Tiptap/);
assert.doesNotMatch(editorModule, /ai-generate|ai-refine|FormData|\/api\/admin\/learning/);
});
test('Learning Hub feed renderers are isolated from API and editor flows', () => {
assert.match(source, /from '\.\/learningHub\/feedRenderer\.js'/);
assert.match(feedRendererModule, /export function renderCategoryPills\(categories\)/);
assert.match(feedRendererModule, /export function renderFeed\(items, feedEl\)/);
assert.match(feedRendererModule, /export function renderSearchHeader\(count, query\)/);
assert.match(feedRendererModule, /export function renderEmptySearchMessage\(\)/);
assert.match(feedRendererModule, /fa-presentation-screen/);
assert.doesNotMatch(feedRendererModule, /fetch\(|getJson|sendJson|deleteJson|ai-generate|FormData|window\.Tiptap/);
assert.doesNotMatch(source, /function renderFeed\(items, feedEl\)/);
});
test('Learning Hub CMS renderers are isolated from API and editor flows', () => {
assert.match(source, /from '\.\/learningHub\/cmsRenderer\.js'/);
assert.match(cmsRendererModule, /export function renderCmsCategoryList\(categories\)/);
assert.match(cmsRendererModule, /export function renderCmsContentRow\(item\)/);
assert.match(cmsRendererModule, /export function renderQuestionBlockShell\(existingQ, qNum\)/);
assert.match(cmsRendererModule, /export function renderOptionRowShell\(opt\)/);
assert.match(cmsRendererModule, /lh-cms-cat-filter/);
assert.match(cmsRendererModule, /selectedQuestionType\(existingQ, type\)/);
assert.doesNotMatch(cmsRendererModule, /fetch\(|getJson|sendJson|deleteJson|ai-generate|FormData|window\.Tiptap/);
});
test('Learning Hub preserves AI-created category selection before save', () => {
assert.match(cmsControllerModule, /function loadCategories\(editorCategoryId, filterCategoryId\)/);
assert.match(cmsControllerModule, /var selectedEditorId = editorCategoryId !== undefined \? editorCategoryId : sel\.value/);
assert.match(cmsControllerModule, /if \(selectedEditorId && hasSelectValue\(sel, selectedEditorId\)\) sel\.value = selectedEditorId/);
assert.match(source, /cms\.loadCategories\(String\(data\.id\)\)/);
});
test('Learning Hub CMS sidebar categories filter content list', () => {
assert.match(source, /cms\.filterByCategory\(cmsCat\.dataset\.id\)/);
assert.match(cmsControllerModule, /function filterByCategory\(categoryId\)/);
assert.match(cmsControllerModule, /function setActiveCategory\(categoryId\)/);
assert.match(cmsControllerModule, /item\.classList\.toggle\('active'/);
});
test('Learning Hub refreshes CMS category counts after content changes', () => {
assert.match(source, /saveQuestions\(contentId, questions, 0, function\(\) \{[\s\S]*?cms\.refreshAll\(\);/);
assert.match(source, /showToast\('Content deleted', 'info'\);[\s\S]*?cms\.refreshAll\(\);/);
assert.match(cmsControllerModule, /function refreshAll\(\) \{[\s\S]*?loadContent\(\);[\s\S]*?loadCategories\(\);[\s\S]*?loadStats\(\);/);
});
test('Learning Hub CMS controller owns admin list operations', () => {
assert.match(source, /from '\.\/learningHub\/cmsController\.js'/);
assert.match(source, /var cms = createCmsController\(/);
assert.match(source, /cms\.loadContent\(\); cms\.loadStats\(\)/);
assert.match(source, /cms\.addCategory\(\)/);
assert.match(source, /cms\.deleteCategory\(delCat\.dataset\.id\)/);
assert.match(cmsControllerModule, /export function createCmsController\(deps\)/);
assert.match(cmsControllerModule, /function loadStats\(\)/);
assert.match(cmsControllerModule, /function loadCategories\(editorCategoryId, filterCategoryId\)/);
assert.match(cmsControllerModule, /function loadContent\(\)/);
assert.match(cmsControllerModule, /function handleFilterInput\(e\)/);
assert.doesNotMatch(cmsControllerModule, /window\.Tiptap|ai-generate|FormData/);
assert.doesNotMatch(source, /function loadCmsStats\(\)|function loadCmsCategories\(|function loadCmsContent\(\)/);
});
test('Learning Hub slide controller owns preview and PPTX flow', () => {
assert.match(source, /from '\.\/learningHub\/slideController\.js'/);
assert.match(source, /var slides = createSlideController\(/);
assert.match(source, /slides\.openSlidesFromSlug\(item\.slug\)/);
assert.match(source, /slides\.previewCurrentSlides\(\)/);
assert.match(source, /slides\.downloadPptx\(\)/);
assert.match(slideControllerModule, /export function createSlideController\(deps\)/);
assert.match(slideControllerModule, /function openSlideModal\(slides, css\) \{\s*closeSlideModal\(\);/);
assert.match(slideControllerModule, /if \(!modal \|\| !previewSlides\.length\) \{ deps\.showToast\('No slides to display', 'error'\); return; \}/);
assert.match(slideControllerModule, /modal\.removeEventListener\('touchstart', modal\._touchStart\)/);
assert.match(slideControllerModule, /modal\.removeEventListener\('touchend', modal\._touchEnd\)/);
assert.match(slideControllerModule, /sendJsonBlob\('\/api\/admin\/learning\/generate-pptx'/);
assert.doesNotMatch(slideControllerModule, /window\.Tiptap|ai-generate|FormData/);
assert.doesNotMatch(source, /function openSlideModal\(slides, css\)/);
});
test('Learning Hub quiz renderers are isolated from API and submit flow', () => {
assert.match(quizControllerModule, /from '\.\/quizRenderer\.js'/);
assert.match(quizRendererModule, /export function renderQuizQuestions\(questions\)/);
assert.match(quizRendererModule, /export function renderQuizResultExplanations\(results\)/);
assert.match(quizRendererModule, /sanitizeHtml\(q\.question_text\)/);
assert.match(quizRendererModule, /sanitizeHtml\(r\.generalExplanation\)/);
assert.doesNotMatch(quizRendererModule, /fetch\(|getJson|sendJson|deleteJson|submitQuiz|showLoading|currentContent/);
});
test('Learning Hub quiz controller owns submission and answer tracking', () => {
assert.match(source, /from '\.\/learningHub\/quizController\.js'/);
assert.match(source, /var quiz = createQuizController\(/);
assert.match(source, /quiz\.submitQuiz\(\)/);
assert.match(source, /quiz\.renderQuiz\(item\.questions\)/);
assert.match(quizControllerModule, /export function createQuizController\(deps\)/);
assert.match(quizControllerModule, /export function buildQuizAnswers\(questions, root\)/);
assert.match(quizControllerModule, /sendJson\('\/api\/learning\/submit-quiz', 'POST', \{ contentId: currentContent\.id, answers: answers \}\)/);
assert.match(quizControllerModule, /optionIds: optionIds/);
assert.match(quizControllerModule, /optionId: selected \? parseInt\(selected\.value\) : null/);
assert.doesNotMatch(source, /function submitQuiz\(\)|function showQuizResults\(|function renderQuiz\(questions\)/);
});
test('Learning Hub WebDAV controller owns browser state and rendering', () => {
assert.match(source, /from '\.\/learningHub\/webdavController\.js'/);
assert.match(source, /var webdav = createWebdavController\(/);
assert.match(source, /webdav\.browse\(webdav\.getCurrentPath\(\)\)/);
assert.match(source, /webdav\.openItem\(wdItem\)/);
assert.match(source, /var webdavPath = webdav\.getSelectedPath\(\)/);
assert.match(webdavControllerModule, /export function createWebdavController\(deps\)/);
assert.match(webdavControllerModule, /export function renderWebdavListHtml\(data\)/);
assert.match(webdavControllerModule, /export function getFileIcon\(mime, name\)/);
assert.match(webdavControllerModule, /export function formatBytes\(bytes\)/);
assert.doesNotMatch(source, /var _aiWebdav|function browseWebdav\(|function renderWebdavList\(|function selectWebdavFile\(/);
assert.doesNotMatch(webdavControllerModule, /window\.Tiptap|ai-generate|FormData/);
});
test('Learning Hub AI panel controller owns panel options and source tabs', () => {
assert.match(source, /from '\.\/learningHub\/aiPanelController\.js'/);
assert.match(source, /var aiPanel = createAiPanelController\(/);
assert.match(source, /aiPanel\.open\(\)/);
assert.match(source, /aiPanel\.close\(\)/);
assert.match(source, /aiPanel\.switchTab\(aiTab\.dataset\.aitab\)/);
assert.match(aiPanelControllerModule, /export function createAiPanelController\(deps\)/);
assert.match(aiPanelControllerModule, /export function applyAiOptionState\(type, els\)/);
assert.match(aiPanelControllerModule, /window\._buildModelOptions\(sel\)/);
assert.match(aiPanelControllerModule, /getJson\('\/api\/auth\/me'\)/);
assert.match(aiPanelControllerModule, /deps\.browseWebdav\(deps\.getWebdavPath\(\)\)/);
assert.doesNotMatch(source, /function openAiPanel\(\)|function updateAiOptions\(|function switchAiTab\(/);
assert.doesNotMatch(aiPanelControllerModule, /window\.Tiptap|ai-generate|FormData|generate-pptx/);
});
test('Learning Hub viewer renderers are isolated from API and slide loading', () => {
assert.match(source, /from '\.\/learningHub\/viewerRenderer\.js'/);
assert.match(viewerRendererModule, /export function buildViewerMeta\(item\)/);
assert.match(viewerRendererModule, /export function renderPresentationCard\(item\)/);
assert.match(viewerRendererModule, /export function renderProgressList\(progress\)/);
assert.match(viewerRendererModule, /btn-lh-view-slides/);
assert.doesNotMatch(viewerRendererModule, /fetch\(|getJson|sendJson|deleteJson|openSlidesFromSlug|showViewer/);
});