blob: 7636fca49df6f10571ccb51a5d1e271c2dab9921 [file]
import assert from 'node:assert/strict';
import test from 'node:test';
import {
analyzeTokenSheet,
collectClassSelectors,
hasExactConsumer,
parseLeafRules,
resolveLiveTokens,
} from './check-dead-css.mjs';
// #1980 blind spot 2: `maka-shell` was reported live because the substring
// check found it inside `maka-shell-astryx`. Class names extend across
// hyphens, so a prefix hit in either direction is not a consumer.
test('a longer hyphenated class name is not a consumer of its prefix', () => {
assert.equal(hasExactConsumer('className="maka-shell-astryx"', 'maka-shell'), false);
assert.equal(hasExactConsumer('className="maka-shell"', 'maka-shell-astryx'), false);
});
test('exact matches count as consumers regardless of surrounding syntax', () => {
assert.equal(hasExactConsumer('className="maka-shell chat"', 'maka-shell'), true);
assert.equal(hasExactConsumer("classes.push('maka-shell')", 'maka-shell'), true);
assert.equal(hasExactConsumer('<div class="a maka-shell">', 'maka-shell'), true);
});
test('class selectors are collected from compact and nested rules', () => {
const css = `
.maka-shell { display: grid; }
@media (min-width: 600px) { .maka-sidebar:hover, .maka-titlebar strong { color: red; } }
/* .maka-commented { } */
`;
assert.deepEqual([...collectClassSelectors(css)].sort(), [
'maka-shell',
'maka-sidebar',
'maka-titlebar',
]);
});
test('parseLeafRules flattens at-rule wrappers to selector/body pairs', () => {
const rules = parseLeafRules(
'@layer components { .a { color: red; } @media (x) { .b { color: blue; } } }',
);
assert.deepEqual(
rules.map((rule) => rule.selector),
['.a', '.b'],
);
});
// #1980 blind spot 3: --w-rail / --w-sidebar were reported live because dead
// shell rules inside the token sheet itself read them.
test('a token read only by a dead rule in the token sheet is dead', () => {
const sheet = `
:root { --w-rail: 240px; }
.maka-shell { grid-template-columns: var(--w-rail) 1fr; }
`;
const analysis = analyzeTokenSheet(sheet, () => false);
assert.equal(resolveLiveTokens(new Set(), analysis).has('--w-rail'), false);
});
test('the same read counts once the rule class has a consumer', () => {
const sheet = `
:root { --w-rail: 240px; }
.maka-shell { grid-template-columns: var(--w-rail) 1fr; }
`;
const analysis = analyzeTokenSheet(sheet, (cls) => cls === 'maka-shell');
assert.equal(resolveLiveTokens(new Set(), analysis).has('--w-rail'), true);
});
// A comma group applies when any branch matches, so one live branch must
// keep the whole rule's reads alive — and all-dead branches must not.
test('a comma group with one live branch keeps its token reads', () => {
const sheet = '.maka-dead, .maka-live { width: var(--w-shared); }';
const live = (isLive) =>
resolveLiveTokens(new Set(), analyzeTokenSheet(sheet, isLive)).has('--w-shared');
assert.equal(
live((cls) => cls === 'maka-live'),
true,
);
assert.equal(
live(() => false),
false,
);
});
test('token-to-token derivation keeps a base token alive only via a live head', () => {
const sheet = ':root { --derived: var(--base); --base: 4px; }';
const analysis = analyzeTokenSheet(sheet, () => false);
assert.equal(resolveLiveTokens(new Set(['--derived']), analysis).has('--base'), true);
assert.equal(resolveLiveTokens(new Set(), analysis).has('--base'), false);
});
test('derivation chains resolve transitively', () => {
const sheet = ':root { --a: var(--b); --b: var(--c); --c: 1px; }';
const analysis = analyzeTokenSheet(sheet, () => false);
const live = resolveLiveTokens(new Set(['--a']), analysis);
assert.equal(live.has('--c'), true);
});
test('a self-referencing token does not loop and does not revive itself', () => {
const sheet = ':root { --a: var(--a, 4px); }';
const analysis = analyzeTokenSheet(sheet, () => false);
assert.equal(resolveLiveTokens(new Set(), analysis).has('--a'), false);
assert.equal(resolveLiveTokens(new Set(['--a']), analysis).has('--a'), true);
});
test('a derivation cycle terminates and lives or dies as one unit', () => {
const sheet = ':root { --a: var(--b); --b: var(--a); }';
const analysis = analyzeTokenSheet(sheet, () => false);
const dead = resolveLiveTokens(new Set(), analysis);
assert.equal(dead.has('--a'), false);
assert.equal(dead.has('--b'), false);
const live = resolveLiveTokens(new Set(['--a']), analysis);
assert.equal(live.has('--a'), true);
assert.equal(live.has('--b'), true);
});
test('a diamond of derivations resolves every path to the shared base', () => {
const sheet =
':root { --top: var(--left) var(--right); --left: var(--base); --right: var(--base); --base: 1px; }';
const analysis = analyzeTokenSheet(sheet, () => false);
const live = resolveLiveTokens(new Set(['--top']), analysis);
for (const token of ['--left', '--right', '--base']) {
assert.equal(live.has(token), true, `${token} should be live via the diamond`);
}
});
test('reads in rules without class selectors always count', () => {
const sheet = 'body { margin: var(--space-page); }';
const analysis = analyzeTokenSheet(sheet, () => false);
assert.equal(resolveLiveTokens(new Set(), analysis).has('--space-page'), true);
});