Audit, test, and maintain

Prioritize and prevent regressions

Rank barriers by user impact and reach, then add the cheapest reliable guard that stops recurrence.

An audit of the flawed page produced twenty-three findings. You can’t fix twenty-three things at once, and not all of them matter equally. A div that blocks every keyboard user from registering is not the same as a status message that repeats once. Automated tools call both “serious”. You have to rank them yourself.

Rank by the task, not by the tool

For each finding I answer five questions: does it block the task completely, how many users does it affect, how often do they hit it, is there a workaround, and how sure am I. The first two dominate.

Here are the first five rows of the triage for our page:

#  Finding                                 Blocks task  Affects                  Fix
1  Register control is a div               yes          all keyboard and SR      now
2  Errors shown only by red border         yes          SR, color vision         now
3  Contact radios have no group name       no           SR                       now
4  "Saved" announced twice per change      no           SR                       later
5  Footer link contrast 4.3:1              no           low vision               later

Rows 1 and 2 make registration impossible for whole groups of people, so they go first even though row 5 is a one-line CSS change. Row 3 doesn’t block anyone, but it’s cheap, it’s confusing, and it’s in the main task path, so it rides along. Row 4 is annoying and goes in the next batch. Nobody defends a schedule by tool severity in a review. They defend it by who couldn’t register.

Every fix gets a guard

A fix without a guard is temporary. The next refactor of the form component reintroduces the placeholder-only field, and nobody notices until a user writes in. So each fix ships with the cheapest reliable check that fails when it comes back.

For the Register button, a Playwright test that asks for the button by role. A div has no button role, so getByRole finds nothing and the test fails:

import { test, expect } from '@playwright/test'

test('Register is a real button reachable by keyboard', async ({ page }) => {
  await page.goto('http://localhost:3000/register')
  await page.getByLabel('Arrival day').focus()
  await page.keyboard.press('Tab')
  await expect(page.getByRole('button', { name: 'Register' })).toBeFocused()
})

For the labels, getByLabel does the same job. It only finds inputs whose accessible name comes from a label:

test('every registration field has a label', async ({ page }) => {
  await page.goto('http://localhost:3000/register')
  for (const name of ['Full name', 'Email', 'Arrival day']) {
    await expect(page.getByLabel(name)).toBeVisible()
  }
})

Run it against the flawed page and you get the failure you want:

Error: expect(locator).toBeVisible() failed
  Locator: getByLabel('Full name')
  Expected: visible
  Received: <element(s) not found>

Match the guard to the fix

Not everything needs a browser test. The #999 contrast issue is guarded by a design token: there’s one --color-text-muted variable and it’s #595959. The aria-label mistakes are guarded by a line in the pull request template: “visible text is the accessible name”. The body aria-live problem is guarded by a one-line check in review: searching the codebase for aria-live and role="status" should return exactly one element. Pick the lightest thing that would catch the regression, and write down which finding it protects.

Try this on your own audit: sort your findings with the five questions, defend the first three out loud, and attach a guard to each before you call it fixed.

Lesson completed