190 lines
12 KiB
JavaScript
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/);
|
|
});
|