CSS is Awesome

Testing

A design system fails in ways a unit test rarely sees: a renamed mixin, a theme missing a token, an import that works in the repo and breaks on install. cia runs twelve checks, and every one of them exists because something in that list actually happened.

The suite

CheckWhat it proves
lintESLint across the docs site
lint:scssstylelint across the SCSS library
check:theme-driftTheme artifacts match their SCSS sources. validate-themes reads the committed CSS, not the SCSS sources, and theme building was in no CI step — so a fix could land in scss/themes/*.scss, never be rebuilt, and CI would validate the stale output and report success. The drift gate rebuilds into a scratch copy and diffs, so source and artifacts cannot silently diverge. Runs before every check that reads the artifacts.
validate-themesEvery theme declares all 127 required contract tokens, and every one of the 22 audited pairs meets WCAG 2.2 AA contrast. Checks both light-dark() branches and keeps the worse result — a token that is two colours cannot pass by being legible in only one mode. Fails the build by default.
validate-iconsThe 49-glyph core pack is intact (extras are allowed)
validate-apiThe css-is-awesome/api barrel still emits zero CSS until a mixin is called
validate-packagePacks the tarball, installs it into a temp project, and compiles all ten documented @use specifiers
coverage:apiCalls every public mixin and function — 174 of them — and asserts the output is usable
coverage:mcpSpawns the MCP server over stdio and calls all 30 tools
size-budgetFails when a gzipped bundle outgrows its budget, and names the docs that quote the number. Raising a budget is a deliberate edit in the same commit as the growth — the figures had drifted to be overstated by up to 2× while nothing measured them.
validate-recipesCompiles every SCSS block in the recipes and asserts every cia.* symbol still exists, so a mixin rename fails here rather than in a consumer’s editor — a recipe that teaches a call which no longer exists is worse than no recipe.
testPlaywright — 240 tests across three engines: route smoke, axe accessibility, per-theme visual snapshots, theme-editor behaviour. 74 functional tests run in Chromium, Firefox and WebKit; the 18 visual-snapshot tests are Chromium-only by design.

All twelve run in CI on every pull request.

Call-and-assert coverage

SCSS has no conventional line-coverage tooling, so cia measures something more useful for a design system: is every part of the public API actually callable? The harness parses every public @mixin and @function out of the source, generates a fixture that calls it, compiles it, and asserts the result — it compiles, no null leaks into the CSS, functions return a value, mixins emit.

174 of 174 SCSS units and 30 of 30 MCP tools. CI fails below 98%. A mixin with no fixture counts as uncovered and lowers the number, so skipping a test is visible rather than invisible.

What 100% does and doesn’t mean

It means every public mixin and function is invoked by a test and produces sane output. It catches renames, broken signatures, undefined variables and bad refactors — it found a real one on its first run, an undefined $icon-size that broke four icon mixins whenever they were called without an explicit size.

It does not mean the CSS is visually correct. That is a deliberate trade: golden-file snapshots of ~180 mixins would catch silent output drift, but a single token change would churn dozens of snapshot files and train everyone to approve diffs without reading them. Visual correctness is covered by the per-theme Playwright snapshots instead, at page level.

Known gaps

Stated plainly, because a test suite that hides its blind spots is worse than a small one:

Cross-engine coverage used to head this list. It no longer does: the functional, smoke and a11y specs run in Chromium, Firefox and WebKit. That mattered because cia leans on light-dark(), :has(), [popover], <details name> and mask — exactly the surface where engines diverge, and exactly why it was worth closing.

visual.spec.ts is the deliberate exception. It owns the PNG baselines and Firefox and WebKit testIgnore it, because Desktop Safari’s deviceScaleFactor: 2 would make its screenshots a structurally different artifact rather than a comparable one. That is a decision about what a baseline is, not a gap in coverage.

Running it locally

npm run lint && npm run lint:scss
npm run check:theme-drift # artifacts vs sources
npm run validate-themes
npm run validate-package
npm run coverage          # api + mcp
npm test                  # Playwright (needs `npm run build` first)

Visual snapshots are keyed by platform, so a local run generates its own baselines rather than fighting CI’s. See Accessibility for the contrast contract that validate-themes enforces — 22 foreground/background pairs per theme, up from 17 once the five --code-* pairs were added. Those five surfaced 33 real failures across nine themes, all since fixed.

The contrast validator itself had the same shape of bug the gates exist to catch: it ignored unquoted [data-theme=dark] selectors and silently audited only :root, so a failing theme reported a pass. Fixed.

Theme