Files
harbor-net/test/web/style-boundaries.test.js
T
dokril 396c5d1917
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 6s
Improve server picker scrolling and subscription input layout
2026-08-11 08:10:15 +03:00

314 lines
15 KiB
JavaScript

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 {
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: 819,
customProperties: 31,
declarations: 2975,
important: 0,
keyframes: 56,
media: 13,
rules: 919,
variableReferences: 328,
},
hashes: {
cascadeEdges: '1a3882545e488b80ac5454cc8fa236fd242d15eb8a27ac3371876af866b6c6e8',
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
declarations: '3972e7fd7da24d872a16cfa9dad9e9355861f489708fd9643a8610422e712825',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
ruleDeclarationSequences: '99469fb4eeb3af72a12cc4365f135e988ab95ebcf194572779d62e5185778d20',
selectors: 'b2564f9e6c078233b388e84680181cbd5f21d55e963907990b237b14c5bc250f',
variableReferences: 'c8348faf26b2174f02b9760aaa5812e22e02a72da7902fb5897f0c23e7f4e939',
witnesses: '3c44c91b2cc2a03049b328b0f7a974b1c189c66d2a778dbb386601eed82994e8',
},
};
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-local-rule-enter', 'client-local-rule-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('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 730);
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 <section className="child">{slot}<h2 className="title" /></section>; }',
},
{
file: '/fixture/App.tsx',
source: 'export function App() { return <main className="scope"><Child slot={<strong className="slot" />} /></main>; }',
},
]);
const title = fixtureWitnesses.find((witness) => witness.classes.includes('title'));
const slot = fixtureWitnesses.find((witness) => witness.classes.includes('slot'));
assert.deepEqual(title?.ancestorClasses, ['child', 'scope']);
assert.deepEqual(slot?.ancestorClasses, ['child', 'scope']);
const renderPropWitnesses = createStyleWitnesses([
{
file: '/fixture/List.tsx',
source: 'export function List({ renderItem }) { return <section className="list">{renderItem()}</section>; }',
},
{
file: '/fixture/App.tsx',
source: 'export function App() { return <main className="scope"><List renderItem={() => <button className="item" />} /></main>; }',
},
]);
assert.deepEqual(
renderPropWitnesses.find((witness) => witness.classes.includes('item'))?.ancestorClasses,
['list', 'scope'],
);
const localBindingWitnesses = createStyleWitnesses([{
file: '/fixture/App.tsx',
source: `export function App() {
const content = <h2 className="local-title" />;
return <main className="local-scope"><section className="local-wrapper">{content}</section></main>;
}`,
}]);
assert.deepEqual(
localBindingWitnesses.find((witness) => witness.classes.includes('local-title'))?.ancestorClasses,
['local-scope', 'local-wrapper'],
);
assert.throws(() => createStyleWitnesses([{
file: '/fixture/App.tsx',
source: 'export function App() { return <Missing />; }',
}]), /Unresolved JSX witness component/);
assert.throws(() => createStyleWitnesses([{
file: '/fixture/App.tsx',
source: 'export function App() { const props = {}; return <Child {...props} />; } function Child() { return <div />; }',
}]), /Unsupported spread props/);
const witnesses = readStyleWitnesses(root);
const app = witnesses.find((witness) => witness.classes.includes('app') && witness.classes.includes('client-app'));
assert.ok(app);
const appThemeEdge = createStyleLedger(
'.app.client-app { --client-bg: white; } '
+ '@media (prefers-color-scheme: dark) { .app.client-app { --client-bg: black; } }',
{ witnesses },
);
assert.equal(appThemeEdge.counts.cascadeEdges, 1);
const marker = witnesses.find((witness) => witness.classes.includes('client-diagnostics-active-marker'));
assert.ok(marker?.classes.includes('is-visible'));
assert.ok(marker?.classes.includes('is-moving'));
const confirmations = witnesses.filter((witness) => witness.classes.includes('client-confirmation-popup'));
assert.ok(confirmations.some((witness) => witness.ancestorClasses.length === 0));
assert.ok(confirmations.some((witness) => witness.ancestorClasses.includes('app')));
const trafficTooltips = witnesses.filter((witness) => witness.classes.includes('client-device-traffic-point-tooltip'));
assert.ok(trafficTooltips.every((witness) => witness.ancestorClasses.length === 0));
});
test('JSX witnesses keep real multi-class collisions and exclude impossible element collisions', () => {
const witnesses = readStyleWitnesses(root);
const collision = createStyleLedger(
'.client-copy-button { color: red; } .client-instruction-copy-button { color: blue; }',
{ witnesses },
);
assert.equal(collision.counts.cascadeEdges, 1);
const impossible = createStyleLedger(
'.client-copy-button { color: red; } .client-power { color: blue; }',
{ witnesses },
);
assert.equal(impossible.counts.cascadeEdges, 0);
const conservativeSibling = createStyleLedger(
'.client-subscription-drawer .client-servers { margin-inline: auto; } '
+ '.client-server-group + .client-server-group { margin-top: 8px; }',
{ witnesses },
);
assert.equal(conservativeSibling.counts.cascadeEdges, 1);
});
test('selector proof uses the observed level-four grammar and exact specificity', () => {
const fixtures = [
['#root', [{ a: 1, b: 0, c: 0 }]],
['.client-shell:has(.harbor-brand.is-gateway-active)', [{ a: 0, b: 3, c: 0 }]],
['.client-instructions-toggle:not(.client-devices-toggle):not(.client-diagnostics-toggle)', [{ a: 0, b: 3, c: 0 }]],
[".client-power[aria-checked='true']::before", [{ a: 0, b: 2, c: 1 }]],
['.client-instruction-block:nth-child(n)', [{ a: 0, b: 2, c: 0 }]],
['.client-confirmation-actions button:hover:not(:disabled), #root', [
{ a: 0, b: 3, c: 1 },
{ a: 1, b: 0, c: 0 },
]],
];
for (const [selector, expected] of fixtures) {
assert.deepEqual(analyzeSelectorList(selector).map((entry) => entry.specificity), expected, selector);
}
assert.throws(() => analyzeSelectorList('.broken:has('), /Unsupported CSS selector grammar/);
assert.throws(() => analyzeSelectorList('& .proof'), /Unsupported CSS selector node nesting/);
assert.throws(() => analyzeSelectorList('.proof:is(.active)'), /Unsupported CSS pseudo :is/);
});
test('variable proof preserves nested and fallback references and fails closed', () => {
assert.deepEqual(variableReferences('var(--outer, color-mix(in srgb, var(--inner, red), white))'), [
{ name: '--outer', fallback: 'color-mix(in srgb, var(--inner, red), white)' },
{ name: '--inner', fallback: 'red' },
]);
assert.throws(() => variableReferences('var(color, red)'), /Invalid var\(\) name/);
assert.throws(() => variableReferences('var(--unfinished'), /Unterminated var\(\) reference/);
});
test('ledger mutations expose loss, order changes, duplicate selectors, keyframes, variables, and cascade orientation', () => {
const declaration = createStyleLedger('.proof { color: red; background: black; }');
const removed = createStyleLedger('.proof { color: red; }');
const reordered = createStyleLedger('.proof { background: black; color: red; }');
assert.notEqual(removed.hashes.declarations, declaration.hashes.declarations);
assert.equal(reordered.hashes.declarations, declaration.hashes.declarations);
assert.notEqual(reordered.hashes.ruleDeclarationSequences, declaration.hashes.ruleDeclarationSequences);
const duplicate = createStyleLedger('.proof { color: red; } .other { color: green; } .proof { color: blue; }');
const duplicateReordered = createStyleLedger('.proof { color: blue; } .other { color: green; } .proof { color: red; }');
assert.equal(duplicateReordered.hashes.declarations, duplicate.hashes.declarations);
assert.notEqual(duplicateReordered.hashes.duplicateSelectors, duplicate.hashes.duplicateSelectors);
const keyframe = createStyleLedger('@keyframes pulse { from { opacity: 0; } to { opacity: 1; } }');
const changedKeyframe = createStyleLedger('@keyframes pulse { from { opacity: 0.1; } to { opacity: 1; } }');
assert.notEqual(changedKeyframe.hashes.keyframes, keyframe.hashes.keyframes);
const variable = createStyleLedger('.proof { color: var(--outer, var(--inner, red)); }');
const changedVariable = createStyleLedger('.proof { color: var(--outer, var(--fallback, red)); }');
assert.notEqual(changedVariable.hashes.variableReferences, variable.hashes.variableReferences);
const edge = createStyleLedger('.alpha { color: red; } .beta { color: blue; }');
const reversedEdge = createStyleLedger('.beta { color: blue; } .alpha { color: red; }');
assert.equal(edge.counts.cascadeEdges, 1);
assert.equal(reversedEdge.counts.cascadeEdges, 1);
assert.notEqual(reversedEdge.hashes.cascadeEdges, edge.hashes.cascadeEdges);
const independent = createStyleLedger('.alpha { color: red; } .beta { background: blue; }');
const independentReordered = createStyleLedger('.beta { background: blue; } .alpha { color: red; }');
assert.equal(independent.counts.cascadeEdges, 0);
assert.deepEqual(independentReordered.hashes, independent.hashes);
for (const shorthand of [
'.x { place-items: center; } .y { align-items: start; }',
'.x { place-content: center; } .y { justify-content: start; }',
'.x { grid-area: 1 / 1; } .y { grid-row: 2; }',
]) {
assert.equal(createStyleLedger(shorthand).counts.cascadeEdges, 1, shorthand);
}
assert.equal(createStyleLedger('.a.c.d { color: red; } .x:not(.a.b) { color: blue; }').counts.cascadeEdges, 1);
assert.throws(() => createStyleLedger('.proof { unknown-proof-property: 1; }'), /Unsupported CSS property/);
assert.throws(() => createStyleLedger('@supports (display: grid) { .proof { display: grid; } }'), /Unsupported CSS at-rule/);
assert.throws(() => createStyleLedger('@media screen { .proof { color: red; } }'), /Unsupported media grammar/);
});
test('cascade proof dependencies are exact direct dev dependencies', () => {
const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
assert.equal(packageJson.devDependencies['@babel/parser'], '7.29.3');
assert.equal(packageJson.devDependencies.postcss, '8.5.14');
assert.equal(packageJson.devDependencies['postcss-selector-parser'], '7.1.4');
assert.equal(packageJson.devDependencies['@csstools/selector-specificity'], '6.0.0');
});
test('main owns one public stylesheet and the regrouped production CSS is deterministic', () => {
const main = fs.readFileSync(path.join(root, 'src/web/main.tsx'), 'utf8');
assert.equal((main.match(/import ['"]\.\/styles\/index\.css['"]/g) || []).length, 1);
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-kNiIGd5E.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 112296);
assert.equal(sha256(built), 'cbda70aff44215a69dc7c87ceaa4166ae573131bc53aaea7ddd935950b2c983f');
});