Test kit
Test your own agent interface
A small, read-only script and a fifteen-minute protocol for checking any chat or agent product against these patterns, yours or anyone else's.
The harness runs in the page you're testing. It records what the page exposes to assistive technology while a response streams (every live-region update), where keyboard focus goes, and a handful of structural checks. It sends nothing anywhere. Everything stays in window.a11yHarness until you copy it out.
It is not a screen reader. It tells you what text reaches the accessibility layer, not how VoiceOver, NVDA, JAWS or TalkBack will speak it. Screen readers disagree with each other about live regions, so treat the harness as the first pass and confirm the important results by ear.
1. Load the harness
Open the product you want to test, then do one of these.
Console (works everywhere)
- Open developer tools (Cmd+Option+J on Mac, Ctrl+Shift+J on Windows) and go to the Console.
- Paste the contents of harness.js and press Enter. You’ll see "Agent A11y Harness v1.1 ready".
Bookmarklet
Drag this to your bookmarks bar, then click it on the page you're testing. Some sites' security policies block bookmarklets. If nothing happens, use the console.
2. Run the protocol
Use the same three prompts on every product so the results line up. Run each command in the console. Start a fresh or incognito chat so your history isn't involved.
| Step | Do this | Patterns it checks |
|---|---|---|
| A. Baseline | a11yHarness.regions() before sending anything. Note which live regions exist and whether they start empty. | Announced State Changes |
| B. Structured answer | Run a11yHarness.reset(), then send prompt 1 with Enter and keep focus in the composer. When it finishes, run summary(), structure() and await a11yHarness.actions(). | Announced State Changes, Focus Stays Put, Speaker Labels, Semantic Rendering, Actions on Focus |
| C. Long answer | reset(), send prompt 2. About five seconds in, run await a11yHarness.scrollTest(). Then click the composer and press Escape. Run summary() and check that the partial answer is still there. | Scroll Stays Put, Stop Control, Post-Stream Readability |
| D. Composer | Type a word in the composer, leave focus there, run await a11yHarness.imeTest(). If it didn't send, press Enter once normally to confirm synthetic Enter can send at all. | Forgiving Composer |
| E. Narrow screen | Set the window, or the device toolbar in dev tools, to 320px wide on the chat from step B. Run a11yHarness.reflow(). | Reflowable Output |
| F. By ear | Turn on a screen reader, repeat step B without looking, and write down what you hear. | All of the above, for real |
The prompts
Compare total-market, S&P 500 and international index funds in a table with columns for typical expense ratio, diversification and risk. Start with a one-sentence summary and end with three bullet-point caveats.Without searching the web, write a 900-word plain-language explanation of how index funds work, with several paragraphs.ime test(typed but not sent, for step D)
3. Read the results
- Announcements. A good result is a few short messages, like "responding" and then "done".
flags.possibleFirehoselists regions that updated more than 20 times or grew past 600 characters, which usually means the streaming text itself is live.flags.silent: truemeans nothing was announced at all.flags.busyGatedlists live regions that were updated whilearia-busy="true". That's usually a streaming answer that's meant to be announced once at the end, but screen readers don't all honouraria-busy, so check those by ear. - Focus.
focusNowshould still be the composer after the answer arrives. Any entry infocusChangesthat you didn't cause is a Focus Hijack. - Structure. Tables should have
thcells.messagesshould show who said what.unnamedVisibleButtonsshould be zero. - Actions.
hoverOnly: truemeans the control stays invisible even with keyboard focus. - Scroll.
held: truewithcontentGrew: trueis a pass. - Reflow.
pageScrollsSidewaysshould be false. Tables and code may scroll in their own box, ideally a focusable, named one.
Save copy(a11yHarness.report()) output with the date, the browser version and the product version. These products change every week, so an undated result doesn't mean much.
4. Share what you find
Results files follow a simple JSON shape: one verdict (pass, partial, fail or not tested) and one line of evidence per pattern. Start from the results template. Pull requests with new results, or fixes to the harness, are welcome.