From f09c6b5d4d57bac9f26da116cb7857f953c871a3 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 15 Aug 2026 12:46:24 +0530 Subject: [PATCH] fix(web): read aria-disabled as disabled the enabled fact came off the disabled property, which only real form controls have. it reads undefined on the role-based controls the tappable set now covers, so every one of them looked enabled however plainly it was marked otherwise, and the fuzzer would spend actions on inert ones. both producers answer the same two ways, and the parity fixture carries a disabled row so the comparison covers it: reverting one side alone names the element and the fact. --- internal/driver/chrome/driver.go | 10 +++++++++- internal/driver/chrome/testdata/fact-parity.html | 1 + pkg/spec/src/web-runtime.ts | 14 ++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/internal/driver/chrome/driver.go b/internal/driver/chrome/driver.go index 074cae3..fac3554 100644 --- a/internal/driver/chrome/driver.go +++ b/internal/driver/chrome/driver.go @@ -393,6 +393,14 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) { // root a full-viewport tap target here and nowhere else. const NON_TEXT_INPUT_TYPES = ['button','submit','checkbox','radio','range','color','file','image','reset']; + // The disabled property belongs to real form controls only, so it reads + // undefined on the role-based controls the tappable set now covers, and every + // one of them looked enabled however plainly it was marked otherwise. + // isEnabled in pkg/spec/src/web-runtime.ts answers the same two ways. + function isEnabled(el) { + if (el.disabled) return false; + return el.getAttribute('aria-disabled') !== 'true'; + } function isEditableElement(el) { if (el.isContentEditable) return true; const tag = el.tagName.toLowerCase(); @@ -465,7 +473,7 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) { attributes: attrs, children: children, clickable: isClickable || null, - enabled: (!el.disabled) || null, + enabled: isEnabled(el) || null, focused: document.activeElement === el || null, checked: el.checked || null, selected: el.selected || null, diff --git a/internal/driver/chrome/testdata/fact-parity.html b/internal/driver/chrome/testdata/fact-parity.html index c29d0ed..c378ec9 100644 --- a/internal/driver/chrome/testdata/fact-parity.html +++ b/internal/driver/chrome/testdata/fact-parity.html @@ -71,6 +71,7 @@
tree item
attribute click
delegating root
diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 5e4b89b..24deca8 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -328,6 +328,16 @@ function selectorTag(selector: unknown): string { return ""; } +// isEnabled answers the `enabled` fact. `.disabled` is a property only real form +// controls have, so it reads undefined on the role-based controls the tappable +// set now covers, and every one of them looked enabled however plainly it was +// marked otherwise. internal/driver/chrome/driver.go answers the same two ways +// for the dump the goja host reads. +function isEnabled(element: Element): boolean { + if ((element as HTMLButtonElement).disabled) return false; + return element.getAttribute("aria-disabled") !== "true"; +} + function elementHandle(element: Element, selector: unknown): Record { const rect = element.getBoundingClientRect(); const x = Math.round(rect.left + rect.width / 2); @@ -346,7 +356,7 @@ function elementHandle(element: Element, selector: unknown): Record