UCDL Accessibility Use Case Definition Language
Contents

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

Here'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

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

Every 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 "..." or level 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.

The 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.

Run 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