AFL

Parity harness

Commands, the check matrix, design mode and adding a widget.

Proves a migrated widget matches live afl.com.au. It records the live page once into git, then on every run replays that recording offline next to the new build rendered from the same data, and compares them pixel for pixel, style for style and interaction for interaction. Club sites are checked through their theme tokens.

The Software Factory runs it through the migrate-afl-widget skill. The wider framework is in the widget migration docs.

Quick start

From the repo root, after bun install:

bun run --cwd tools/parity parity:install
bun run --cwd tools/parity parity

parity starts apps/web through portless at https://parity.afl.localhost with PARITY=1 CONTENT_SOURCE=fixtures (or uses PARITY_BUILD_URL if set) and runs every widget spec. Open the report with:

bun run --cwd tools/parity playwright show-report .parity/report

Commands

CommandWhat it does
parity:installInstalls the Chromium build Playwright needs
parityRuns every widgets/*/widget.parity.ts; add -g <text> to filter by widget id or cell
parity:record <widget-id>Records the live pages at every width (plus the club reference) into live.har.zip, writes live.json (the frozen clock) and the fixtures from extract.ts
parity:inspect <widget-id> [filter]Dumps every element's matched live CSS rules, pseudo-elements and media queries included, into .parity/inspect/; club as the filter inspects the club reference and lists the club theme tokens it uses
parity:themesRecords the 30 club theme tokens from all 18 club sites into themes/live-tokens.json and regenerates packages/config/src/club-themes.css
parity:rebuild <widget-id>Starts a regeneration: records a rebuild on the tracker and snapshots the prior field mappings, latest runs and the component's fixtures against it (--component, --trigger, --rule, --change-log, --track); see Regenerating a widget
parity:publishUploads the screenshots in .parity/shots/ and posts .parity/runs.json to the tracker (TRACKER_URL, TRACKER_API_TOKEN); fails if the tracker rejects either. After parity:rebuild it also tags the runs with the rebuild and publishes its report

Run from inside tools/parity, drop the --cwd tools/parity.

How a check works

  1. Live side. Replays live.har.zip with every unrecorded request aborted, so nothing reaches the network; API stubs from the spec override the HAR; 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.
  2. Build side. Opens apps/web at /parity/<component>/<fixture>, which renders the fixture through the real block registry at the live widget's width, background and document offset (sub-pixel position changes rasterisation). Images come from the same HAR, so both sides decode identical bytes.
  3. Checks per cell.
    • screenshot: the widget at a 0.05 per-pixel colour threshold (absorbs anti-aliasing of a few levels, not a colour or position change). By default zero pixels may differ. Set PARITY_SCREENSHOT_THRESHOLD_PCT (percent of pixels, default 0) to allow a few: a difference within it is a yellow warn (counts as passed, never fails the run), above it fails. Any difference attaches diff.png, the build with differing pixels in yellow. Decision (OQ-1): the threshold is a share of pixels, not a colour distance.
    • styles: computed styles, attributes and boxes of every part in the spec.
    • behaviour:<name>: each hover, focus or click, followed by both checks again.
    • tokens (club cells): every club theme token resolved at the widget root equals that club's live value.
  4. Screenshots. Every screenshot and behaviour check keeps the live and build images, pass or fail, in .parity/shots/ and in the HTML report; the tokens check keeps the club render. parity:publish sends them to the tracker, where the item page compares them.

Regenerating a widget

A rebuild is triggered by hand, per widget. Run it from tools/parity:

  1. bun src/rebuild.ts <widget-id> creates the rebuild record (the same one POST /api/items/:id/rebuilds writes, snapshot included) and keeps the prior build in it: the item's field mappings, its latest runs and screenshots, and the component's fixtures. It writes .parity/rebuild.json.
  2. Regenerate the widget, then run parity.
  3. parity:publish sees .parity/rebuild.json, compares each new build screenshot with the prior one (yellow …-change.png for differing pixels, at PARITY_SCREENSHOT_THRESHOLD_PCT), publishes the runs with the rebuild id and asks the tracker for the report, then removes the file.

The report on /items/:id lists the field mappings and block fields that were added, removed or changed, the style and behaviour checks that changed state, and the changed screenshots side by side. A dropped field and an added one are linked as a replacement when the new field's mapping declares replaces: "<old target>" (POST /api/items/:id/mappings takes fieldType and replaces), or are suggested when they share a legacy source or a similar name; a suggestion is shown as such until replaces confirms it.

Design mode (redesign track)

Redesigned widgets have no live baseline. A widgets/<widget-id>/widget.design.ts spec (defineDesign, run by design()) renders the build at each Figma frame's viewport and width and runs:

  • figma: screenshots the build and compares it with widgets/<widget-id>/figma/<viewport>.png, the frame exported from AFLDS-Web-Library. It records the share of differing pixels as score and both images; it never fails on a difference, only when the export is missing.
  • a11y: axe with WCAG 2.1 AA rules on the widget. Any violation fails.
  • behaviour:<name>: performs the hover, focus or click, compares with <viewport>-<name>.png when it exists, and runs axe again.

parity:publish sends the scores and screenshots to the tracker, where the review grid compares Figma with the build. The redesign-afl-widget skill describes the whole flow.

Matrix

DimensionValues
WidthsDerived from the --breakpoint-* screens in packages/config/tailwind-preset.css: the existing 375, 600, 768, 1024, 1280, 1440 plus one width at the top of any live CSS band they miss (today 1159). Falls back to the six if the config is missing. Capped at 25 width cells per widget (maxCells in the spec overrides); trimmed evenly, with a warning
Siteafl.com.au for pixel, style and behaviour checks
ClubsToken check at 1280: all 18 if themeable: true, Carlton only otherwise
ModeLight; a source with mode: "dark" adds a .t-dark cell when the live widget has one

Club sites adopt the AFL markup with club theme tokens, so club cells compare tokens, not aflc-web markup.

Adding a widget

widgets/<widget-id>/
  widget.ts          defineWidget({ id, component, themeable, clubReference, sources, parts, behaviours, stubs, hide, mask })
  extract.ts         export async function extract(root, source) -> Block JSON fixture
  widget.parity.ts   parity(spec)
  live.har.zip       recorded by parity:record
  live.json          recording time
  • id is the tracker id; component is the folder in packages/ui/src the fixture and widget live in.
  • parts pair a live selector with a build selector ([data-slot=...]); :scope is the widget root.
  • Fixtures are written to packages/ui/src/<component>/fixtures/<fixture>.json.
  • hide and mask are only for things outside the widget's control; say why in the PR.

Notes

  • HAR files are a few MB per widget. Track widgets/**/*.har.zip with Git LFS before many widgets land.
  • Re-record when the live widget changes on purpose; the new baseline shows up as a diff in review.
  • The web server step builds apps/web's workspace dependencies first, so packages/ui changes are always in the render.
  • Passing --reporter on the command line replaces the configured reporters, including the one that writes .parity/runs.json; filter with -g instead.
  • Tests: bun test src (style diffing, HAR cleanup, check names, theme CSS generation).

Regeneration, the screenshot threshold and the matrix widths are described above and in How the parity harness works.