From a460eba3dbc015b81301201987c82795b1f44389 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 15 Aug 2026 12:45:49 +0530 Subject: [PATCH] fix(web): offer the aria roles a user activates only role=button was in the tappable set, so link, checkbox, radio, switch, tab, option, the menuitems and treeitem were invisible to the enumeration however plain the control looked. the replay ui builds its step rows as
  • , and the spec dogfooding it had to hand-write an action to reach them because no default verb could see a single row. both producers build the set from the same role list, since the parity test compares them element by element. --- internal/driver/chrome/driver.go | 7 ++++++- .../driver/chrome/testdata/fact-parity.html | 16 ++++++++++++++++ pkg/spec/src/web-runtime.ts | 17 ++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/internal/driver/chrome/driver.go b/internal/driver/chrome/driver.go index 97498ee..074cae3 100644 --- a/internal/driver/chrome/driver.go +++ b/internal/driver/chrome/driver.go @@ -413,8 +413,13 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) { visit(document); return out; } + const TAPPABLE_ROLES = [ + 'button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'option', + 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']; const clickableSet = new Set(deepQuery( - 'a, button, input, select, textarea, [role="button"], [onclick]')); + 'a, button, input, select, textarea, ' + + TAPPABLE_ROLES.map(role => '[role="' + role + '"]').join(', ') + + ', [onclick]')); const editableSet = new Set(deepQuery( 'input, textarea, [contenteditable]').filter(isEditableElement)); function buildTree(el, isRoot) { diff --git a/internal/driver/chrome/testdata/fact-parity.html b/internal/driver/chrome/testdata/fact-parity.html index dba230a..c29d0ed 100644 --- a/internal/driver/chrome/testdata/fact-parity.html +++ b/internal/driver/chrome/testdata/fact-parity.html @@ -56,6 +56,22 @@
    bio
    + + + + +
    switch
    + + +
    menu item checkbox
    +
    menu item radio
    +
    tree item
    +
    attribute click
    delegating root
    plain
    diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 244bc34..5e4b89b 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -569,7 +569,22 @@ function sanitizeAt(value: unknown, depth: number, seen: WeakSet): unkno // only how the DOM answers "is this clickable" / "is this editable", the two // facts with no direct DOM equivalent of the accessibility attributes native // platforms expose. -const TAPPABLE_SELECTOR = 'a, button, input, select, textarea, [role="button"], [onclick]'; +// +// TAPPABLE_ROLES are the ARIA roles whose whole contract is that a user +// activates the element. Covering only role="button" left every other one +// invisible to the enumeration, however plain the control looked: the replay UI +// builds its step rows as
  • , and the spec dogfooding it had to +// hand-write an action to reach them because no default verb could see a single +// row. internal/driver/chrome/driver.go resolves the same set for the hierarchy +// dump the goja host reads, and the two are compared element by element by +// TestHierarchy_DerivesTheSameFactsAsTheWebRuntime. +const TAPPABLE_ROLES = [ + "button", "link", "checkbox", "radio", "switch", "tab", "option", + "menuitem", "menuitemcheckbox", "menuitemradio", "treeitem", +]; +const TAPPABLE_SELECTOR = `a, button, input, select, textarea, ${ + TAPPABLE_ROLES.map((role) => `[role="${role}"]`).join(", ") +}, [onclick]`; const EDITABLE_SELECTOR = "input, textarea, [contenteditable]"; const NON_TEXT_INPUT_TYPES = [