Files
HMDesign/scripts/font-weight-policy.test.mjs

61 lines
3.6 KiB
JavaScript

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);
});