Skip to content

Commit 9a5385a

Browse files
balzssclaude
andcommitted
feat(ui-scripts): highlight changed regions and add theme facet filter to the visual-diff report
Make the report easier to read for the multi-theme regression suite: - The Diff image now renders the actual screenshot with a bright outline (and faint fill) around each changed region, grouped from the changed-pixel mask via a coarse-grid connected-components pass. This replaces pixelmatch's faded ghost + scattered red pixels, which was hard to interpret — especially for large or anti-aliased changes. Diff PNGs are now generated only for changed pairs. - A new --facets flag renders a row of one-click filter chips (e.g. canvas/light/dark) that filter screenshots by the "<name>-<facet>" suffix, alongside the existing status filter and name search. Generic; no theme names are hardcoded in the tool. - The visual regression workflow passes --facets canvas,light,dark. Adds unit tests for the region-boxing logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 99746bf commit 9a5385a

3 files changed

Lines changed: 279 additions & 27 deletions

File tree

.github/workflows/visual-regression.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ jobs:
8585
--pr-url ${{ github.event.pull_request.html_url }}
8686
--meta cypress/meta.json
8787
--source-base-url https://github.com/${{ github.repository }}/blob/${{ github.head_ref }}/regression-test/src/app
88+
--facets canvas,light,dark
8889
continue-on-error: true
8990

9091
- name: Publish report to gh-pages

packages/ui-scripts/lib/__node_tests__/visual-diff.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,25 @@ import {
2727
badgeFor,
2828
thumb,
2929
indexByName,
30-
sourceLinkFor
30+
sourceLinkFor,
31+
boxesFromMask
3132
} from '../commands/visual-diff.ts'
3233

34+
// Build a w*h changed-mask with the given filled rectangles set to 1.
35+
function mask(
36+
w: number,
37+
h: number,
38+
rects: Array<{ x: number; y: number; w: number; h: number }>
39+
) {
40+
const m = new Uint8Array(w * h)
41+
for (const r of rects) {
42+
for (let y = r.y; y < r.y + r.h; y++) {
43+
for (let x = r.x; x < r.x + r.w; x++) m[y * w + x] = 1
44+
}
45+
}
46+
return m
47+
}
48+
3349
describe('badgeFor', () => {
3450
it('returns the OK pill for unchanged status', () => {
3551
const html = badgeFor('unchanged')
@@ -155,3 +171,55 @@ describe('sourceLinkFor', () => {
155171
expect(html).toContain('rel="noopener"')
156172
})
157173
})
174+
175+
describe('boxesFromMask', () => {
176+
it('returns no boxes for an unchanged mask', () => {
177+
expect(boxesFromMask(mask(32, 32, []), 32, 32)).toEqual([])
178+
})
179+
180+
it('wraps a single changed cluster in one box that contains it', () => {
181+
const boxes = boxesFromMask(
182+
mask(32, 32, [{ x: 4, y: 4, w: 10, h: 10 }]),
183+
32,
184+
32
185+
)
186+
expect(boxes).toHaveLength(1)
187+
const [b] = boxes
188+
// the box must fully contain the changed region
189+
expect(b.x).toBeLessThanOrEqual(4)
190+
expect(b.y).toBeLessThanOrEqual(4)
191+
expect(b.x + b.w).toBeGreaterThanOrEqual(14)
192+
expect(b.y + b.h).toBeGreaterThanOrEqual(14)
193+
})
194+
195+
it('separates two distant clusters into two boxes', () => {
196+
const boxes = boxesFromMask(
197+
mask(64, 64, [
198+
{ x: 0, y: 0, w: 6, h: 6 },
199+
{ x: 50, y: 50, w: 6, h: 6 }
200+
]),
201+
64,
202+
64
203+
)
204+
expect(boxes).toHaveLength(2)
205+
})
206+
207+
it('drops specks smaller than minPixels', () => {
208+
// a single changed pixel is below the default minPixels threshold
209+
expect(
210+
boxesFromMask(mask(32, 32, [{ x: 5, y: 5, w: 1, h: 1 }]), 32, 32)
211+
).toEqual([])
212+
})
213+
214+
it('never lets a box exceed the image bounds', () => {
215+
const boxes = boxesFromMask(
216+
mask(20, 20, [{ x: 0, y: 0, w: 20, h: 20 }]),
217+
20,
218+
20
219+
)
220+
expect(boxes).toHaveLength(1)
221+
const [b] = boxes
222+
expect(b.x + b.w).toBeLessThanOrEqual(20)
223+
expect(b.y + b.h).toBeLessThanOrEqual(20)
224+
})
225+
})

0 commit comments

Comments
 (0)