diff --git a/docs/case-study.md b/docs/case-study.md index 9158315..6827161 100644 --- a/docs/case-study.md +++ b/docs/case-study.md @@ -37,7 +37,7 @@ Eleven routes, one per layout plus a few long posts with code, tables and math: ```bash R=(/ /about /blog /blog/page/2 /projects /tags /tags/markdown /blog/code-sample \ /blog/github-markdown-guide /blog/deriving-ols-estimator /blog/new-features-in-v1) -renderdiff http://localhost:3001 http://localhost:3002 $R +renderdiff http://localhost:3001 http://localhost:3002 "${R[@]}" ``` That is 55 shots at the default widths (1440, 1152, 896, 704, 375). renderdiff 0.3.0, Node @@ -117,7 +117,7 @@ The rest is two things the v4 migration does on purpose: After looking at crops for each, I accepted them: ```bash -renderdiff http://localhost:3001 http://localhost:3002 $R --rerun-failed \ +renderdiff http://localhost:3001 http://localhost:3002 "${R[@]}" --rerun-failed \ --accept "Tailwind v4 oklch palette and sub-pixel prose line-height (24.0001px); checked crops, invisible" ``` @@ -164,6 +164,10 @@ not verified: dark mode, hover and focus states, the open mobile menu, and the p ![newsletter: the v4 pink is brighter](img/case-study/newsletter-pink.png) + Fixed after this study: the threshold is now 0.05 (cut-off about 88). Rerun against the + same two builds, the pink shows up on every page that has it: 10 of 11 routes differ + instead of 4, and the new differences are the pink button, links and tags. + - **Phantom bands.** In run 1, `/` showed `band +48@y~1719 band -44@y~1721 band +24 band -28` on a page whose height didn't change. The bands sit on the newsletter form and the footer, where nothing differs above the pixel threshold. Once the header was fixed, the diff --git a/src/compare.ts b/src/compare.ts index 3a696c0..aa4783a 100644 --- a/src/compare.ts +++ b/src/compare.ts @@ -90,7 +90,8 @@ export interface Spot { target: Region | Band; } -const PIXELMATCH = { threshold: 0.1, includeAA: false, alpha: 0.25 }; +/** Not pixelmatch's default 0.1: it passes Tailwind v3→v4 pink-500 (YIQ delta 117, cut-off 352). */ +const PIXELMATCH = { threshold: 0.05, includeAA: false, alpha: 0.25 }; /** Edit-distance cap for `myers`: past it, memory is O(D²) and the answer is "unrelated" anyway. */ const MAX_EDIT = 2000; diff --git a/tests/fixtures/pages/README.md b/tests/fixtures/pages/README.md index 87168f5..bedc325 100644 --- a/tests/fixtures/pages/README.md +++ b/tests/fixtures/pages/README.md @@ -12,7 +12,7 @@ Three groups: `inline-block-vs-flex`. The two sides are visually identical and built with different CSS. These must score ≈100%. They are the corpus's central claim: the score tracks pixels, not markup, so a CSS refactor that changed nothing visible reads as "nothing changed". -- **Controls** — `shifted`, `taller`, `color`, `nav`, `margin`. Deliberately different. These must score low. +- **Controls** — `shifted`, `taller`, `color`, `tint`, `nav`, `margin`. Deliberately different. These must score low. A corpus of only passing cases proves nothing; without these, a tool that returned 100% unconditionally would pass the suite. They also pin the diff regions (`color`), the row alignment (`taller`, `nav`, and `shifted`, which has no band), the text diff (`nav`) and the diff --git a/tests/fixtures/pages/abs-vs-flex/left.html b/tests/fixtures/pages/abs-vs-flex/left.html index 8a5459a..eb57b54 100644 --- a/tests/fixtures/pages/abs-vs-flex/left.html +++ b/tests/fixtures/pages/abs-vs-flex/left.html @@ -8,9 +8,11 @@ /* LEFT: the transform-centring idiom. `translate(-50%, -50%)` only lands on whole pixels when the free space either side is an integer, which is why the hero and the box both have explicit even dimensions: - desktop (926 - 448) / 2 = 239 and (318 - 160) / 2 = 79, both exact. */ + desktop (926 - 448) / 2 = 239 and (318 - 160) / 2 = 79, both exact. At 375 the shared hero + is 343 wide, so left: 50% and translate(-50%) both land on .5; the box below keeps them whole. */ .hero { position: relative; } .hero-box { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } +@media (max-width: 480px) { .hero { width: 342px; } .hero-box { width: 310px; } } diff --git a/tests/fixtures/pages/abs-vs-flex/right.html b/tests/fixtures/pages/abs-vs-flex/right.html index 99d806a..9075ed4 100644 --- a/tests/fixtures/pages/abs-vs-flex/right.html +++ b/tests/fixtures/pages/abs-vs-flex/right.html @@ -7,6 +7,7 @@ diff --git a/tests/fixtures/pages/tint/left.html b/tests/fixtures/pages/tint/left.html new file mode 100644 index 0000000..d6a534c --- /dev/null +++ b/tests/fixtures/pages/tint/left.html @@ -0,0 +1,50 @@ + + + + +Layout parity + + + + +
+

