import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import { teachingDiagrams, getModuleDiagrams, hasLessonDiagrams, } from '../app/diagram-catalog.ts'; const root = fileURLToPath(new URL('../', import.meta.url)); const modules = [ 'api', 'database', 'api-database', 'tasks', 'redis', 'docker', 'linux', 'troubleshooting', 'distributed', ]; const lessons = new Map(); for (const moduleSlug of modules) { const file = moduleSlug === 'api' ? 'app/lessons.json' : `public/content/${moduleSlug}.json`; const content = JSON.parse(await readFile(path.join(root, file), 'utf8')); lessons.set(moduleSlug, new Set(content.map((lesson) => lesson.id))); } const diagramIds = new Set(); for (const diagram of teachingDiagrams) { assert(!diagramIds.has(diagram.id), `Duplicate diagram: ${diagram.id}`); diagramIds.add(diagram.id); assert( lessons.has(diagram.module), `${diagram.id}: unknown module ${diagram.module}`, ); assert(diagram.lessons.length > 0, `${diagram.id}: missing lesson mapping`); assert.equal( new Set(diagram.lessons).size, diagram.lessons.length, `${diagram.id}: duplicate lesson mapping`, ); for (const lesson of diagram.lessons) { assert( lessons.get(diagram.module).has(lesson), `${diagram.id}: unknown lesson ${diagram.module}/${lesson}`, ); } const nodeIds = new Set(diagram.nodes.map((node) => node.id)); assert.equal( nodeIds.size, diagram.nodes.length, `${diagram.id}: duplicate node ID`, ); assert( diagram.steps.length >= 3 && diagram.misconception.length > 0, `${diagram.id}: missing text explanation`, ); for (const node of diagram.nodes) { assert( node.x >= 0 && node.x + 210 <= 900, `${diagram.id}/${node.id}: node outside SVG width`, ); assert( node.y >= 0 && node.y + 104 <= diagram.rows.length * 185 + 60, `${diagram.id}/${node.id}: node outside SVG height`, ); const lineCount = (text, maxUnits) => { let lines = 1; let units = 0; for (const character of text) { const width = /[\u0020-\u007e]/.test(character) ? 1 : 2; if (units + width > maxUnits) { lines++; units = 0; } units += width; } return lines; }; const bottomBaseline = 26 + lineCount(node.title, 24) * 19 + 5 + (lineCount(node.detail, 29) - 1) * 16; assert( bottomBaseline <= 98, `${diagram.id}/${node.id}: text exceeds node height`, ); } for (const edge of diagram.edges) { assert( nodeIds.has(edge.from) && nodeIds.has(edge.to), `${diagram.id}: dangling edge ${edge.from} -> ${edge.to}`, ); for (const [x, y] of [ ...(edge.via ?? []), ...(edge.labelAt ? [edge.labelAt] : []), ]) { assert( x >= 0 && x <= 900 && y >= 0 && y <= diagram.rows.length * 185 + 60, `${diagram.id}: edge outside viewBox`, ); } } } for (const moduleSlug of modules) { assert( getModuleDiagrams(moduleSlug).length >= 1, `${moduleSlug}: missing overview diagram`, ); } for (const lesson of lessons.get('distributed')) { assert( hasLessonDiagrams('distributed', lesson), `distributed/${lesson}: missing diagram`, ); } console.log( `Validated ${teachingDiagrams.length} diagrams: unique IDs, valid edges, text bounds, lesson mappings, 9 module overviews, and all distributed lessons.`, ); for (const moduleSlug of modules) { const covered = [...lessons.get(moduleSlug)].filter((lesson) => hasLessonDiagrams(moduleSlug, lesson), ).length; console.log( `${moduleSlug}: ${teachingDiagrams.filter((diagram) => diagram.module === moduleSlug).length} diagrams, ${covered}/${lessons.get(moduleSlug).size} lessons illustrated`, ); }