import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const SRC_ROOT = fileURLToPath(new URL('.', import.meta.url)); const REPO_ROOT = path.resolve(SRC_ROOT, '..'); const INDEX_HTML = path.join(REPO_ROOT, 'index.html'); /** The glyph written as element text: `radar`. */ const SPAN_TEXT = /class="[^"]*material-symbols-outlined[^"]*"[^>]*>\s*([a-z0-9_]+)\s*} glyph -> the first file that names it. */ function referencedGlyphs() { const found = new Map(); for (const file of sourceFiles()) { const source = readFileSync(file, 'utf8'); const relative = path.relative(REPO_ROOT, file).split(path.sep).join('/'); const add = (glyph) => { if (!found.has(glyph)) found.set(glyph, relative); }; for (const match of source.matchAll(SPAN_TEXT)) add(match[1]); for (const match of source.matchAll(TEXT_ASSIGNMENT)) { const statement = match[1].replaceAll('?.', '.'); const assigned = statement.includes('?') ? statement.slice(statement.indexOf('?') + 1) : statement; for (const literal of assigned.matchAll(STRING_LITERAL)) add(literal[1]); } } return found; } /** The `icon_names` list index.html asks Google for. */ function subsettedGlyphs(html = readFileSync(INDEX_HTML, 'utf8')) { const match = /Material\+Symbols\+Outlined[^"]*[?&]icon_names=([a-z0-9_,]+)/.exec(html); assert.ok( match, 'index.html must request Material Symbols with an icon_names subset', ); return new Set(match[1].split(',')); } test('every glyph the sources render is in the icon_names subset', () => { const subset = subsettedGlyphs(); const missing = [...referencedGlyphs()] .filter(([glyph]) => !subset.has(glyph)) .map(([glyph, file]) => `${glyph} (${file})`); assert.deepEqual( missing, [], 'Glyphs named by the sources but absent from the index.html icon_names list. ' + 'Add them there — an unlisted glyph renders as its own name on screen: ' + missing.join(', '), ); }); test('the unused Material Icons Round family is not loaded', () => { // A second icon font, 173 kB, for a family no source ever uses — and // src/cockpitMarkup.test.mjs already asserts the markup must not use it. const html = readFileSync(INDEX_HTML, 'utf8'); assert.doesNotMatch( html, /Material\+Icons\+Round/, 'index.html loads an icon font nothing renders', ); });