UCDL Accessibility Use Case Definition Language
Contents

Writing Accessibility Use Case Tests with @afixt/usecase-runner

Part 5b: Iteration — Test Every Element of a Kind

Some accessibility checks are audit-shaped, not flow-shaped: "every image has alt text," "every link inside the breadcrumb has an accessible name," "after pressing Ctrl+A every option in the listbox is selected." The scope.for_each field promotes a single use case into an iteration over a collection of elements; the self token refers to the current iteration element.

id: every-image-has-alt
title: 'Every image on the page has an accessible name'
type: positive
start_location: 'https://example.com'
expected_result: 'No image lacks alt text'

scope:
  for_each: 'role "img"' # iterate every element with role="img"

steps:
  - locate: self
  - verify: self attribute "alt" present

for_each accepts a relaxed target form. The accessible-name modifier is optional (you usually want to iterate all elements of a role, not one):

scope:
  for_each: 'role "img"' # custom-role form, no name
  # equivalent:
  # for_each: image             # curated-token shorthand
  # for_each: 'image "Logo"'    # narrow to images named "Logo"

Scoping iteration to a parent region

Without scoping, "every link in the breadcrumb" iterates every link on the page — wrong scope, but a passing test still masks the mistake. Use within (or its synonym inside) to scope the iteration:

scope:
  for_each: 'link within navigation "Breadcrumb"'
  # equivalent:
  # for_each: 'link inside navigation "Breadcrumb"'
  # for_each: 'role "option" within role "listbox" name "Tags"'

Terminal aggregate assertions

After all iterations complete, the runner builds an iteration_summary with total, passed, failed, and inapplicable counts. Assert against those aggregates with a terminal verify: iteration_summary.<field> is N step — these are pulled out of the iteration body and evaluated after iteration completes:

scope:
  for_each: 'option within listbox "Tags"'
steps:
  - locate: self
  - keyboard: 'Control+a'
  - verify: self attribute "aria-selected" is "true"
  - verify: iteration_summary.failed is 0 # terminal — runs once after iteration

If for_each resolves to zero elements, the case is reported as inapplicable (not as a failure) — iteration_summary.inapplicable = 1.