import assert from 'node:assert/strict'; import test from 'node:test'; import { readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; const allowed = new Set(['400', '500', '700']); async function sources(directory) { const output = []; for (const item of await readdir(directory, { withFileTypes: true })) { const file = path.join(directory, item.name); if (item.isDirectory()) output.push(...await sources(file)); else if (/\.(css|tsx?|html|svg)$/.test(item.name) && !/\.test\./.test(item.name)) output.push(file); } return output; } const files = (await Promise.all(['packages/ui/src', 'packages/org-chart/src', 'packages/org-chart-react/src', 'packages/center-graphic/src', 'src', '.storybook'].map(sources))).flat(); const contents = await Promise.all(files.map(async file => ({ file, text: await readFile(file, 'utf8') }))); test('HM source uses only Regular, Medium and Bold; font-face ranges are asset metadata', () => { const issues = []; for (const { file, text } of contents) { const css = text.replace(/@font-face\s*\{[^}]*\}/g, '').replace(/\/\*[\s\S]*?\*\//g, ''); for (const match of css.matchAll(/(?:font-weight\s*:\s*|fontWeight\s*:\s*)(\d+(?:\.\d+)?)/g)) { if (!allowed.has(match[1])) issues.push(`${file}: ${match[0]}`); } for (const match of css.matchAll(/\bfont-(?:thin|extralight|light|semibold|extrabold|black)\b|\bfont-\[(\d+)\]/g)) { if (!match[1] || !allowed.has(match[1])) issues.push(`${file}: ${match[0]}`); } } assert.deepEqual(issues, []); }); test('Every default HM weight token resolves to a supported value without dangling references', () => { const tokens = new Map(); for (const { file, text } of contents.filter(item => item.file.startsWith(`packages${path.sep}ui${path.sep}src`) && item.file.endsWith('.css'))) { for (const [, name, value] of text.matchAll(/(--hm-[\w-]+)\s*:\s*([^;{}]+);/g)) tokens.set(name, value.trim()); } function resolve(name, chain = []) { assert.ok(!chain.includes(name), `Circular token: ${[...chain, name].join(' -> ')}`); assert.ok(tokens.has(name), `Missing token: ${name}`); const value = tokens.get(name), reference = value.match(/^var\((--[\w-]+)\)$/); return reference ? resolve(reference[1], [...chain, name]) : value; } for (const name of tokens.keys()) if (/weight/.test(name)) assert.ok(allowed.has(resolve(name)), `${name} = ${resolve(name)}`); assert.equal(resolve('--hm-input-font-weight'), '400'); assert.equal(resolve('--hm-font-weight-medium'), '500'); assert.equal(resolve('--hm-font-weight-bold'), '700'); assert.equal(resolve('--hm-card-title-font-weight'), '700'); for (const { file, text } of contents) for (const [, name] of text.matchAll(/var\((--hm-(?!orgchart)[\w-]*weight[\w-]*)/g)) assert.ok(tokens.has(name), `Missing weight reference in ${file}: ${name}`); }); test('Theme verification baseline agrees with declared default weight tokens', async () => { const baseline = JSON.parse(await readFile('src/theme/verification-baseline.json', 'utf8')); const declarations = new Map(); for (const { file, text } of contents.filter(item => item.file.startsWith(`packages${path.sep}ui${path.sep}src${path.sep}theme${path.sep}`) && item.file.endsWith('.css'))) { for (const [, name, value] of text.matchAll(/(--hm-[\w-]*weight[\w-]*)\s*:\s*([^;{}]+);/g)) declarations.set(name, { file, value: value.trim() }); } // The verification fixture captures a subset, not every later-added token. for (const name of Object.keys(baseline).filter(name => /weight/.test(name))) assert.equal(declarations.get(name)?.value, baseline[name], name); });