UCDL Accessibility Use Case Definition Language
Contents

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)