import assert from 'node:assert/strict'; import { readdirSync, readFileSync } from 'node:fs'; import { test } from 'node:test'; import { ICONS_FILE } from '../scripts/icons.mjs'; import { allIconKeys, assetsSkeleton, fileFor, renderText, THEME_IDS } from '../scripts/icon-keys.js'; import { loadContext, PUBLIC_DIR } from './helpers.js'; const ctx = loadContext(); const normalise = (text) => text.replaceAll('\r\n', '\n'); test('ICONS.txt is up to date (run: npm run icons -- --write)', () => { assert.equal(normalise(readFileSync(ICONS_FILE, 'utf8')), renderText(ctx)); }); test('the README points to ICONS.txt instead of embedding the list', () => { const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); assert.ok(readme.includes('ICONS.txt')); assert.ok(!readme.includes('icons:start'), 'no generated block in the README'); assert.ok(readme.split('\n').length < 200, 'the README stays light'); }); test('ICONS.txt lists every icon, once per theme, with its file name', () => { const text = normalise(readFileSync(ICONS_FILE, 'utf8')); const keys = allIconKeys(ctx); assert.equal(new Set(keys).size, keys.length, 'icon keys are unique'); for (const theme of THEME_IDS) { const section = text.slice(text.indexOf(`THEME: ${theme}\n`)); const end = section.indexOf('\n\n\nTHEME: ', 1); const body = end < 0 ? section : section.slice(0, end); for (const key of keys) { assert.ok(body.includes(` ${key} `) && body.includes(fileFor(key)), `${theme}: ${key}`); } } }); test('every achievement shares one trophy icon', () => { assert.ok(allIconKeys(ctx).includes('trophy')); assert.ok(!allIconKeys(ctx).some((key) => key.startsWith('ach.')), 'no per-achievement icons'); const view = readFileSync(`${PUBLIC_DIR}js/ui/achievementsView.js`, 'utf8'); assert.ok(view.includes("icon('trophy')")); }); test('nothing in the list is left as a raw text key or placeholder', () => { const text = readFileSync(ICONS_FILE, 'utf8'); assert.ok(!text.includes('{@') && !/\{target\}|\{value\}/.test(text)); assert.ok(!/ (theme|research|legacy|ach|endless)\.[\w.]+\s*$/m.test(text), 'every label resolved'); assert.ok(THEME_IDS.length >= 5); }); test('the interface only uses icon keys that are listed', () => { const listed = new Set(allIconKeys(ctx).map((key) => key.split('.')[0])); const used = new Set(); const walk = (dir) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = `${dir}/${entry.name}`; if (entry.isDirectory()) walk(path); else if (entry.name.endsWith('.js') && !path.endsWith('ui/dom.js')) { // icon('trophy'), icon(`inv.${id}`) ...: the first segment of the key for (const m of readFileSync(path, 'utf8').matchAll(/\bicon\(\s*[`'"]([a-z]+)/g)) used.add(m[1]); } } }; walk(`${PUBLIC_DIR}js`); for (const m of readFileSync(`${PUBLIC_DIR}index.html`, 'utf8').matchAll(/data-icon="([a-z]+)/g)) used.add(m[1]); assert.ok(used.size >= 7, 'the scan found the icons'); const unlisted = [...used].filter((prefix) => !listed.has(prefix)); assert.deepEqual(unlisted, [], `icons used but not in ICONS.txt: ${unlisted.join(', ')}`); }); test('the generated manifest covers every key with a real file name', () => { const assets = assetsSkeleton(ctx); assert.deepEqual(Object.keys(assets), allIconKeys(ctx)); assert.equal(assets['gen.gen1'], 'img/gen-gen1.svg'); assert.equal(assets.trophy, 'img/trophy.svg'); assert.ok(Object.values(assets).every((file) => /^img\/[a-z0-9_-]+\.svg$/.test(file))); });