Files
ditto.site/compiler/test/tailwind.test.ts
Samraaj BathandClaude Fable 5 6e4921884c Hover capture via forced pseudo-state, text-wrap support, per-viewport video source re-selection, svg paint recovery, semantic tokens & section naming
Capture:
- Hover/focus states are driven with CDP CSS.forcePseudoState instead of
  moving the real cursor - transparent full-viewport overlay layers were
  swallowing every pointer hover, silently capturing zero hover states on
  hover-rich sites
- text-wrap (balance/pretty) captured and emitted (Tailwind text-balance/
  text-pretty, arbitrary fallback)
- Videos re-run <source> selection per viewport before frame-0
  normalization: resize-without-reload kept aspect-gated variants stuck on
  the load-time choice, poisoning mobile ground-truth screenshots
- svg roots capture their computed paint; fill="none" with a computed
  paint (the fill-current pattern) recovers the real color instead of
  rendering blank

Tokens & naming:
- Full CSS-Color-4 parsing (oklab/oklch/lab/lch/hsl) so modern colors
  cluster and earn semantic roles; visually-equal literals share one token;
  decoration/gradient/shadow colors consult the palette before minting
  opaque tokens (ridge: 45 opaque tokens -> 15, anthropic: 12 -> 7)
- Expanded role vocabulary (background/foreground/primary/accent/border/
  surface/muted) with deterministic tiebreaks and a chroma guard
- Section names mine CMS section ids and js-* hooks with hashy-suffix
  stripping (split_callout_JtTWTt -> split-callout-section)

364 tests pass (42 new), typecheck clean; determinism verified by
double-regen byte-comparison on two reference runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 02:22:15 -07:00

