Fix flex-basis 0% collapse, fluid max-width centering, letter-spacing snap, video frame normalization

- Keep basis/h/min-h [0%] literal (0% content-sizes on an indefinite axis;
  0px is definite zero) and fold grow + zero-basis into flex-1 - a
  flex:1 1 0% child no longer collapses to height 0 in auto-height columns
- Width centering accepts per-viewport caps (max-width: min(Npx, fluid))
  with symmetric gaps -> mx-auto + banded max-w instead of literal banded
  margins that drift off-centre between captured widths; non-painted
  viewports no longer poison the inference
- letter-spacing keeps sub-0.1px precision (no zero snap); style gate
  normalizes Chromium's letter-spacing "normal" <-> 0px serialization
- Videos pause + seek to frame 0 before every source AND clone screenshot,
  removing playback-time nondeterminism from perceptual evidence

278 tests pass (19 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-04 00:59:48 -07:00
co-authored by Claude Fable 5
parent 15d1e7a9e3
commit b445b62a69
8 changed files with 246 additions and 13 deletions
+26
View File
@@ -388,6 +388,32 @@ describe("generateCss literal-margin vs auto-centring", () => {
const all = allRulesX(css, "n1");
assert.ok(/margin-left:auto/.test(all), `a constrained centred block should still auto-centre, got: ${all}`);
});
// A centred `max-width` container whose cap is FLUID — `max-width: min(1272px, 100vw 2·gutter)`
// resolves to a DIFFERENT px at every width (311/673/1145). The block is centred at every sample
// (symmetric positive gaps that scale with width). It must auto-centre (`mx-auto`) with per-band
// caps, NOT freeze to literal per-band `margin-left` (which pins it left at non-captured widths).
it("auto-centres a container with a per-viewport (fluid) max-width cap", () => {
// border-box block, max-width per vp, box width == cap, symmetric gaps everywhere.
const child = xNode("n1", "div", {
375: { cs: { display: "block", boxSizing: "border-box", maxWidth: "311px", marginLeft: "0px", marginRight: "0px", width: "311px" }, bbox: { x: 32, y: 0, width: 311, height: 200 } },
768: { cs: { display: "block", boxSizing: "border-box", maxWidth: "673.2px", marginLeft: "0px", marginRight: "0px", width: "673.2px" }, bbox: { x: 47.4, y: 0, width: 673.2, height: 200 } },
1280: { cs: { display: "block", boxSizing: "border-box", maxWidth: "1145.08px", marginLeft: "0px", marginRight: "0px", width: "1145.08px" }, bbox: { x: 67.46, y: 0, width: 1145.08, height: 200 } },
});
const parent = xNode("n0", "body", {
375: { cs: { display: "block" }, bbox: { x: 0, y: 0, width: 375, height: 200 } },
768: { cs: { display: "block" }, bbox: { x: 0, y: 0, width: 768, height: 200 } },
1280: { cs: { display: "block" }, bbox: { x: 0, y: 0, width: 1280, height: 200 } },
}, [child]);
const css = generateCss(xIr(parent), new Map());
const all = allRulesX(css, "n1");
assert.ok(/margin-left:auto/.test(all), `fluid-cap centred container must auto-centre, got: ${all}`);
assert.ok(!/margin-left:0?67|margin-left:32px|margin-left:47/.test(all), `must NOT bake literal per-band left margins, got: ${all}`);
// The per-viewport caps survive as banded max-width (the canonical 1145.08 at base, others banded).
assert.ok(baseRule(css, "n1").includes("max-width:1145.08px"), `base carries the canonical cap, got: ${baseRule(css, "n1")}`);
const mobile = xBandRule(css, /^\(max-width/, "n1");
assert.ok(/max-width:311px/.test(mobile), `mobile band carries its own fluid cap, got: ${mobile}`);
});
});
// BUG C — a single-line text leaf whose unwrapped width nearly fills its column at every width gets
+38
View File
@@ -0,0 +1,38 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { letterSpacingEquivalent } from "../src/validate/gates.js";
// Chromium serializes a computed `letter-spacing: 0` back as the keyword `normal`. The emitter, after
// snapping a sub-0.1px authored tracking to 0, ships `letter-spacing: 0px` — which the CLONE then
// reports as `normal` too, but a source that authored an explicit near-zero px can land on either
// spelling. Gate 4 must treat `normal` and `0px` as equal, or every such node fails on spelling alone.
describe("gate4 letterSpacing normal ↔ 0px normalization", () => {
it("treats `normal` and `0px` as equivalent", () => {
assert.ok(letterSpacingEquivalent("normal", "0px"));
assert.ok(letterSpacingEquivalent("0px", "normal"));
});
it("treats `normal` and `normal` as equivalent", () => {
assert.ok(letterSpacingEquivalent("normal", "normal"));
});
it("treats a sub-2px authored tracking vs `normal` as equivalent (within ±2px)", () => {
// source computed `-0.08px`, clone serialized `normal` — a 0.08px delta, well within tolerance.
assert.ok(letterSpacingEquivalent("-0.08px", "normal"));
assert.ok(letterSpacingEquivalent("normal", "-0.0375px"));
});
it("still equates two close real px values (0.24px vs 0.2px)", () => {
assert.ok(letterSpacingEquivalent("-0.24px", "-0.2px"));
});
it("still FAILS a genuine tracking difference beyond ±2px", () => {
assert.ok(!letterSpacingEquivalent("4px", "normal"));
assert.ok(!letterSpacingEquivalent("normal", "-3px"));
assert.ok(!letterSpacingEquivalent("6px", "2px"));
});
it("passes when the source did not constrain the property (undefined)", () => {
assert.ok(letterSpacingEquivalent(undefined, "0px"));
});
});
+63 -1
View File
@@ -1,6 +1,6 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { declToUtil, snapBase } from "../src/generate/tailwind.js";
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
@@ -71,3 +71,65 @@ describe("snapBase spacing-scale snapping", () => {
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]");
});
});