Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
--pr-url ${{ github.event.pull_request.html_url }}
--meta cypress/meta.json
--source-base-url https://github.com/${{ github.repository }}/blob/${{ github.head_ref }}/regression-test/src/app
--facets canvas,light,dark
continue-on-error: true

- name: Publish report to gh-pages
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/testing/visual-regression.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ InstUI runs visual regression tests on every pull request. They guard against un

On every PR:

1. **Build & capture.** GitHub Actions builds InstUI, builds the `regression-test` Next.js app as a static site, and serves it. Cypress visits every page under `regression-test/src/app/*`, waits for it to render, and calls `cy.screenshot()` in an `afterEach` hook, producing one PNG per test case.
1. **Build & capture.** GitHub Actions builds InstUI, builds the `regression-test` Next.js app as a static site, and serves it. Cypress visits every page under `regression-test/src/app/*` once per theme (via the `?theme=` query param), waits for each to render, and calls `cy.screenshot()` directly in the spec, producing one PNG per page/theme combination.
2. **Diff.** The `ui-scripts visual-diff` command compares each screenshot against the matching baseline fetched from the `visual-baselines` branch of the repo. It uses [pixelmatch](https://github.com/mapbox/pixelmatch) with antialiasing detection enabled, so typical font-rendering noise doesn't register as a diff.
3. **Publish.** The diff script writes an HTML report (with baseline / actual / diff for every row, plus a lightbox viewer) and the action publishes it to the `gh-pages` branch under `visual-regression/pr-<N>/`. The report is reachable at `https://instructure.design/visual-regression/pr-<N>/index.html`.
4. **Comment.** A sticky PR comment summarizes the result, links to the report, and inlines each changed row's diff image inside a collapsed `<details>` block.
Expand Down
70 changes: 69 additions & 1 deletion packages/ui-scripts/lib/__node_tests__/visual-diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,25 @@ import {
badgeFor,
thumb,
indexByName,
sourceLinkFor
sourceLinkFor,
boxesFromMask
} from '../commands/visual-diff.ts'

// Build a w*h changed-mask with the given filled rectangles set to 1.
function mask(
w: number,
h: number,
rects: Array<{ x: number; y: number; w: number; h: number }>
) {
const m = new Uint8Array(w * h)
for (const r of rects) {
for (let y = r.y; y < r.y + r.h; y++) {
for (let x = r.x; x < r.x + r.w; x++) m[y * w + x] = 1
}
}
return m
}

describe('badgeFor', () => {
it('returns the OK pill for unchanged status', () => {
const html = badgeFor('unchanged')
Expand Down Expand Up @@ -155,3 +171,55 @@ describe('sourceLinkFor', () => {
expect(html).toContain('rel="noopener"')
})
})

describe('boxesFromMask', () => {
it('returns no boxes for an unchanged mask', () => {
expect(boxesFromMask(mask(32, 32, []), 32, 32)).toEqual([])
})

it('wraps a single changed cluster in one box that contains it', () => {
const boxes = boxesFromMask(
mask(32, 32, [{ x: 4, y: 4, w: 10, h: 10 }]),
32,
32
)
expect(boxes).toHaveLength(1)
const [b] = boxes
// the box must fully contain the changed region
expect(b.x).toBeLessThanOrEqual(4)
expect(b.y).toBeLessThanOrEqual(4)
expect(b.x + b.w).toBeGreaterThanOrEqual(14)
expect(b.y + b.h).toBeGreaterThanOrEqual(14)
})

it('separates two distant clusters into two boxes', () => {
const boxes = boxesFromMask(
mask(64, 64, [
{ x: 0, y: 0, w: 6, h: 6 },
{ x: 50, y: 50, w: 6, h: 6 }
]),
64,
64
)
expect(boxes).toHaveLength(2)
})

it('drops specks smaller than minPixels', () => {
// a single changed pixel is below the default minPixels threshold
expect(
boxesFromMask(mask(32, 32, [{ x: 5, y: 5, w: 1, h: 1 }]), 32, 32)
).toEqual([])
})

it('never lets a box exceed the image bounds', () => {
const boxes = boxesFromMask(
mask(20, 20, [{ x: 0, y: 0, w: 20, h: 20 }]),
20,
20
)
expect(boxes).toHaveLength(1)
const [b] = boxes
expect(b.x + b.w).toBeLessThanOrEqual(20)
expect(b.y + b.h).toBeLessThanOrEqual(20)
})
})
Loading
Loading