Dev.to AI 🤖 Ai 👁 0 📖 4 min read

Smoke-test agent UI with keyboard-only before you merge

I used to treat “tab through the page” as an a11y nicety — something for the checklist after visual QA. Then an agent shipped a polished settings drawer that looked fine with a mouse and was basically unusable from the k

I used to treat “tab through the page” as an a11y nicety — something for the checklist after visual QA. Then an agent shipped a polished settings drawer that looked fine with a mouse and was basically unusable from the keyboard.

Focus landed on a hidden close icon. Esc did nothing. The primary Save button was a <div onClick>. Arrow keys in a custom select ate the page scroll. Mouse users never noticed. Keyboard users bounced in under a minute.

So I added a hard gate: before merge, smoke the agent UI keyboard-only. No mouse. No trackpad. Five minutes, max. If I can’t finish the happy path, the PR isn’t done — green CI or not.

Why agents fail keyboard more than mouse

Coding agents optimize for what shows up in screenshots and Storybook canvases: layout, color, copy. Pointer paths are implicit (“click the button”). Keyboard paths are a second product:

  • focus order
  • visible focus rings
  • Escape / Enter semantics
  • roving tabindex in menus / lists
  • which nodes are actually <button> / <a> vs clickable divs

Unless you put keyboard constraints in the prompt and verify them by hand (or with a tiny Playwright keyboard script), the model ships the pretty mouse path and leaves the other half for support tickets.

The five-minute smoke (I do this on every agent UI PR)

Unplug the mouse metaphorically: don’t touch it.

  1. Land on the surface — refresh so focus starts where a real user would (URL bar → page, or skip link if you have one).
  2. Tab to the primary action — count stops. If it takes 20+ tabs through chrome you don’t care about, the surface needs a skip / better order.
  3. Complete the happy path with keyboard only — open the modal/drawer, fill required fields, submit, see success. Enter should activate the focused control; Space too for buttons.
  4. Cancel / dismiss — Esc closes overlays; focus returns to the opener. If focus drops to <body>, reject.
  5. One “trap” check — open a menu or combobox, Arrow through options, Tab out (or Esc). Confirm you aren’t stuck inside a focus jail with no exit.

That’s it. Not a full WCAG audit. A smoke that catches the agent’s most common misses.

What I reject on sight

Patterns that keep showing up in agent diffs:

  • Clickable non-buttons — <div role="button"> without tabIndex={0} and key handlers, or worse, bare <div onClick>
  • outline: none with no replacement — focus invisible on dark panels the agent “cleaned up”
  • Modal without focus trap / restore — Tab escapes into the page under the overlay; Esc doesn’t close
  • Custom select that only listens to click — looks like a Select, acts like a painting
  • Disabled reasons only on hover title — keyboard users never see why Save is dead
  • Auto-focus into a destructive field — agent “helpfully” focuses Delete on open

I paste the failing step into the review (“Tab 4 lands on Close before Title; Esc leaves drawer open”) and ask for a patch-sized fix, not a rewrite.

Make the agent earn the keyboard path up front

Don’t wait for review. Bake it into the task:

## Keyboard acceptance (required)

- All interactive controls reachable by Tab in a sensible order
- Visible focus ring on every focusable (no outline:none without :focus-visible replacement)
- Primary actions are <button> or <a href>, not div/span click handlers
- Overlay: Esc closes; focus returns to opener; Tab cycles inside while open
- Happy path completable keyboard-only (no mouse)

Do not mark done until you can describe the Tab sequence for the happy path in the PR.

When the agent lists the Tab sequence in the PR body, review gets faster — and the model usually fixes half the issues before you touch the branch.

Optional: one Playwright keyboard check

For surfaces agents touch often (auth, checkout, settings), I keep a tiny test that never moves the mouse:

test('settings drawer keyboard happy path', async ({ page }) => {
  await page.goto('/settings');
  await page.getByRole('button', { name: 'Edit profile' }).focus();
  await page.keyboard.press('Enter');

  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();

  await page.keyboard.press('Tab'); // name
  await page.keyboard.type('Ada');
  await page.keyboard.press('Tab'); // save
  await page.keyboard.press('Enter');

  await expect(page.getByText('Saved')).toBeVisible();
});

It’s brittle if you over-specify Tab counts — prefer getByRole + Enter/Space, and assert focus with toBeFocused() on the critical stops. One test beats another “looks good in Storybook” merge.

Tiny habit

Next agent UI PR: before you approve, put the mouse aside and Tab the happy path once. If you can’t Save and Esc cleanly, send it back with the exact stop that failed.

Agents are great at arranging pixels. Keyboard smoke is how you make sure a human who never touches the pointer can still finish the job.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.