154 lines
7.9 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { declToUtil, snapBase, prettifyBase, collapseBases } from "../src/generate/tailwind.js";
// Zero-value gating. A named `-0` step only exists for props on Tailwind's spacing (or numeric)
// scales; for the rest the class compiles to NOTHING — a silent no-op that ships the wrong style
// (a map label captured at font-size:0 painted at the inherited 20px because `text-0` isn't a
// utility). Zeros for scale-less props must stay arbitrary so the declaration really compiles.
describe("declToUtil zero values", () => {
it("emits arbitrary text-[0px] for font-size:0 (no text-0 utility in v4)", () => {
assert.equal(declToUtil("font-size", "0px"), "text-[0px]");
assert.equal(declToUtil("font-size", "0"), "text-[0px]");
});
it("emits arbitrary tracking-[0px] for letter-spacing:0 (no tracking-0 utility in v4)", () => {
assert.equal(declToUtil("letter-spacing", "0px"), "tracking-[0px]");
assert.equal(declToUtil("letter-spacing", "0"), "tracking-[0px]");
});
it("emits arbitrary rounded corners for radius:0 (radius scale has no numeric 0)", () => {
assert.equal(declToUtil("border-top-left-radius", "0px"), "rounded-tl-[0px]");
});
it("keeps the named -0 for spacing-scale props", () => {
assert.equal(declToUtil("width", "0px"), "w-0");
assert.equal(declToUtil("width", "0"), "w-0");
assert.equal(declToUtil("top", "0px"), "top-0");
assert.equal(declToUtil("margin-left", "0px"), "ml-0");
assert.equal(declToUtil("line-height", "0px"), "leading-0");
});
it("keeps the named -0 for numeric scales that include 0", () => {
assert.equal(declToUtil("flex-grow", "0"), "grow-0");
assert.equal(declToUtil("flex-shrink", "0"), "shrink-0");
assert.equal(declToUtil("order", "0"), "order-0");
assert.equal(declToUtil("z-index", "0"), "z-0");
});
it("leaves non-zero values untouched", () => {
assert.equal(declToUtil("font-size", "20px"), "text-[20px]");
assert.equal(declToUtil("letter-spacing", "-0.5px"), "tracking-[-0.5px]");
});
});
// BUG B — the spacing-scale snap must only fire when a value lands ESSENTIALLY ON a step (≤0.25px).
// The scale is 2px-granular (`p-0.5`=2px, `p-1.5`=6px), so 3.5px is BETWEEN steps — snapping it up to
// p-1 (4px) adds +0.5px per side, which accumulates across a fixed-width flex row until items overflow
// and wrap. On-step values (2px→p-0.5, 4px→p-1) still snap.
describe("snapBase spacing-scale snapping", () => {
it("keeps a between-steps value arbitrary (3.5px does NOT snap to p-1)", () => {
assert.equal(snapBase("p-[3.5px]"), "p-[3.5px]");
});
it("does not snap the same sub-step delta on other spacing props", () => {
assert.equal(snapBase("pl-[3.5px]"), "pl-[3.5px]");
assert.equal(snapBase("gap-[3.5px]"), "gap-[3.5px]");
assert.equal(snapBase("mt-[13.5px]"), "mt-[13.5px]"); // between p-3 (12px) and p-3.5 (14px)
});
it("still snaps a value that sits on a 0.5-step (2px → p-0.5, 6px → p-1.5)", () => {
assert.equal(snapBase("p-[2px]"), "p-0.5");
assert.equal(snapBase("p-[6px]"), "p-1.5");
});
it("still snaps a near-exact on-step value within the tight budget (3.98px → p-1)", () => {
assert.equal(snapBase("p-[3.98px]"), "p-1");
});
it("leaves an already-clean scale utility unchanged and snaps 16px → mx-4", () => {
assert.equal(snapBase("p-1"), "p-1");
assert.equal(snapBase("mx-[16px]"), "mx-4");
});
});
// A percentage 0 on a MAIN-SIZE axis (flex-basis) or a %-of-indefinite-height axis (height/min-height)
// is NOT the definite zero `-0`: `flex-basis:0%` content-sizes against an auto-sized flex container,
// whereas `flex-basis:0` gives a zero base size (collapsing a `flex:1 1 0%` item in an auto-height
// column). prettifyBase must keep those literal. Width/inset 0% resolve against the definite
// containing-block width, so their `-0` rewrite stays.
describe("prettifyBase 0% on indefinite-axis prefixes", () => {
it("keeps basis-[0%] literal (0% ≠ definite 0 for flex-basis)", () => {
assert.equal(prettifyBase("basis-[0%]"), "basis-[0%]");
});
it("keeps h-[0%] and min-h-[0%] literal (%-of-indefinite-height → auto)", () => {
assert.equal(prettifyBase("h-[0%]"), "h-[0%]");
assert.equal(prettifyBase("min-h-[0%]"), "min-h-[0%]");
});
it("still rewrites width/inset 0% to the definite -0 (definite containing-block width)", () => {
assert.equal(prettifyBase("w-[0%]"), "w-0");
assert.equal(prettifyBase("min-w-[0%]"), "min-w-0");
assert.equal(prettifyBase("left-[0%]"), "left-0");
assert.equal(prettifyBase("inset-x-[0%]"), "inset-x-0");
});
it("still rewrites non-zero fractions on every prefix (basis-[33.3333%] → basis-1/3)", () => {
assert.equal(prettifyBase("basis-[33.3333%]"), "basis-1/3");
assert.equal(prettifyBase("h-[50%]"), "h-1/2");
assert.equal(prettifyBase("min-h-[100%]"), "min-h-full");
});
});
// flex:1 1 0% is Tailwind's `flex-1` — fold the grow-[1] + zero-basis pair so the emitted class both
// reads idiomatically AND resolves to the exact flex longhands (avoiding the basis-[0%] hazard).
describe("collapseBases flex-1 folding", () => {
it("folds grow-[1] + basis-[0%] → flex-1 (shrink defaults to 1, elided)", () => {
assert.deepEqual(collapseBases(["grow-[1]", "basis-[0%]"]), ["flex-1"]);
});
it("folds grow-[1] + basis-0 (already-shortened band delta) → flex-1", () => {
assert.deepEqual(collapseBases(["grow-[1]", "basis-0"]), ["flex-1"]);
});
it("does NOT fold when shrink-0 is present (flex:1 0 0% ≠ flex-1)", () => {
assert.deepEqual(collapseBases(["grow-[1]", "shrink-0", "basis-[0%]"]).sort(), ["basis-[0%]", "grow-[1]", "shrink-0"]);
});
it("does NOT fold a non-zero basis (grow-[1] + basis-[50%] left as-is)", () => {
assert.deepEqual(collapseBases(["grow-[1]", "basis-[50%]"]).sort(), ["basis-[50%]", "grow-[1]"]);
});
});
// letter-spacing is authored at a finer scale than box lengths; a real -0.08px tracking is within
// snapLen's 0.1px integer-snap window and would collapse to 0px → Chromium serializes it as `normal`
// → a false style-gate mismatch. Tracking must skip the integer snap (snapBase keeps 2 decimals).
describe("declToUtil letter-spacing sub-0.1px preservation", () => {
it("keeps a real -0.08px tracking (does NOT snap to tracking-[0px])", () => {
assert.equal(declToUtil("letter-spacing", "-0.08px"), "tracking-[-0.08px]");
});
it("keeps -0.0375px through declToUtil, then snapBase rounds to 2 decimals (not to zero)", () => {
assert.equal(declToUtil("letter-spacing", "-0.0375px"), "tracking-[-0.0375px]");
assert.equal(snapBase("tracking-[-0.0375px]"), "tracking-[-0.04px]");
});
it("still keeps a genuine zero as tracking-[0px]", () => {
assert.equal(declToUtil("letter-spacing", "0px"), "tracking-[0px]");
});
it("still integer-snaps a BOX length near an integer (204.9994px → 205px)", () => {
assert.equal(declToUtil("width", "204.9994px"), "w-[205px]");
});
});
// text-wrap: modern heading line-balancing. `balance`/`pretty` rebalance where a title wraps;
// without emitting them a two-line heading breaks differently in the clone. Tailwind v4 has the
// named utilities text-balance / text-pretty / text-nowrap / text-wrap; anything else (e.g.
// `stable`) falls through to the arbitrary property escape.
describe("declToUtil text-wrap", () => {
it("maps balance and pretty to the named Tailwind v4 utilities", () => {
assert.equal(declToUtil("text-wrap", "balance"), "text-balance");
assert.equal(declToUtil("text-wrap", "pretty"), "text-pretty");
});
it("maps wrap and nowrap to their named utilities", () => {
assert.equal(declToUtil("text-wrap", "wrap"), "text-wrap");
assert.equal(declToUtil("text-wrap", "nowrap"), "text-nowrap");
});
it("falls back to the arbitrary property for an unmapped value", () => {
assert.equal(declToUtil("text-wrap", "stable"), "[text-wrap:stable]");
});
});