From bd01a76d70b7c138438a608fc71c1a43935a112d Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 18 Aug 2026 00:05:16 +0530 Subject: [PATCH] fix(web-runtime): a handle answers editable for itself, not its container isContentEditable is inherited, so every span inside a contenteditable div called itself typeable. collectTargets and the chrome dump both require the element itself to match; the handle was the one that did not. --- pkg/spec/src/web-runtime.ts | 6 +++++- pkg/spec/test/web-runtime.test.ts | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index dd158ad..833d3c6 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -547,7 +547,11 @@ function elementHandle( // container a spec reached through state.ax claimed to be a tap target. clickable: element.matches(TAPPABLE_SELECTOR), enabled: isEnabled(element), - editable: isEditableElement(element as HTMLElement), + // isContentEditable is inherited, so reading it alone made every span inside + // a contenteditable container typeable here while collectTargets and the + // hierarchy dump, which both require the element ITSELF to match + // EDITABLE_SELECTOR, called the same span inert. + editable: element.matches(EDITABLE_SELECTOR) && isEditableElement(element as HTMLElement), focused: focusedElement === element, // Checkbox and option state lives in the DOM PROPERTY: the markup attribute // records only what the page started with, so a handle reading it reports a diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index fc8d461..f2edde7 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -627,12 +627,13 @@ function domElement(spec: { labels?: string[]; text?: string; checked?: boolean; + contentEditable?: boolean; }): unknown { const attributes = spec.attributes ?? {}; return { tagName: spec.tag.toUpperCase(), type: spec.tag === "input" ? "text" : "", - isContentEditable: false, + isContentEditable: spec.contentEditable ?? false, checked: spec.checked, id: attributes.id ?? "", className: attributes.class ?? "", @@ -777,6 +778,25 @@ test("an element reached through ax reports the tappable selector's clickability ); }); +// isContentEditable is inherited, so the handle called every span inside a +// contenteditable container typeable while the enumeration and the hierarchy +// dump, which both ask whether the element itself matches EDITABLE_SELECTOR, +// called the same span inert. +test("an element reached through ax reports the editable selector's editability", () => { + const editabilityOf = (element: unknown) => handleOf(element).editable; + assert.equal( + editabilityOf( + domElement({ tag: "div", attributes: { id: "note", contenteditable: "" }, contentEditable: true }), + ), + true, + ); + assert.equal( + editabilityOf(domElement({ tag: "span", attributes: { id: "word" }, contentEditable: true })), + false, + ); + assert.equal(editabilityOf(domElement({ tag: "textarea", attributes: { id: "memo" } })), true); +}); + test("a non-editable element carries no hintText", () => { const element = domElement({ tag: "button", attributes: { id: "txn-submit" }, text: "Add credit" }); assert.equal(attrsOf(element).hintText, undefined);