Testing the Way Users Actually Experience the Web
An introduction to a tutorial series on @afixt/usecase-runner.
Most web test suites answer one question: does the page do what the developer built it to do? They find the button by its CSS class, click it at its coordinates, and check that the right div appeared. Every one of those steps reaches around the interface a person would use and pokes the DOM directly. The suite can be entirely green while a keyboard user can't reach the button, a screen-reader user can't tell what it's called, and nobody hears the confirmation.
@afixt/usecase-runner asks a different question: can a user accomplish this
goal? You write what a person does — find the link, reach it, activate it,
confirm where they ended up — in a small YAML syntax that a QA tester can author
and a product owner can read. The tool turns that into Playwright tests, or runs
it directly against a browser. And it targets every element the way assistive
technology does: by ARIA role and accessible name, never by CSS selector or
XPath.
That one constraint carries the whole philosophy. If an element isn't exposed in the accessibility tree, the test can't find it, and it fails. That failure isn't a limitation to work around — it's the finding. A flow written this way is a functional test and an accessibility test at once, and it can't be one without being the other.
The ideas underneath
Permalink to The ideas underneathA few convictions show up in every design decision, and each post in this series demonstrates one of them with a runnable example:
- A failure is a finding. Every red step maps to something a real user can't do and to a concrete fix in the page, not in the test.
- The test plan and the test are the same artifact. The YAML carries preconditions, expected results, and prose intent, and the report comes out in the same step-and-comments table a manual tester would deliver.
- Verbs are contracts about outcomes.
focusmeans focus was received.togglemeans the state flipped.verify: alertmeans arole="alert"announced it. None of them describes a gesture; all of them assert what the user experienced. - Strictness over silence. A step the parser doesn't fully understand is rejected, not quietly trimmed — because an assertion that silently asserts nothing is indistinguishable from a pass, and that's the most expensive defect a test tool can have.
- Report what actually ran. Whether a profile was enforced, whether a virtual or real screen reader spoke, which engine audited the page — the result always says how it was produced.
The posts
Permalink to The postsThe series is ordered so it reads as a progressive tutorial, but each post
stands alone with its own complete .uc.yaml.
The on-ramp
Permalink to The on-ramp- Part 1: Why Your Test Selectors Are Lying to You — a passing Playwright test against an inaccessible form, rewritten with usecase-runner so that it fails in four useful places.
- Part 2: A Test Your QA Team Can Actually Read — the anatomy of a use case file, and why it's shaped like a manual test plan on purpose.
The core
Permalink to The core- Part 3:
locate,focus,activate— the three questions every interactive element must answer, and a nav menu that fails the second one. - Part 4: Verify Like a Screen Reader, Not Like a Screenshot — form validation that looks perfect and announces nothing.
- Part 5: Strictness Is a Feature — the bug that makes the parser reject what it doesn't understand, and why ARIA defaults are never resolved silently.
- Part 6:
selectIs Nottoggle— a disclosure that only opens, and the verb that catches it. - Part 7: Testing Without a Mouse —
interaction profiles, and what happens to a hover-only menu under
--profile no-pointer.
For people already using it
Permalink to For people already using it- Part 8: One Flow, Many Outcomes — extension cases, negative paths, and the checklist every error state should pass.
- Part 9: Generate for CI, Run for Right Now — the two execution modes, why they must agree, and the three-value score.
- Part 10: Beyond the Step —
audit,contrast,lang_check,read_image,sr_says, and iterating a case over every element of a kind.
Following along
Permalink to Following alongnpm install @afixt/usecase-runner @playwright/test
npx playwright install
npx usecase-runner init
init drops a sample use case and config into the current directory. Point it
at something you own and read the failures as a list of things a real user can't
do. That's the habit the whole series is trying to build: don't test the page
you built; test the page your users get.