Merge 42dd9f4ca7 into 759652207f
This commit is contained in:
commit
58159a1bb9
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -5,6 +5,16 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- The Material Symbols icon font is now requested as a subset of the glyphs the
|
||||
app actually renders, and the unused Material Icons Round stylesheet is gone.
|
||||
Loading the app pulled 442 kB from Google Fonts over six requests, 330 kB of
|
||||
it the complete icon font served for the 28 glyphs on screen; it now pulls
|
||||
115 kB over five. `src/materialSymbolsSubset.test.mjs` fails when a source
|
||||
names a glyph the subset is missing, because an absent glyph renders as its
|
||||
own name rather than as a missing-glyph box.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mapped-site outages show their scheduled retry countdown and distinguish
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet" />
|
||||
<!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync
|
||||
with the sources: src/materialSymbolsSubset.test.mjs fails when a source
|
||||
names a glyph this list is missing, because a missing glyph does not draw
|
||||
a box — the ligature never forms and the element renders the literal word. -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,chevron_left,chevron_right,close,close_fullscreen,dark_mode,east,flare,flight,layers_clear,light_mode,local_fire_department,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,skip_next,skip_previous" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
|
||||
/** Files that SHIP markup. Tests assert on markup, they do not render it. */
|
||||
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)$/.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(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');
|
||||
});
|
||||
Loading…
Reference in New Issue