ui: the two icon names that were only ever defaults (#652)

InfoPopup and HelpPopup default to `icon = 'info circle'` and `icon = 'help circle'`.
Neither name was in iconNameAliases, so both fell through to the fallback glyph and
logged an error -- and being defaults, they fired on every use of either helper.
Opening Settings on a device logged 43 of them.

The test that should have caught this was called "maps every icon name the app uses"
and checked a list of twenty names written by hand; these two were not on it.  It now
reads the icon defaults off the source -- 85 of them -- and names the file it found an
unmapped one in.

Scanning `name='...'` as well was tried and reverted: `name` is overwhelmingly a
form-field attribute, so it reported 27 field names as unmapped icons and failed on
correct code.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-08-08 18:02:07 -06:00 • committed by Roland
parent 7e63bbb3e2
commit 24e2dd8b9b
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 38 additions and 7 deletions

View file

@ -99,8 +99,10 @@ const iconNameAliases: Record<string, keyof typeof Tabler> = {
'hdd': 'IconDeviceSdCard',
'heart': 'IconHeart',
'heartbeat': 'IconHeartRateMonitor',
'help circle': 'IconHelpCircle',
'history': 'IconHistory',
'image': 'IconPhoto',
'info circle': 'IconInfoCircle',
'lightbulb': 'IconBulb',
'lightbulb outline': 'IconBulb',
'lightning': 'IconBolt',

View file

@ -61,15 +61,44 @@ describe('Icon', () => {
expect(container.querySelector('svg')).toBeInTheDocument();
});
it('maps every icon name the app uses', () => {
// A name that falls through renders a fallback glyph, leaving a hole in the UI.
const names = ['trash', 'warning sign', 'download', 'refresh', 'plus', 'edit', 'wifi',
'search', 'lock', 'check', 'upload', 'play', 'folder', 'filter', 'file text',
'external', 'disk', 'circle notched', 'terminal', 'eye'];
it('maps every icon name the app defaults to', () => {
/*
* A name that falls through renders a fallback glyph, leaving a hole in the UI.
*
* Read off the source rather than listed by hand. The list this replaced held
* twenty names and was titled "every icon name the app uses", and the two that
* were actually missing -- `info circle` and `help circle`, the DEFAULTS of
* InfoPopup and HelpPopup -- were not among them. Being defaults, they fired on
* every use of those two helpers: opening Settings on a real device logged 43
* console errors, which no test saw because the assertion only read its own list.
*
* Defaults only (`icon = '...'`). Scanning `name='...'` as well was tried and
* reverted: `name` is overwhelmingly a form-field attribute, so it reported 27
* field names as unmapped icons and would have failed on correct code.
*/
const SRC = path.join(__dirname, '..', '..');
const sourceFiles = (dir) => fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return entry.name === 'node_modules' ? [] : sourceFiles(full);
if (!/\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
if (/\.(test|cy)\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
return [full];
});
const unmapped = names.filter(name => !resolveIconName(name));
const defaults = new Map();
for (const file of sourceFiles(SRC)) {
const source = fs.readFileSync(file, 'utf8');
for (const match of source.matchAll(/\bicon(?:Name)?\s*=\s*(?:"([^"]+)"|'([^']+)')/g)) {
const name = (match[1] ?? match[2]).toLowerCase().trim();
if (!defaults.has(name)) defaults.set(name, path.relative(SRC, file));
}
}
expect(unmapped).toEqual([]);
// The premise: a scan that matched nothing would report nothing unmapped.
expect(defaults.size).toBeGreaterThan(50);
const unmapped = [...defaults].filter(([name]) => !resolveIconName(name));
expect(unmapped.map(([name, file]) => `${name} (${file})`)).toEqual([]);
});
it('hides decorative icons from assistive technology but names labelled ones', () => {