mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* Give DESIGN.md a real type ramp so the design hook stops crying wolf The design hook fired on nearly every CSS file we touched. The cause was DESIGN.md's typography block: it declared seven named roles rather than a scale, and two of those roles used clamp(), which the extractor skipped outright. That left an allowlist of five sizes standing against the 86 distinct font sizes actually in use, so design-system-font-size flagged roughly 500 declarations. Editing any .astro page made it worse, because the companion-stylesheet scan re-reported the whole backlog. Extractor (cli/engine/design-system.mjs): - Read a typography.scale map as the enumerated ramp. - Read both clamp() endpoints as allowed sizes. These stay additive on purpose: clamp endpoints alone cannot switch the rule on, because a fully fluid system enumerates no discrete ramp and inferring one from its endpoints would flag every intermediate size. The existing abstention test still passes, and three new tests cover the added behavior. DESIGN.md: - Document a 19-step ramp, 8px through 72px at a 16px root. - Snap the five discrete role sizes onto ramp steps. This also fixes real drift. DESIGN.md claims to mirror kinpaku-tokens.css verbatim, but wordmark was 1.15rem in the CSS against 1.3rem documented, with tracking at 0.42em against 0.15em. Both are re-synced. Standardization, 64 declarations: - Six near-identical steps between 13.7px and 15.4px collapse onto 14 and 15. - .foundation-card-label, .designing-lane-mock-title and .designing-iterate-name each existed at two different sizes in two files. Now unified. - The wordmark rendered at four sizes (20.8, 18.4, 17, 16.8px). Now 18px, plus one deliberate smaller nav variant. Exemptions, for designs that are foreign on purpose: the antipattern-example fixtures, the neo-mirai case-study build, the periodic-table cell annotations in framework-viz.js (5 to 7px diagram geometry sitting at 2 to 3px offsets), and the .why-slop-* before-state card's Inter and gradient text. Verified by computed style across ten rendered pages: every element lands on a ramp step except clamp() values mid-interpolation, which is what fluid means. Full test suite and build validators pass. Generated provider output is deliberately left out; the sync workflow owns it. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Validate clamp() endpoints in usage, not just when reading DESIGN.md Reading clamp endpoints as documented steps without also checking them in source left an asymmetry: `isAllowedFontSizeRaw` returned true for anything failing the px/rem literal test, so `clamp(99rem, 1vw, 200rem)` passed. That is how `.ptable-symbol` at `clamp(1.45rem, 1.8vw, 1.8rem)` stayed invisible until someone measured computed styles, which is not a check the hook can run. Fluid values are now judged on their min and max. The viewport term interpolates between them and is never a fixed step, so it is left alone. Endpoints that cannot be resolved, such as var() or calc() or em, abstain rather than guess. Findings name the offending endpoint and use it as the ignore-value, because the whole clamp string is not actionable on its own. Turning the check on surfaced 22 fluid declarations that had never been looked at. Three used hero sizes above the ramp's 72px cap (80, 83.2 and 88px) alongside the display role's documented 89.6px max, so the top of the ramp was genuinely incomplete. Added the 80 and 88 steps, which gives the display end consistent 8px increments instead of 48/56/64/72 plus an orphan at 89.6, and fixes two declarations outright. The other 20 are snapped by a stated rule: nearest step, ties toward the smaller step, endpoints already matching a documented fluid role left as-is, and where nearest-step would make a breakpoint override meet or exceed its base, the next smaller step so the override still reduces. That last case applies once, to .designing-page-title. Also narrows the framework-viz.js waiver. The periodic-table cell annotations now carry two `impeccable-disable-line` comments naming the reason, instead of a config entry wildcarding the whole file for the rule. Inline waivers travel with the code and cannot silence future drift elsewhere in that file. Verified at 420px, 900px and 1600px across seven pages. The pinned ends are fully on-ramp; the only off-ramp values at 900px are the vw term mid-interpolation, which is what fluid means. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Address review: wordmark tracking picked the wrong side, stale ramp count Two review findings, both fair. Wordmark tracking (greptile, bugbot). This PR moved DESIGN.md's wordmark letterSpacing from 0.15em to 0.42em on the grounds that DESIGN.md claims to mirror kinpaku-tokens.css and the token read 0.42em. That was the wrong side to trust. `--ks-type-wordmark-track` has exactly one consumer, design-system.css:570, which is the specimen page. Every production lockup (.ks-wordmark, .kinpaku-chrome .site-header-brand-name, .footer-logo) hardcodes 0.15em, so 0.15em is what every visitor actually sees and what DESIGN.md already documented correctly before this PR touched it. Reverted the doc to 0.15em and moved the token to 0.15em as well, so the specimen now renders the same lockup as production instead of a wider one nothing else uses. Verified by computed style: header and specimen both report 18px with 2.7px tracking. No production visual change. Stale ramp count (copilot). The sidecar described an "18-step ramp, 8px through 72px". It went stale twice inside this PR, once when the 8 step was added and again when 80 and 88 were added for the hero display sizes. It is 21 steps, 8px through 88px. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Strip !important from the font-size ignore value Follow-on from the waiver wiring in the hook branch. The ignoreValue is what a `hooks ignore-value` waiver has to match, and `font-size: 1.4rem !important` emitted `1.4rem !important` while a plain declaration emitted `1.4rem`. Once font-size is a direct-value rule, that means the same size needs two different waivers depending on whether it carries a priority marker. font-family already strips the marker before matching, and there is a test for that. font-size now does the same. The snippet still shows the declaration as authored. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Have the wordmark rules consume their tokens instead of copying the values Follow-up to the tracking fix, and the residual half of what the reviewers were pointing at. `.ks-wordmark` and the kinpaku chrome lockup each repeated `1.125rem` and `0.15em` literally rather than reading `--ks-type-wordmark-size` and `--ks-type-wordmark-track`. That duplication is exactly how the token drifted to 0.42em while every production lockup stayed at 0.15em and nobody noticed, which is the confusion that started this thread. The values already agree, so this is a no-op visually and is verified as such: computed styles across the home, design-system, docs and changelog pages all still report 18px with 2.7px tracking. What changes is that there is now one place to edit, so the next tracking change cannot silently apply to the specimen page alone. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
503 lines
18 KiB
JavaScript
503 lines
18 KiB
JavaScript
/**
|
|
* Design-system normalization and source-rule tests.
|
|
* Run with: node --test tests/design-system.test.mjs
|
|
*/
|
|
|
|
import { describe, it, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import {
|
|
checkSourceDesignSystem,
|
|
collectStaticDesignSystemFindings,
|
|
isAllowedColorRaw,
|
|
isAllowedFont,
|
|
isAllowedRadiusRaw,
|
|
isAllowedFontSizeRaw,
|
|
loadDesignSystemForCwd,
|
|
normalizeDesignSystem,
|
|
} from '../cli/engine/design-system.mjs';
|
|
|
|
const tempDirs = [];
|
|
|
|
function mkTmp() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-design-system-'));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function sampleDesignSystem() {
|
|
return normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
|
|
body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '16px' },
|
|
label: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '0.875rem' },
|
|
},
|
|
colors: {
|
|
ink: '#241f1a',
|
|
paper: '#f7f4ee',
|
|
accent: '#b8422e',
|
|
gold: 'oklch(84% 0.19 80.46)',
|
|
},
|
|
rounded: {
|
|
sm: '4px',
|
|
md: '8px',
|
|
'"2xl"': '80px',
|
|
full: '999px',
|
|
},
|
|
},
|
|
sidecar: {
|
|
extensions: {
|
|
colorMeta: {
|
|
gold: {
|
|
canonical: 'oklch(84% 0.19 80.46)',
|
|
tonalRamp: ['#d9a531', '#b98518'],
|
|
},
|
|
},
|
|
roundedMeta: {
|
|
soft: {
|
|
canonical: '12px',
|
|
values: ['24px'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (tempDirs.length) {
|
|
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('normalizeDesignSystem()', () => {
|
|
it('normalizes typography, colors, sidecar ramps, and quoted rounded keys', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
|
|
assert.equal(isAllowedFont('avenir next', designSystem), true);
|
|
assert.equal(isAllowedFont('ibm plex sans', designSystem), true);
|
|
assert.equal(isAllowedFont('system-ui', designSystem), true);
|
|
assert.equal(isAllowedFont('poppins', designSystem), false);
|
|
|
|
assert.equal(isAllowedColorRaw('#241f1a', designSystem), true);
|
|
assert.equal(isAllowedColorRaw('oklch(84% 0.19 80.46 / 0.5)', designSystem), true);
|
|
assert.equal(isAllowedColorRaw('#d9a531', designSystem), true);
|
|
assert.equal(isAllowedColorRaw('#ff00aa', designSystem), false);
|
|
assert.equal(isAllowedColorRaw('var(--brand-accent)', designSystem), true);
|
|
assert.equal(isAllowedColorRaw('currentColor', designSystem), true);
|
|
|
|
assert.equal(isAllowedRadiusRaw('0', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('50%', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('80px', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('12px', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('24px', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('100px', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('9999px', designSystem), true);
|
|
assert.equal(isAllowedRadiusRaw('18px', designSystem), false);
|
|
|
|
assert.equal(isAllowedFontSizeRaw('16px', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('1rem', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('0.875rem', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('14px', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('12.5px', designSystem), false);
|
|
assert.equal(isAllowedFontSizeRaw('1.2em', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('var(--text-body)', designSystem), true);
|
|
|
|
// Fluid values are judged on their endpoints. This fixture documents 14px,
|
|
// 16px, and the display role's 40px/64px endpoints, so a 2rem (32px) max is
|
|
// off the ramp even though the 1rem min is on it.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 2rem)', designSystem), false);
|
|
assert.equal(isAllowedFontSizeRaw('clamp(2.5rem, 6vw, 4rem)', designSystem), true);
|
|
});
|
|
|
|
it('reads a typography.scale map as literal ramp steps', () => {
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
scale: {
|
|
micro: '0.5625rem', // 9px
|
|
body: '1rem', // 16px
|
|
title: '1.5rem', // 24px
|
|
},
|
|
body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '1rem' },
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(designSystem.hasFontSizes, true);
|
|
assert.equal(isAllowedFontSizeRaw('9px', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('0.5625rem', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('24px', designSystem), true);
|
|
// Off every step by more than the 0.5px tolerance.
|
|
assert.equal(isAllowedFontSizeRaw('0.82rem', designSystem), false);
|
|
assert.equal(isAllowedFontSizeRaw('20px', designSystem), false);
|
|
});
|
|
|
|
it('accepts both clamp() endpoints as ramp steps', () => {
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
scale: { body: '1rem' },
|
|
display: { fontFamily: 'Alumni Sans, sans-serif', fontSize: 'clamp(3.4rem, 6.5vw, 5.6rem)' },
|
|
},
|
|
},
|
|
});
|
|
|
|
// 3.4rem = 54.4px (min) and 5.6rem = 89.6px (max) are both documented.
|
|
assert.equal(isAllowedFontSizeRaw('3.4rem', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('5.6rem', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('54.4px', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('89.6px', designSystem), true);
|
|
// The vw middle term is viewport-relative, never a fixed step.
|
|
assert.equal(isAllowedFontSizeRaw('6.5px', designSystem), false);
|
|
// An arbitrary size between the endpoints is still off the ramp.
|
|
assert.equal(isAllowedFontSizeRaw('4.2rem', designSystem), false);
|
|
});
|
|
|
|
it('validates clamp() endpoints in usage, not just in DESIGN.md', () => {
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
scale: { body: '1rem', title: '1.5rem' }, // 16px, 24px
|
|
},
|
|
},
|
|
});
|
|
|
|
// Both endpoints documented.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 1.5rem)', designSystem), true);
|
|
// Neither endpoint is a step: 23.2px and 28.8px.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1.45rem, 1.8vw, 1.8rem)', designSystem), false);
|
|
// One bad endpoint is enough.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 1.8rem)', designSystem), false);
|
|
// The viewport term interpolates and is never judged as a step.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1rem, 6.5vw, 1.5rem)', designSystem), true);
|
|
// Unjudgeable endpoints abstain rather than guess.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(var(--a), 2vw, 1.5rem)', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('clamp(var(--a), 2vw, var(--b))', designSystem), true);
|
|
// Malformed or unparseable fluid values abstain.
|
|
assert.equal(isAllowedFontSizeRaw('clamp(1.45rem, 1.8vw)', designSystem), true);
|
|
// Non-clamp functional values keep abstaining.
|
|
assert.equal(isAllowedFontSizeRaw('calc(1rem + 3px)', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('var(--text-body)', designSystem), true);
|
|
});
|
|
|
|
it("accepts DESIGN.md's own fluid roles when used verbatim in source", () => {
|
|
// The endpoints a fluid role declares are documented sizes, so authoring
|
|
// that exact clamp must not flag. Regression guard for the asymmetry where
|
|
// the extractor read clamp endpoints but the checker never validated them.
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
scale: { body: '1rem' },
|
|
display: { fontFamily: 'Alumni Sans, sans-serif', fontSize: 'clamp(3.4rem, 6.5vw, 5.6rem)' },
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(isAllowedFontSizeRaw('clamp(3.4rem, 6.5vw, 5.6rem)', designSystem), true);
|
|
assert.equal(isAllowedFontSizeRaw('clamp(3.4rem, 6.5vw, 6rem)', designSystem), false);
|
|
});
|
|
|
|
it('strips CSS priority markers from the font-size ignore value', () => {
|
|
// The ignoreValue is what a `hooks ignore-value` waiver has to match, so a
|
|
// size must not need two different waivers depending on whether the
|
|
// declaration carries !important. font-family already behaves this way.
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: { typography: { scale: { body: '1rem' } } },
|
|
});
|
|
const findings = checkSourceDesignSystem(
|
|
'.a { font-size: 1.4rem !important; }\n.b { font-size: 1.4rem; }',
|
|
'/tmp/important.css',
|
|
{ designSystem },
|
|
);
|
|
const sizes = findings.filter((f) => f.antipattern === 'design-system-font-size');
|
|
assert.equal(sizes.length, 2);
|
|
assert.deepEqual(sizes.map((f) => f.ignoreValue), ['1.4rem', '1.4rem']);
|
|
});
|
|
|
|
it('reports which fluid endpoint is off the ramp', () => {
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: { typography: { scale: { body: '1rem' } } },
|
|
});
|
|
const findings = checkSourceDesignSystem(
|
|
'.a { font-size: clamp(1.45rem, 1.8vw, 1.8rem) !important; }',
|
|
'/tmp/fluid.css',
|
|
{ designSystem },
|
|
);
|
|
const sizes = findings.filter((f) => f.antipattern === 'design-system-font-size');
|
|
assert.equal(sizes.length, 1);
|
|
assert.match(sizes[0].snippet, /1\.45rem/);
|
|
assert.match(sizes[0].snippet, /1\.8rem/);
|
|
assert.equal(sizes[0].ignoreValue, '1.45rem');
|
|
});
|
|
|
|
it('does not let clamp() endpoints alone switch the font-size rule on', () => {
|
|
// A fully fluid system enumerates no discrete ramp, so inferring one from
|
|
// clamp endpoints would flag every intermediate size. Keep abstaining.
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
|
|
body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: 'clamp(1rem, 2vw, 1.125rem)' },
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(designSystem.hasFontSizes, false);
|
|
assert.equal(isAllowedFontSizeRaw('12.5px', designSystem), true);
|
|
});
|
|
});
|
|
|
|
describe('loadDesignSystemForCwd()', () => {
|
|
it('loads DESIGN.md plus .impeccable/design.json and marks stale sidecars', () => {
|
|
const cwd = mkTmp();
|
|
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
|
|
const designMd = path.join(cwd, 'DESIGN.md');
|
|
const sidecarJson = path.join(cwd, '.impeccable', 'design.json');
|
|
|
|
fs.writeFileSync(designMd, `---
|
|
typography:
|
|
body:
|
|
fontFamily: "IBM Plex Sans, Arial, sans-serif"
|
|
colors:
|
|
ink: "#241f1a"
|
|
rounded:
|
|
"2xl": "80px"
|
|
---
|
|
|
|
# Design System
|
|
`);
|
|
fs.writeFileSync(sidecarJson, JSON.stringify({
|
|
extensions: {
|
|
colorMeta: {
|
|
accent: {
|
|
canonical: '#b8422e',
|
|
tonalRamp: ['#d55a42'],
|
|
},
|
|
},
|
|
roundedMeta: {
|
|
lg: { canonical: '24px' },
|
|
},
|
|
},
|
|
}));
|
|
|
|
fs.utimesSync(sidecarJson, new Date('2026-01-01T00:00:00Z'), new Date('2026-01-01T00:00:00Z'));
|
|
fs.utimesSync(designMd, new Date('2026-01-02T00:00:00Z'), new Date('2026-01-02T00:00:00Z'));
|
|
|
|
const loaded = loadDesignSystemForCwd(cwd);
|
|
assert.equal(loaded.present, true);
|
|
assert.equal(loaded.sourcePath, designMd);
|
|
assert.equal(loaded.sidecarPath, sidecarJson);
|
|
assert.equal(loaded.mdNewerThanJson, true);
|
|
assert.equal(isAllowedColorRaw('#d55a42', loaded), true);
|
|
assert.equal(isAllowedRadiusRaw('80px', loaded), true);
|
|
assert.equal(isAllowedRadiusRaw('24px', loaded), true);
|
|
});
|
|
});
|
|
|
|
describe('checkSourceDesignSystem()', () => {
|
|
it('reports source fonts, literal colors, and radii outside DESIGN.md', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
const findings = checkSourceDesignSystem(`
|
|
.good {
|
|
font-family: "IBM Plex Sans", Arial, sans-serif;
|
|
color: #241f1a;
|
|
background: rgba(184, 66, 46, 0.45);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.bad {
|
|
font-family: "Poppins", sans-serif;
|
|
color: #ff00aa;
|
|
background: rgba(255, 0, 170, 1);
|
|
border-radius: 18px;
|
|
}
|
|
`, '/tmp/source.css', { designSystem });
|
|
|
|
assert.deepEqual(
|
|
findings.map((item) => item.antipattern),
|
|
['design-system-font', 'design-system-color', 'design-system-color', 'design-system-radius'],
|
|
);
|
|
assert.deepEqual(
|
|
findings.map((item) => item.ignoreValue),
|
|
['Poppins', '#ff00aa', 'rgba(255, 0, 170, 1)', '18px'],
|
|
);
|
|
});
|
|
|
|
it('strips CSS priority markers before checking font-family declarations', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
const findings = checkSourceDesignSystem(`
|
|
.good {
|
|
font-family: "IBM Plex Sans", Arial, sans-serif !important;
|
|
}
|
|
|
|
.also-good {
|
|
font-family: "Avenir Next" !important;
|
|
}
|
|
|
|
.bad {
|
|
font-family: "Poppins" !important;
|
|
}
|
|
`, '/tmp/important.css', { designSystem });
|
|
|
|
assert.deepEqual(
|
|
findings.map((item) => item.ignoreValue),
|
|
['Poppins'],
|
|
);
|
|
});
|
|
|
|
it('does not treat issue labels, HTML entities, or font variables as literal design values', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
const findings = checkSourceDesignSystem(`
|
|
<a href="https://github.com/example/repo/issues/155">#155</a>
|
|
<span class="spread-flow-icon">↔</span>
|
|
const MONO = 'SFMono-Regular, Roboto Mono, Consolas, monospace';
|
|
const FONT = 'IBM Plex Sans, Arial, sans-serif';
|
|
const COLOR_SAMPLE = 'rgba(255, 0, 170, 1)';
|
|
const COLOR_NOTE = 'oklch(60% 0.2 20)';
|
|
button.innerHTML = \`<span style="font-family:\${labelFont || FONT};">Pick</span>\`;
|
|
scale.style.cssText = 'font-family:' + MONO + '; font-size: 10px;';
|
|
.demo [style*="background: #fef3c7"] {
|
|
border-color: #ff00aa;
|
|
}
|
|
|
|
.bad {
|
|
font-family: "Poppins", sans-serif;
|
|
color: #cc00ff;
|
|
}
|
|
`, '/tmp/source.jsx', { designSystem });
|
|
|
|
assert.deepEqual(
|
|
findings.map((item) => item.ignoreValue),
|
|
['10px', '#ff00aa', 'Poppins', '#cc00ff'],
|
|
);
|
|
});
|
|
|
|
it('reports literal font sizes outside the DESIGN.md type ramp', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
const source = `.off-ramp {
|
|
font-size: 12.5px;
|
|
}
|
|
const label = { fontSize: "11px" };
|
|
const badge = { className: "text-[10px]" };
|
|
/* font-size: 9px; */
|
|
.on-ramp {
|
|
font-size: 1rem;
|
|
}
|
|
`;
|
|
const findings = checkSourceDesignSystem(source, '/tmp/sizes.css', { designSystem });
|
|
const fontSizeFindings = findings.filter((item) => item.antipattern === 'design-system-font-size');
|
|
|
|
assert.equal(fontSizeFindings.length, 3);
|
|
assert.deepEqual(
|
|
fontSizeFindings.map((item) => item.ignoreValue),
|
|
['12.5px', '11px', '10px'],
|
|
);
|
|
assert.deepEqual(
|
|
fontSizeFindings.map((item) => item.line),
|
|
[2, 4, 5],
|
|
);
|
|
});
|
|
|
|
it('abstains on font-size checks when DESIGN.md has no literal ramp steps', () => {
|
|
const designSystem = normalizeDesignSystem({
|
|
frontmatter: {
|
|
typography: {
|
|
display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
|
|
body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: 'clamp(1rem, 2vw, 1.125rem)' },
|
|
},
|
|
},
|
|
});
|
|
assert.equal(designSystem.hasFontSizes, false);
|
|
|
|
const findings = checkSourceDesignSystem('.bad { font-size: 12.5px; }', '/tmp/clamp-only.css', { designSystem });
|
|
assert.equal(findings.some((item) => item.antipattern === 'design-system-font-size'), false);
|
|
});
|
|
});
|
|
|
|
describe('collectStaticDesignSystemFindings()', () => {
|
|
function makeElement(tagName, { text = '', attrs = {}, style = {}, parentElement = null } = {}) {
|
|
return {
|
|
tagName: tagName.toUpperCase(),
|
|
textContent: text,
|
|
parentElement,
|
|
_style: style,
|
|
childNodes: text ? [{ nodeType: 3, textContent: text }] : [],
|
|
getAttribute(name) {
|
|
return Object.prototype.hasOwnProperty.call(attrs, name) ? attrs[name] : null;
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeWindow() {
|
|
const defaults = {
|
|
color: 'rgb(36, 31, 26)',
|
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
borderTopWidth: '0px',
|
|
borderRightWidth: '0px',
|
|
borderBottomWidth: '0px',
|
|
borderLeftWidth: '0px',
|
|
borderTopColor: 'rgb(36, 31, 26)',
|
|
borderRightColor: 'rgb(36, 31, 26)',
|
|
borderBottomColor: 'rgb(36, 31, 26)',
|
|
borderLeftColor: 'rgb(36, 31, 26)',
|
|
outlineWidth: '0px',
|
|
outlineColor: 'rgb(36, 31, 26)',
|
|
borderRadius: '0px',
|
|
display: '',
|
|
visibility: 'visible',
|
|
fontFamily: 'IBM Plex Sans, Arial, sans-serif',
|
|
};
|
|
return {
|
|
getComputedStyle(el) {
|
|
return { ...defaults, ...(el?._style || {}) };
|
|
},
|
|
};
|
|
}
|
|
|
|
it('skips non-rendered tags and hidden elements in the static DOM pass', () => {
|
|
const designSystem = sampleDesignSystem();
|
|
const hiddenParent = makeElement('section', { attrs: { hidden: '' } });
|
|
const elements = [
|
|
makeElement('style', {
|
|
text: '.hidden { color: #ff00aa; font-family: Poppins; }',
|
|
style: { color: 'rgb(0, 0, 0)', fontFamily: 'Poppins, sans-serif' },
|
|
}),
|
|
makeElement('script', {
|
|
text: 'const color = "#ff00aa";',
|
|
style: { color: 'rgb(0, 0, 0)', fontFamily: 'Poppins, sans-serif' },
|
|
}),
|
|
makeElement('div', {
|
|
text: 'Hidden Drift',
|
|
parentElement: hiddenParent,
|
|
style: { color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
|
|
}),
|
|
makeElement('div', {
|
|
text: 'Display None Drift',
|
|
style: { display: 'none', color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
|
|
}),
|
|
makeElement('div', {
|
|
text: 'Visible Drift',
|
|
style: { color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
|
|
}),
|
|
];
|
|
const findings = collectStaticDesignSystemFindings(
|
|
{ querySelectorAll: () => elements },
|
|
makeWindow(),
|
|
'/tmp/page.html',
|
|
designSystem,
|
|
);
|
|
const snippets = findings.map(item => item.snippet).join('\n');
|
|
|
|
assert.match(snippets, /Visible Drift/);
|
|
assert.doesNotMatch(snippets, /Hidden Drift/);
|
|
assert.doesNotMatch(snippets, /Display None Drift/);
|
|
assert.doesNotMatch(snippets, /\.hidden/);
|
|
assert.doesNotMatch(snippets, /const color/);
|
|
});
|
|
});
|