JS Sketchbooksee JavaScript think ✏️
← back to phase 11

11.17 — Visual & a11y testing (bonus)

Bonus lesson — two specialist tools for the gaps everything else misses: visual testing (a pixel-level toEqual, because functional tests can’t see a destroyed layout) and the automated accessibility scan. Know they exist, know their honest limits, reach for them when the question is theirs.

watch it happen
// visual: a pixel-level toEqual
await expect(page).toHaveScreenshot("checkout.png");
// 1st run: SAVES the baseline image
// later runs: pixel-compare → fail on diff

// intended redesign? update the expected value:
// $ npx playwright test --update-snapshots

// tolerance for pixel weather:
await expect(page).toHaveScreenshot({
  maxDiffPixels: 100,
});

// a11y: the automated floor
import AxeBuilder from "@axe-core/playwright";
const results = await new AxeBuilder({ page })
  .analyze();
expect(results.violations).toEqual([]);

The gap first: a CSS regression rotates the total and shrinks the Pay button to unreadable — and every functional test PASSES, because toHaveText("₹900") only asks whether the text exists (11.6). The words are all there. The page is destroyed. Text assertions are blind to LOOKS.

functionally perfect · visually destroyedhow it should lookTotal: ₹900[ Pay now ]after the CSS regressionTotal: ₹900[Pay now]toHaveText("₹900") passes on BOTH — the text is therethe gap: every functional test passes — toHaveText, toBeVisible, all green —while the page looks DESTROYED. CSS regressions are invisible to textassertions✓ all functional tests pass
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Screenshot scope composes: expect(locator).toHaveScreenshot() snapshots ONE component instead of the page — much less brittle (only that widget’s pixels can fail), and usually the better default. fullPage: true, mask: [locator] (hide dynamic regions like timestamps), and stylePath (inject test-only CSS) handle the classic instabilities.

Baselines are stored per-platform and per-project (11.12) — checkout-chromium-linux.png — which is exactly why teams generate them on CI and treat local visual runs as advisory. The --update-snapshots flag exists on CI reruns for that reason.

The axe engine behind AxeBuilder is the industry’s shared standard (the same one inside browser devtools’ accessibility audits). Its rules map to WCAG — the Web Content Accessibility Guidelines — the spec accessibility law references worldwide. “Passes axe” is the floor; “meets WCAG AA” is the certificate humans verify toward.

Job note: teams increasingly staff “quality” to cover both of these — and interviewers probe with “how would you catch a CSS regression?” (answer: component-scoped toHaveScreenshot on key screens, baselines governed like spec changes) and “what does automated a11y testing miss?” (answer: everything needing judgment — focus order sensibility, alt-text QUALITY, screen-reader flow — the scan is a floor). Both answers are now yours.

your turn

⌨️ build the pixel differ

toHaveScreenshot is a comparison engine with a tolerance dial. Build it on strings-as-pixel-rows: count differences position by position, then let maxDiffPixels decide pass or fail.

requirements:

  • Write diffCount(baseline, actual) for two same-length strings: split each into characters (1.6’s train, car by car) and count positions where they differ — filter’s callback accepts the INDEX as its second argument (4.9’s fine print, finally used).
  • Write compare(baseline, actual, maxDiffPixels): compute the count, print diff pixels: N, then print within tolerance: pass or beyond tolerance: FAIL.
  • Run twice: "##########" vs "#########_" with tolerance 2 (antialiasing dust — passes), then "##########" vs "____######" with tolerance 2 (a real regression — fails).

when you press RUN, the console must show exactly:

diff pixels: 1
within tolerance: pass
diff pixels: 4
beyond tolerance: FAIL

✏️ Quick check 1

The first-ever run of toHaveScreenshot("checkout.png") — what does it do?

✏️ Quick check 2

A teammate runs --update-snapshots to make a red visual test green, without knowing why it was red. Which 10.3 trap is that?

✏️ Quick check 3

Does a passing axe scan certify the page as fully accessible? Type yes or no.

teach it back

🗣️ Now teach it back

Explain both bonus tools to a friend: the gap visual testing fills, the baseline/compare/diff cycle (and who governs baseline updates), the pixel-weather kit, and what the axe scan is — including what it honestly can’t do.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Visual = pixel-level toEqual: first run SAVES the baseline; later runs compare and write a DIFF image on mismatch. Baselines ARE expected values — 10.3’s law governs updates (--update-snapshots = a reviewed spec change, never a silencer).
Pixel weather kit: maxDiffPixels tolerance, animations disabled, baselines generated on ONE OS (CI’s), component-scoped screenshots over full pages. Budget: a FEW key screens — the most brittle layer buys precision, spent deliberately.
Axe scan = executable a11y FLOOR: violations (labels, contrast, roles — the tree 11.4’s locators already kept semantic) must be []. Machine-checkable ≈ a third to half; the rest needs human judgment. Floor, never certificate.