How the parity harness works
Explanation: what the harness compares, how it makes both sides deterministic, and what the pilot taught.
The harness opens the live page and the new build in two browser contexts with identical data, then compares them.
- Live side: replays
live.har.zipwithnotFound: "abort", so nothing reaches the network and the page is deterministic;stubsoverride API responses; the clock is frozen at the recording time; fixed and sticky elements outside the widget are hidden; the widget root is scrolled into view and its images decoded. - Build side:
apps/webat/parity/<component>/<fixture>renders the fixture through the real block registry, inside the theme class, at the live widget's width, background and document offset (sub-pixel positions change rasterisation). Images come from the same HAR, so both sides decode identical bytes. - Checks per cell: a screenshot of the widget (a 0.05 per-pixel colour threshold absorbs edge anti-aliasing of at most a few levels but not a colour or position change; by default zero pixels may differ, and
PARITY_SCREENSHOT_THRESHOLD_PCTlets a small share of pixels differ as a yellow warning that counts as passed); computed styles, attributes and boxes of every part in the spec; each behaviour (hover, focus, click) followed by both checks again. - Matrix: pixel, style and behaviour checks run for every afl.com.au source at every matrix width. The widths are derived from the
--breakpoint-*screens in the Tailwind preset (today 375, 600, 768, 1024, 1280, 1440 and 1159), capped at 25 per widget. Club cells run a tokens check: the build renders the widget undertheme--aflc-<club>and every club theme token resolved at the widget root must equal the club's live value (compared as rendered colours and borders, so#031e2fequalsrgb(3 30 47)); a screenshot of the club rendering is attached to the report for review. A themeable widget (themeable: true) runs all 18 clubs, a non-themeable one runscarlonly. - Themeability: give the spec a
clubReference(the same widget on a club page).recordadds it to the HAR andparity:inspect <id> clublists every rule in the club version that reads a club theme token. Any hit means themeable: use the same tokens on the same parts of the AFL markup. No hits means not themeable.
Lessons from the pilot
The framework was proven on article-list-sidebar-must-watch (Pulse widget 105, article-list/article-list-must-watch.ftl), which passed all 25 checks: screenshot, computed styles, hover and focus at all six widths on afl.com.au, and the Carlton token check. The widget itself is not part of the framework; it ships through the Software Factory like any other. What it took is the model for the skill:
- Cascade order decides real values: under 641px the generic
.widget-headermargin beats the sidebar one, and the list gets a 16px bottom margin only under 1026px. Read them frominspector from the style diff, not from the SCSS alone. - Pseudo-elements carry styling: the card shadow (
::before), the dark image placeholder (::before) that shows at anti-aliased corners, and the focus-within outline and shadow. - Live images are
<picture>with fixed transform sizes per media query (?width=344&height=215,808x505,451x268). The build must request the same URLs to decode the same pixels. - Sync static assets from the live version (
/resources/v5.55.20/i/svg-output/icons.svg); the repo's old sprite differed.
Regeneration
A widget that was already migrated can be rebuilt. parity:rebuild snapshots the prior build (field mappings, latest runs and screenshots, fixtures) on a tracker rebuild record, the widget is regenerated and re-run, and parity:publish diffs the new build against the snapshot: block fields added, removed or changed (a dropped and an added field are paired through the new mapping's replaces), checks that changed state, and screenshots with the differing pixels in yellow. The change log is always written; the item's original ticket is reused, and a child ticket is filed only when the rebuild is tracked. See Run parity checks.
To run it, see Run parity checks.