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/); });