A Test Your QA Team Can Actually Read
Part 2 of "Testing the Way Users Actually Experience the Web" — a tutorial
series on @afixt/usecase-runner.
Every accessibility consultancy has a pile of these: a spreadsheet or Word document with a row per step, a column for the step description, a column for the tester's comments, and a score at the bottom. A human sits down with a screen reader, walks the steps, and fills in the comments.
Those documents are excellent. They describe what a user is trying to do, they capture preconditions and expected outcomes, and anyone — developer, product owner, client — can read them. They are also slow, expensive, and performed once.
Meanwhile the engineering team has a Playwright suite that runs on every commit, that nobody outside engineering can read, and that encodes none of the accessibility intent from the manual plan.
@afixt/usecase-runner exists to make those two artifacts the same artifact.
This post is about the file format, and why every field in it is there.
Start from the manual test plan
Permalink to Start from the manual test planHere's a row from a typical manual use case document:
Use case: Log in to the system Preconditions: User has a valid username and password; user is not already logged in. Start location: Home page Expected result: User is logged in and lands on the dashboard. Steps: 1. Find the "Client Sign In" link. 2. Activate it. 3. Enter username. 4. Enter password. 5. Activate Login. 6. Confirm dashboard loads.
Now the same thing as a .uc.yaml file:
id: login-success
title: 'Log In To The System'
type: positive
description:
'Verify that a user with valid credentials can log in and reach the dashboard.'
preconditions:
- 'User has a valid username and password'
- 'User is not already logged in'
start_location: 'https://www.example.com'
expected_result: 'The user will be logged in and redirected to the dashboard'
data:
username: '[email protected]'
password: 'xyzpdq123!)'
dashboard_url: 'https://www.example.com/dashboard'
steps:
- locate: link "Client Sign In"
- focus: link "Client Sign In"
- activate: link "Client Sign In"
- verify: url "https://www.example.com/login.php"
- locate: field "username"
- focus: field "username"
- enter: field "username" value "{{ username }}"
- locate: field "password"
- focus: field "password"
- enter: field "password" value "{{ password }}"
- locate: button "Login"
- focus: button "Login"
- activate: button "Login"
- verify: url "{{ dashboard_url }}"
Put them side by side. Nothing was lost in translation. The preconditions are
still preconditions. The expected result is still stated in prose, for a human.
The steps still read as a sequence of things a person does. The only thing added
is precision: "find the link" became locate: link "Client Sign In", which says
exactly what kind of thing it is and what it's called — which is exactly
what a screen-reader user hears.
The fields, and what each one is for
Permalink to The fields, and what each one is for| Field | Why it exists |
|---|---|
id |
Kebab-case, unique. Becomes the filename of the generated test and the key other cases use to extend this one. |
title |
The human name shown in reports. |
type |
positive (happy path) or negative (error path). Recorded in the report for the reader; it does not affect how the document is resolved or run — composition is extends:. |
description |
Longer prose. Where the intent lives. |
preconditions |
A list of prose statements. They are not executed — they are printed into the generated test and the report, because the person reading a failure needs to know what state the test assumed. |
start_location |
The URL the browser opens first. |
expected_result |
Prose. The outcome in the user's terms, not the assertion's. |
data |
Named values referenced in steps as {{ name }}. Keeps credentials and URLs out of the step text and makes variants cheap (more on that in Part 8). |
steps |
The sequence. One action or assertion per line. |
Notice how much of this is prose that is never executed. That's deliberate. A test that only contains executable assertions is unreadable to the people who need to act on its failures. A use case carries its own context.
Steps are a sentence, not a function call
Permalink to Steps are a sentence, not a function callEvery step has the same shape:
keyword: role_token "accessible name" [modifiers]
- keyword — what the user does:
locate,focus,enter,select,activate,toggle,verify, and a handful of others. - role_token — what kind of thing it is:
button,link,field,heading,dialog,checkbox,navigation... 61 tokens covering the concrete ARIA 1.2 roles. - "accessible name" — what it's called. This is the name a screen reader
announces, which is not always the visible text. If a button reads "Submit" on
screen but has
aria-label="Send form", the step says"Send form". - modifiers — keyword-specific extras like
value "..."orlevel 2.
Read activate: button "Login" aloud. It's an instruction a tester could
follow. Read await page.locator('.btn-primary').click() aloud. It isn't.
Validate before you run
Permalink to Validate before you runThe parser is strict — every step must parse completely (Part 5 is entirely about why). So the first thing you do with a new file is validate it, which needs no browser:
npx usecase-runner validate login-success.uc.yaml
Typos in keywords, unknown role tokens, malformed modifiers, and missing required fields are all reported here, with the offending token named.
The report closes the loop
Permalink to The report closes the loopRun it:
npx usecase-runner run login-success.uc.yaml --report html
The HTML report is deliberately styled after the two-column step/comments table
from the manual deliverable. Each row is a step; the comments column carries the
pass/fail status, timing, and — on failure — the error plus
accessibility_notes explaining in plain language what wasn't found and the
likely causes. The score at the bottom uses the same rubric the manual process
does: Pass, Pass w/ Conditions, Fail — plus Error, for a case
whose setup never completed and which therefore never reached the behaviour it
exists to judge.
The person who wrote the manual plan can read the automated result. The developer who gets the failure can read the original intent. Same file.
Next:
locate, focus, activate: The Three Questions Every Interactive Element Must Answer