Writing Accessibility Use Case Tests with @afixt/usecase-runner
LOCATE: locate: role ["name"] → Is it in the accessibility tree?
FOCUS: focus: role ["name"] → Can it receive keyboard focus?
ENTER: enter: field "name" value "..." → Type into a field (name required)
SELECT: select: checkbox|radio|select "name" [option "..."] (set-to-on)
select: option "name" [force true] (force = disabled option)
DESELECT: deselect: checkbox "name" (set-to-off)
ACTIVATE: activate: button|link "name" [via keyboard] [force true]
TOGGLE: toggle: role "name" [attribute "aria-pressed"] → did it flip?
VERIFY: verify: url|title|text|heading|alert|field_error|visible|
hidden|enabled|disabled|checked|unchecked|focus|count|
attribute|live_region|download|iteration_summary
TYPE: type: literal text → into whatever has focus
KEYBOARD: keyboard: Tab|Enter|Shift+Tab → key names ONLY, not text
HOVER: hover: role "name" / hover_out: role "name" (mouse-only)
SCROLL: scroll: to text|button "name"
VIEWPORT: viewport: mobile|tablet|desktop|reflow / viewport: 414x896
WAIT: wait: 2000 | 2000ms | 2s → bare number = milliseconds
AUDIT: audit: page|role "name" [level "A"|"AA"|"AAA"]
CONTRAST: contrast: text|self [level "AA"|"AAA"]
LANG: lang_check: page|self
OCR: read_image: image "name" [has_text "..." | matches "regex"]
SR: sr_says: "phrase" [after <step>] [within 5000|5000ms|5s]
PREDICATES (after attribute "X"):
is "Y" | is_or_absent "Y" | present | absent | starts_with "Y" |
matches "regex" | references_existing_id | within_range_of "min" "max"
ROLES (curated; 61 tokens — see docs/specification.md §5.4 for full list):
Common: button, link, field, text, heading, image, dialog
Widgets: checkbox, radio, switch, option, select, combobox,
searchbox, textbox, slider, spinbutton, progressbar,
meter, tab, tabpanel, treeitem, menuitem
Composite: menu, menubar, tablist, listbox, tree, treegrid, grid,
radiogroup
Structure: article, feed, figure, group, list, listitem, table,
row, rowgroup, cell, gridcell, columnheader, rowheader,
toolbar, tooltip
Landmarks: banner, navigation, main, complementary, contentinfo,
region, search, form
Live: alert, log, status
Window: alertdialog
Escape: role "custom", id "...", data-* "..."
NAME: optional for role-based targets:
locate: main (any main landmark)
verify: count tablist is 1
required for field (getByLabel) and text (getByText)
SCOPE: ... within|inside region|dialog|navigation "name"
ITERATE: scope.for_each: 'role "X"' / 'role "X" within Y "Z"'
steps use `self` token to refer to the current element
terminal: verify: iteration_summary.failed is 0
DATA: {{ variable_name }}
ATTR: verify: target attribute "X" is "Y" | present
| starts_with "Y" | matches "regex"
| references_existing_id
| within_range_of "min" "max"
SR-WITHIN: sr_says: "X" within 5s (or 5000 / 5000ms)
sr_says: "X" after activate button "Y" within 2s
TYPES: positive = happy path
negative = error path
extension = variant of another case (uses extends + steps_override)