[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
luke-ui docs VISUAL_TESTING.md
3.0 kB

Visual regression testing #

Visual cases are visual-tagged browser tests. TESTING.md defines what to capture and comparison policy.

Baseline #

The Visual baseline workflow renders main and uploads the visual-baseline artefact. Pull requests download the newest main run, render their branch, and compare it.

The newest run must have succeeded and retain its artefact. Never fall back to an older successful run: a stale baseline can pass an unchecked change. Re-run Visual baseline on main when the baseline is unavailable.

Run locally #

VISUAL_CAPTURE_DIR=.artifacts/visual-regression/baseline \
  pnpm --filter @luke-ui/react run test:visual:capture
pnpm run test:visual

Run the first command on main. test:visual writes expected, actual, and diff PNGs plus summary.json to .artifacts/visual-regression. Set VISUAL_BASELINE_DIR for another baseline.

Review #

Changed, added, and removed captures upload the visual-regression-diff artefact. The review job waits at the visual-review environment when that environment has a required reviewer.

Capture freeze #

freezeMotionForCapture waits a couple of animation frames, finishes in-flight CSS transitions and animations, then applies reduced-motion and zero-duration overrides. Cancelling a transition mid-flight (for example text-decoration-color on text Links) otherwise freezes an interpolated value and flakes the capture. During the freeze, text-decoration-color is pinned to currentColor so underlines cannot disappear from a cancelled colour transition. Exit animations are paused rather than finished so finishing them cannot unmount an open overlay before the screenshot. Restore resumes only the exit animations the freeze paused. CSS exit transitions are style-locked so reduced-motion recipe resets (transition: none) cannot cancel them mid-capture. Already-paused exit animations stay paused after restore.

captureVisual skips no-op viewport resizes (React Aria closes popovers on document scroll), re-binds body/html locators immediately before the screenshot so live-region text changes cannot invalidate the locator, and parks the pointer outside the viewport after each capture. Browser tests also park the pointer in afterEach so a leftover cursor cannot leave text Links hovered (standard prominence removes the underline under hover).

Tall scenes #

captureVisual expands the page and test iframe before capturing tall scenes.

Theme matrix #

Capture each appearance with captureVisualAppearance:

for (const appearance of visualAppearances) {
	test(`theme matrix: ${appearance.theme} ${appearance.mode}`, { tags: ['visual'] }, async () => {
		const { locator } = render(<Button>Continue</Button>, { appearance });

		await captureVisualAppearance(locator, 'button/theme-matrix', appearance);
	});
}

For portals, open the component and capture the portal or document.body.