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