mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The scene chrome, cockpit, Display controls, command dock, layer panels, Context rail, welcome card and Provider Settings move verbatim into src/ui/views and mount into their hosts before the page is translated and wired. Each view renders the same elements, attributes and text as its template; Vue keeps whitespace between elements as one space, and no view markup sits under a white-space: pre rule. Template asset URLs stay literal, view files are left out of Prettier so the markup is not reflowed, and the Node component loader moves to build/ and is exported as build/register-vue for host test runners. Hosts that do not initialize Core's locale pass localePicker: false instead of editing the markup.
114 lines
4.4 KiB
JavaScript
114 lines
4.4 KiB
JavaScript
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: `<span class="material-symbols-outlined">radar</span>`. */
|
|
const SPAN_TEXT =
|
|
/class="[^"]*material-symbols-outlined[^"]*"[^>]*>\s*([a-z0-9_]+)\s*</g;
|
|
/** A whole `textContent =` statement, across lines, so a multi-line ternary is read once. */
|
|
const TEXT_ASSIGNMENT = /(?:textContent|innerText)\s*=\s*([^;]{0,400})/gs;
|
|
const STRING_LITERAL = /['"`]([a-z0-9_]{2,})['"`]/g;
|
|
/** A rail or menu item's glyph: `{ icon: 'layers' }`, rendered by `UiIcon`. */
|
|
const ICON_PROPERTY = /\bicon:\s*['"]([a-z0-9_]+)['"]/g;
|
|
|
|
/**
|
|
* Files that SHIP markup. Tests assert on markup, they do not render it.
|
|
*
|
|
* The panel markup lives in Vue views under `src`, so markup there is read as
|
|
* well: a glyph named only in a view is still a glyph the font has to carry.
|
|
*/
|
|
function sourceFiles(directory = SRC_ROOT) {
|
|
const files = [];
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
const absolute = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) files.push(...sourceFiles(absolute));
|
|
else if (
|
|
entry.isFile() &&
|
|
/\.(js|mjs|html|vue)$/.test(entry.name) &&
|
|
!entry.name.endsWith('.test.mjs')
|
|
) {
|
|
files.push(absolute);
|
|
}
|
|
}
|
|
return [...files.sort(), INDEX_HTML];
|
|
}
|
|
|
|
/**
|
|
* Glyph names the sources ask the icon font for.
|
|
*
|
|
* Errs WIDE on purpose. A name the font does not carry costs nothing — Google
|
|
* ignores an unknown `icon_names` entry and still returns 200 — while a glyph
|
|
* the subset is missing breaks the interface SILENTLY: the ligature never
|
|
* forms, so the element renders the literal word `right_panel_open` instead of
|
|
* falling back to a visible box.
|
|
*
|
|
* A literal to the LEFT of a `?` is the condition being tested, not the text
|
|
* being shown, so it is dropped: `status === 'loading' ? …` must not enrol
|
|
* `loading`. Optional chaining is neutralised first — `payload?.newsStatus`
|
|
* is not a ternary, and splitting on its `?` would keep the condition.
|
|
* @returns {Map<string, string>} 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(ICON_PROPERTY)) 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',
|
|
);
|
|
});
|