import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import postcss from 'postcss';
import {
analyzeSelectorList,
createStyleWitnesses,
createStyleLedger,
readStyleSource,
readStyleWitnesses,
selectorsWithoutWitness,
styleLeafPaths,
variableReferences,
} from './style-source.js';
const root = path.resolve(import.meta.dirname, '../..');
const stylesRoot = path.join(root, 'src/web/styles');
const indexPath = path.join(stylesRoot, 'index.css');
const index = fs.readFileSync(indexPath, 'utf8');
const expectedImports = [
'./tokens.css',
'./base.css',
'./features/devices.css',
'./features/routing.css',
'./features/instructions.css',
'./features/connection.css',
'./features/subscription.css',
'./features/servers.css',
'./primitives.css',
'./features/diagnostics.css',
'./layout.css',
'./themes.css',
];
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 856,
customProperties: 106,
declarations: 3387,
important: 0,
keyframes: 49,
media: 13,
rules: 959,
variableReferences: 829,
},
hashes: {
cascadeEdges: '0aa9651d9c84e6274ee5f64cd3151304b612aff0a06858c3d86d935996a36216',
customProperties: '06f39794d72566c2b2f8e4a2354866a54dbdceac0a1fdf9ae2619aae9abf2f36',
declarations: '09d0332d509ab6ae1d148a75ef5e931439eab7b29909529c8454382f02778fba',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: 'abdba98d777bbb19d76443af200ea2cc9018a11e5ef4ae2885ff2f5dd07ef178',
keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a',
ruleDeclarationSequences: '6e22245aef1e61fe8d56ad064d5e60d02696ec2af5446caacd66311973d5e02c',
selectors: '6487fcc63dbe251f250b79dd1430542e366c598ce93208eebfe51c344dcac38a',
variableReferences: 'f9899b21c75649b687ebd660de6e3781aade3345e1f5bac49b0f6e235f63947d',
witnesses: '87efd05d7a1fd22bc4f421573b41a115a86b2587703266079f9c913e26c323c9',
},
};
test('public stylesheet exposes exactly twelve flat semantic owners', () => {
const imports = Array.from(index.matchAll(/^@import ['"](\.\/[^'"]+\.css)['"];$/gm), ([, file]) => file);
assert.deepEqual(imports, expectedImports);
assert.equal(index, `${expectedImports.map((file) => `@import '${file}';`).join('\n')}\n`);
assert.equal(new Set(imports).size, imports.length);
assert.equal(fs.existsSync(path.join(root, 'src/web/styles.css')), false);
const actualCssFiles = fs.readdirSync(stylesRoot, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.css'))
.map((entry) => path.relative(stylesRoot, path.join(entry.parentPath, entry.name)).replaceAll('\\', '/'))
.sort();
assert.deepEqual(actualCssFiles, ['index.css', ...expectedImports.map((file) => file.slice(2))].sort());
for (const leaf of styleLeafPaths(root)) {
const source = fs.readFileSync(leaf, 'utf8');
assert.doesNotMatch(source, /@import|@layer/, path.relative(root, leaf));
}
});
test('tokens, shared primitives, and feature styles have one explicit owner', () => {
const sources = Object.fromEntries(expectedImports.map((relativePath) => [
relativePath,
fs.readFileSync(path.resolve(stylesRoot, relativePath), 'utf8'),
]));
const paletteProperties = [
'--client-bg', '--client-panel', '--client-control', '--client-border', '--client-text', '--client-muted',
'--harbor-word', '--harbor-connect', '--harbor-gateway', '--client-accent', '--client-accent-soft',
];
for (const property of paletteProperties) {
const owners = Object.entries(sources)
.filter(([, source]) => new RegExp(`^\\s*${property.replaceAll('-', '\\-')}:`, 'm').test(source))
.map(([owner]) => owner);
assert.deepEqual(owners, ['./tokens.css', './themes.css'], property);
}
for (const [owner, source] of Object.entries(sources)) {
assert.doesNotMatch(source, /\[data-theme\]/, owner);
}
for (const keyframe of [
'client-row-enter', 'client-row-leave', 'client-delete-strike',
'client-delete-content-dim', 'client-spin', 'client-copy-fade',
]) {
const owners = Object.entries(sources)
.filter(([, source]) => new RegExp(`@keyframes ${keyframe}\\b`).test(source))
.map(([owner]) => owner);
assert.deepEqual(owners, ['./primitives.css'], keyframe);
}
});
test('client typography uses the shared semantic scale outside the token owner', () => {
const tokenRoot = postcss.parse(fs.readFileSync(path.join(stylesRoot, 'tokens.css'), 'utf8'));
const tokens = new Map();
tokenRoot.walkDecls((declaration) => tokens.set(declaration.prop, declaration.value));
const roleValues = {
micro: ['0.5rem', 'var(--font-weight-body) var(--font-size-micro)/var(--line-height-micro) var(--font-family-client)', '1.45', 'var(--tracking-normal)', 'var(--text-transform-none)'],
label: ['0.5625rem', 'var(--font-weight-bold) var(--font-size-label)/var(--line-height-label) var(--font-family-client)', '1.2', 'var(--tracking-label)', 'var(--text-transform-label)'],
control: ['0.625rem', 'var(--font-weight-bold) var(--font-size-control)/var(--line-height-control) var(--font-family-client)', '1.2', 'var(--tracking-control)', 'var(--text-transform-none)'],
body: ['0.6875rem', 'var(--font-weight-body) var(--font-size-body)/var(--line-height-body) var(--font-family-client)', '1.7', 'var(--tracking-normal)', 'var(--text-transform-none)'],
data: ['0.75rem', 'var(--font-weight-strong) var(--font-size-data)/var(--line-height-data) var(--font-family-client)', '1.2', 'var(--tracking-normal)', 'var(--text-transform-none)'],
'item-title': ['0.875rem', 'var(--font-weight-bold) var(--font-size-item-title)/var(--line-height-item-title) var(--font-family-client)', '1.2', 'var(--tracking-title)', 'var(--text-transform-none)'],
'section-title': ['1rem', 'var(--font-weight-bold) var(--font-size-section-title)/var(--line-height-section-title) var(--font-family-client)', '1.25', 'var(--tracking-title)', 'var(--text-transform-none)'],
'state-title': ['1.125rem', 'var(--font-weight-bold) var(--font-size-state-title)/var(--line-height-state-title) var(--font-family-client)', '1.3', 'var(--tracking-tight)', 'var(--text-transform-none)'],
'drawer-title': ['1.375rem', 'var(--font-weight-bold) var(--font-size-drawer-title)/var(--line-height-drawer-title) var(--font-family-client)', '1.15', 'var(--tracking-tight)', 'var(--text-transform-none)'],
};
for (const [role, [size, recipe, lineHeight, tracking, transform]] of Object.entries(roleValues)) {
assert.equal(tokens.get(`--font-size-${role}`), size, role);
assert.equal(tokens.get(`--type-${role}`), recipe, role);
assert.equal(tokens.get(`--line-height-${role}`), lineHeight, role);
assert.equal(tokens.get(`--type-${role}-tracking`), tracking, role);
assert.equal(tokens.get(`--type-${role}-transform`), transform, role);
}
assert.equal(tokens.get('--type-tooltip'),
'var(--font-weight-strong) var(--font-size-control)/var(--line-height-tooltip) var(--font-family-client)');
assert.equal(tokens.get('--line-height-tooltip'), '1.35');
const allowed = new Map([
['font', new Set(['inherit', 'var(--type-icon-close)', 'var(--type-tooltip)', ...Object.keys(roleValues).map((role) => `var(--type-${role})`)])],
['font-family', new Set(['var(--font-family-client)'])],
['font-size', new Set([
'var(--font-size-brand)', 'var(--font-size-brand-tooltip)', 'var(--font-size-brand-tooltip-strong)',
'var(--font-size-icon-chevron)', 'var(--font-size-icon-delete)',
])],
['font-weight', new Set(['var(--font-weight-bold)', 'var(--font-weight-brand)', 'var(--font-weight-strong)'])],
['line-height', new Set(['var(--line-height-micro)'])],
['letter-spacing', new Set([
'inherit', 'var(--tracking-normal)', 'var(--tracking-title)',
...Object.keys(roleValues).map((role) => `var(--type-${role}-tracking)`),
'var(--type-tooltip-tracking)',
])],
['text-transform', new Set([
...Object.keys(roleValues).map((role) => `var(--type-${role}-transform)`),
'var(--type-tooltip-transform)',
])],
['font-variant-numeric', new Set(['var(--numeric-tabular)'])],
]);
for (const leaf of styleLeafPaths(root).filter((file) => !file.endsWith('/tokens.css'))) {
const stylesheet = postcss.parse(fs.readFileSync(leaf, 'utf8'), { from: leaf });
stylesheet.walkDecls((declaration) => {
if (allowed.has(declaration.prop)) {
assert.ok(allowed.get(declaration.prop).has(declaration.value),
`${path.relative(root, leaf)}:${declaration.source.start.line} ${declaration.toString()}`);
}
});
stylesheet.walkRules((rule) => {
const font = rule.nodes.find((node) => node.type === 'decl' && node.prop === 'font');
const role = /^var\(--type-(micro|label|control|tooltip|body|data|item-title|section-title|state-title|drawer-title)\)$/.exec(font?.value || '')?.[1];
if (!role) return;
const declarations = new Map(rule.nodes
.filter((node) => node.type === 'decl')
.map((declaration) => [declaration.prop, declaration.value]));
assert.equal(declarations.get('letter-spacing'), `var(--type-${role}-tracking)`, rule.selector);
assert.equal(declarations.get('text-transform'), `var(--type-${role}-transform)`, rule.selector);
});
}
const styles = postcss.parse(readStyleSource(root));
for (const selector of [
'.client-instructions-header h2', '.client-devices-header h2',
'.client-local-rules-header h2', '.client-profiles-header h2',
]) {
const owner = styles.nodes.find((node) => node.type === 'rule' && node.selector === selector);
assert.equal(owner?.nodes.find((node) => node.type === 'decl' && node.prop === 'font')?.value,
'var(--type-drawer-title)', selector);
}
for (const selector of [
'.client-duration', '.client-power-section.is-gateway .client-duration', '.client-proxy-address',
'.client-device-identity-copy span', '.client-device-traffic strong',
'.client-device-traffic-point-tooltip', '.client-device-traffic-point-tooltip time',
'.client-device-traffic-point-tooltip strong', '.client-gateway-traffic-total > strong',
'.client-gateway-traffic-speed', '.client-diagnostics-table code',
'.client-server-health', '.harbor-versions', '.harbor-version-code', '.harbor-version-number',
]) {
const owners = [];
styles.walkRules((rule) => {
if (rule.selectors.includes(selector)) owners.push(rule);
});
assert.ok(owners.length > 0, selector);
assert.ok(owners.some((owner) => owner.nodes.some((node) => node.type === 'decl'
&& node.prop === 'font-variant-numeric' && node.value === 'var(--numeric-tabular)')), selector);
for (const owner of owners.filter((rule) => rule.nodes.some((node) => node.type === 'decl' && node.prop === 'font'))) {
assert.equal(owner.nodes.find((node) => node.type === 'decl' && node.prop === 'font-variant-numeric')?.value,
'var(--numeric-tabular)', selector);
}
}
});
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 838);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
assert.deepEqual(ledger.hashes, acceptedLedger.hashes);
});
test('every live production selector has an expanded DOM witness', () => {
const unmatched = selectorsWithoutWitness(readStyleSource(root), readStyleWitnesses(root));
assert.deepEqual(unmatched, [
'.client-diagnostics-section-title button',
'.client-diagnostics-section-title button:disabled',
'.client-diagnostics-section-title button:focus-visible',
]);
});
test('JSX witness expansion follows cross-file components, render props, ReactNode slots, portals, and imperative classes', () => {
const fixtureWitnesses = createStyleWitnesses([
{
file: '/fixture/Child.tsx',
source: 'export function Child({ slot }) { return } />