UCDL Accessibility Use Case Definition Language
Contents

Accessibility Use Case Definition Language (UCDL) and Runner

This section is normative.

To resolve a target descriptor { role, name, scope? } to a Playwright locator against a page P, optionally given a current iteration element E (set when the descriptor appears inside a scope.for_each body, §5.8):

  1. Let L be undefined.
  2. If role is "self", set L to E. If E is undefined the processor MUST raise an error.
  3. Else if role is "id", set L to P.locator('#' + cssIdent(name)), where cssIdent is the CSS identifier escape defined by CSSOM (CSS.escape): an id such as main:content or 1st-section is a valid HTML id and MUST reach the selector as one.
  4. Else if role matches the pattern data-*, set L to P.locator('[' + role + '="' + cssString(name) + '"]'), where cssString escapes backslash and double quote with a backslash and a newline as \a, so the value cannot end the string or inject selector syntax.
  5. Else look up role in the role token table (§5.4).
    • If found and the entry's method is getByRole, set L to P.getByRole(entry.role, { name }).
    • If found and the method is getByLabel, set L to P.getByLabel(name).
    • If found and the method is getByText, set L to P.getByText(name).
  6. Else (custom role), set L to P.getByRole(role, { name }).
  7. If scope is present, recursively resolve scope to a locator S, and return S.locator(L). Otherwise return L.

Name matching. The authored name is matched in full by default: whitespace is normalised, and the name must equal the element's accessible name rather than appear within it. A processor MUST request exact matching explicitly rather than inheriting the automation engine's default.

contains opts into substring matching for one target:

locate: button contains "Sign"
verify: text contains "results found"

The default is the name because the name is the contract. Under a substring default button "Sign" would match both "Sign In" and "Sign Up" — and on a page carrying only one of them it would pass, so the document would read as an assertion about a specific control and be satisfied by any control whose name began with a fragment. verify: text "Error" would be satisfied by "No errors found".

Ambiguity. When a target resolves to more than one element, the step MUST fail with a message listing the candidates' accessible names, and the result MUST carry failure_reason: "ambiguous_target" (§10.1). Two controls sharing an accessible name are indistinguishable to anyone listing them by name, so this is a finding about the page rather than the automation engine's strict-mode error, and MUST NOT be reported as one.

6.2 Equivalence between modes

The same algorithm MUST be used by:

  • the live runner (runUseCase) when computing locators for direct execution; and
  • the code generator when emitting the locator expression embedded in the produced .spec.ts source.

A conforming implementation SHOULD share a single resolution module across both paths to avoid drift. (The reference implementation does so via src/shared/locator.ts.)

The select token resolves first to a combobox locator. At runtime, if the combobox locator's count() is greater than zero, that locator is used. Otherwise, the processor MUST fall back to getByLabel(name). A native <select> has the implicit role combobox (or listbox when it is multiple or sized), so the first branch finds most native selects and the fallback catches the rest by label. The fallback does not make a custom ARIA combobox driveable: selectOption() requires a native <select> whichever branch located it (§5.6.4).

In codegen, the emitted code MUST embody the same fallback logic.

A scope on the target (§5.5) binds to both branches: the combobox probe and the getByLabel fallback MUST each be resolved inside the scope, in both execution modes. Resolving either against the page discards a clause the document states — the step reads as scoped and asserts page-wide, and on a page carrying two controls of that name the result is the engine's strict-mode error rather than a finding.

When the role token is heading and a level N modifier is present, the locator MUST be constructed with the level option: getByRole('heading', { name, level: N }). This applies to both locate and verify: heading.