Tint control

+

This pair is deliberately different. One swatch is a slightly different pink on each side, and nothing else changes.

+ +
+

One

208 by 160.

+

Two

208 by 160.

+

Three

208 by 160.

+

Four

208 by 160.

+
+
+

Five

208 by 160.

+

Six

208 by 160.

+

Seven

208 by 160.

+

Eight

208 by 160.

+
+
+

Nine

208 by 160.

+

Ten

208 by 160.

+

Eleven

208 by 160.

+

Twelve

208 by 160.

+
+ +
+

Notes

+

A single 208 by 160 swatch is a small share of the page, so this one lands in the amber band rather than the red one. That is the intended reading: the score is a triage signal that tells you where to look, not a verdict on how bad the change is.

+

It still has to move. A recoloured element that scored as parity would mean the comparison was measuring layout instead of pixels.

+
+
+ + diff --git a/tests/fixtures/pages/tint/right.html b/tests/fixtures/pages/tint/right.html new file mode 100644 index 0000000..d51c6a5 --- /dev/null +++ b/tests/fixtures/pages/tint/right.html @@ -0,0 +1,50 @@ + + + + +Layout parity + + + + +
+

Tint control

+

This pair is deliberately different. One swatch is a slightly different pink on each side, and nothing else changes.

+ +
+

One

208 by 160.

+

Two

208 by 160.

+

Three

208 by 160.

+

Four

208 by 160.

+
+
+

Five

208 by 160.

+

Six

208 by 160.

+

Seven

208 by 160.

+

Eight

208 by 160.

+
+
+

Nine

208 by 160.

+

Ten

208 by 160.

+

Eleven

208 by 160.

+

Twelve

208 by 160.

+
+ +
+

Notes

+

A single 208 by 160 swatch is a small share of the page, so this one lands in the amber band rather than the red one. That is the intended reading: the score is a triage signal that tells you where to look, not a verdict on how bad the change is.

+

It still has to move. A recoloured element that scored as parity would mean the comparison was measuring layout instead of pixels.

+
+
+ + diff --git a/tests/helpers/corpus.ts b/tests/helpers/corpus.ts index 8b033ca..30fbbb4 100644 --- a/tests/helpers/corpus.ts +++ b/tests/helpers/corpus.ts @@ -21,7 +21,7 @@ export const EQUIVALENCE = [ ] as const; /** Deliberately different. Must score low — otherwise the suite proves nothing. */ -export const CONTROLS = ["shifted", "taller", "color", "nav", "margin"] as const; +export const CONTROLS = ["shifted", "taller", "color", "tint", "nav", "margin"] as const; /** One per defence in src/capture.ts and src/config.ts. */ export const GUARDRAILS = [ diff --git a/tests/visual/controls.test.ts b/tests/visual/controls.test.ts index 90d4456..03b05d8 100644 --- a/tests/visual/controls.test.ts +++ b/tests/visual/controls.test.ts @@ -7,6 +7,7 @@ * shifted an 8px translation, nothing reflowed, heights identical * taller one extra section, so one side is padded with magenta * color one recoloured box, geometry untouched + * tint the same, but a pink a few units off: the per-pixel threshold must see it * nav one nav link missing on the left, so everything below it moves up 24px * margin one chip's margin-left changed, so it and the chip after it move 8px * @@ -247,6 +248,14 @@ for (const width of WIDTHS) { assert.match(e!.selector, /swatch/, context(run)); assert.deepEqual(e!.changed, { "background-color": ["rgb(31, 111, 235)", "rgb(198, 40, 40)"] }); }); + + test(`tint @${width} catches a pink a few units off, the swatch only`, () => { + const r = result(results(run.outDir), "/tint", width); + assert.equal(r.regions.length, 1, context(run)); + assert.deepEqual(r.regions[0]!.elements![0]!.changed, { + "background-color": ["rgb(246, 51, 154)", "rgb(236, 72, 153)"], + }); + }); } test("a named element's selector is unique on both sides, so --selector takes it", async () => {