Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/case-study.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
```

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tests/fixtures/pages/abs-vs-flex/left.html
Original file line number Diff line number Diff line change
Expand Up @@ -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; } }
</style>
</head>
<body>
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/pages/abs-vs-flex/right.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<style>
/* RIGHT: the same centring delegated to the grid. */
.hero { display: grid; place-items: center; }
@media (max-width: 480px) { .hero { width: 342px; } .hero-box { width: 310px; } }
</style>
</head>
<body>
Expand Down
50 changes: 50 additions & 0 deletions tests/fixtures/pages/tint/left.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Layout parity</title>
<link rel="stylesheet" href="/shared/base.css">
<style>
/* CONTROL for colour sensitivity. Identical geometry; the third swatch is Tailwind v4 pink-500 on
the left and v3 pink-500 on the right: a few units per channel, visible side by side. */
.strip:first-of-type > .swatch:nth-child(3) { background: #f6339a; }
.strip { display: flex; gap: var(--gap); }
.strip + .strip { margin-top: var(--gap); }
@media (max-width: 480px) {
.strip { flex-direction: column; gap: var(--gap-mobile); }
.strip + .strip { margin-top: var(--gap-mobile); }
}
</style>
</head>
<body>
<div class="page">
<h1>Tint control</h1>
<p class="lede">This pair is deliberately different. One swatch is a slightly different pink on each side, and nothing else changes.</p>

<div class="strip">
<div class="swatch"><h3>One</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Two</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Three</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Four</h3><p>208 by 160.</p></div>
</div>
<div class="strip">
<div class="swatch"><h3>Five</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Six</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Seven</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Eight</h3><p>208 by 160.</p></div>
</div>
<div class="strip">
<div class="swatch"><h3>Nine</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Ten</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Eleven</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Twelve</h3><p>208 by 160.</p></div>
</div>

<section class="notes">
<h2>Notes</h2>
<p>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.</p>
<p>It still has to move. A recoloured element that scored as parity would mean the comparison was measuring layout instead of pixels.</p>
</section>
</div>
</body>
</html>
50 changes: 50 additions & 0 deletions tests/fixtures/pages/tint/right.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Layout parity</title>
<link rel="stylesheet" href="/shared/base.css">
<style>
/* CONTROL for colour sensitivity. Identical geometry; the third swatch is Tailwind v4 pink-500 on
the left and v3 pink-500 on the right: a few units per channel, visible side by side. */
.strip:first-of-type > .swatch:nth-child(3) { background: #ec4899; }
.strip { display: flex; gap: var(--gap); }
.strip + .strip { margin-top: var(--gap); }
@media (max-width: 480px) {
.strip { flex-direction: column; gap: var(--gap-mobile); }
.strip + .strip { margin-top: var(--gap-mobile); }
}
</style>
</head>
<body>
<div class="page">
<h1>Tint control</h1>
<p class="lede">This pair is deliberately different. One swatch is a slightly different pink on each side, and nothing else changes.</p>

<div class="strip">
<div class="swatch"><h3>One</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Two</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Three</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Four</h3><p>208 by 160.</p></div>
</div>
<div class="strip">
<div class="swatch"><h3>Five</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Six</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Seven</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Eight</h3><p>208 by 160.</p></div>
</div>
<div class="strip">
<div class="swatch"><h3>Nine</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Ten</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Eleven</h3><p>208 by 160.</p></div>
<div class="swatch"><h3>Twelve</h3><p>208 by 160.</p></div>
</div>

<section class="notes">
<h2>Notes</h2>
<p>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.</p>
<p>It still has to move. A recoloured element that scored as parity would mean the comparison was measuring layout instead of pixels.</p>
</section>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion tests/helpers/corpus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
9 changes: 9 additions & 0 deletions tests/visual/controls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading