From c4fbe79016b5ddd3679342018b689c8e8982bc20 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 19 Aug 2026 10:11:54 +0530 Subject: [PATCH] fix(spec): resolve the identifier, label and class aliases against the DOM identifier, accessibilityIdentifier, accessibilityText and elementType are the names ios writes four facts under, and internal/hierarchy aliases each onto the key the other producers write. This table listed none of them, so each fell through to a raw attribute lookup and built [accessibilityIdentifier="summary_card"], which no element carries. Every one of them resolved against the dump on the goja host and named nothing here. The keys are accepted, so no unknown-key error fires, and a property over the element that was never found passes having checked nothing. --- pkg/spec/src/web-runtime.ts | 13 ++++++++++ pkg/spec/test/web-runtime.test.ts | 40 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index b0fe2b4..6b1bbd4 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -91,6 +91,13 @@ function noopFormula(): unknown { const KNOWN_KEY_TO_CSS: Record string> = { id: (v) => `[id="${cssEscape(v)}"]`, "resource-id": (v) => `[id="${cssEscape(v)}"]`, + // The names ios writes the identifier under, which internal/hierarchy aliases + // onto resource-id. Left out of this table they fell through to a raw + // attribute lookup, and no element carries an attribute called + // accessibilityIdentifier, so they resolved against the dump and named + // nothing here. + identifier: (v) => `[id="${cssEscape(v)}"]`, + accessibilityIdentifier: (v) => `[id="${cssEscape(v)}"]`, // The native rule also accepts the local name after Android's ":id/". // The DOM has no such prefix, so a plain starts-with is the same rule here. idPrefix: (v) => `[id^="${cssEscape(v)}"]`, @@ -109,6 +116,9 @@ const KNOWN_KEY_TO_CSS: Record string> = { "data-testid": (v) => `[data-testid="${cssEscape(v)}"]`, className: (v) => `[class~="${cssEscape(v)}"]`, class: (v) => `[class~="${cssEscape(v)}"]`, + // The name ios writes the class under, which internal/hierarchy aliases onto + // class. It read a raw `elementType` attribute here, which nothing carries. + elementType: (v) => `[class~="${cssEscape(v)}"]`, tag: tagSelector, "aria-label": (v) => `[aria-label="${cssEscape(v)}"]`, ariaLabel: (v) => `[aria-label="${cssEscape(v)}"]`, @@ -116,6 +126,9 @@ const KNOWN_KEY_TO_CSS: Record string> = { contentDescription: (v) => `[aria-label="${cssEscape(v)}"]`, "content-desc": (v) => `[aria-label="${cssEscape(v)}"]`, label: (v) => `[aria-label="${cssEscape(v)}"]`, + // The name the ios sidecar writes the label under, and the canonical key + // internal/hierarchy resolves the whole family through. + accessibilityText: (v) => `[aria-label="${cssEscape(v)}"]`, placeholder: (v) => `[placeholder="${cssEscape(v)}"]`, placeholderValue: (v) => `[placeholder="${cssEscape(v)}"]`, hintText: (v) => `[placeholder="${cssEscape(v)}"]`, diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 4b4d18e..93e87f2 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -958,6 +958,46 @@ test("className and class name the same elements", () => { }); }); +// Four more names internal/hierarchy resolves through an alias and this host +// resolved through nothing: they fell through to a raw attribute lookup, and no +// DOM element carries an attribute called accessibilityIdentifier. Each one +// matched on the goja host and NOTHING here, with no unknown-key error to say +// so, so a property over the element that was never found passed having checked +// nothing. Each is pinned beside the key it aliases onto, so a name dropped +// from the table is a name that stops naming the same element. +test("the identifier aliases name the element id names", () => { + const card = fakeElement({ + tag: "div", x: 0, y: 0, width: 120, height: 40, id: "summary_card", text: "summary", + }); + withFakeDocument([card], () => { + assert.deepEqual(matchedIDs({ identifier: "summary_card" }), ["summary_card"]); + assert.deepEqual(matchedIDs({ accessibilityIdentifier: "summary_card" }), ["summary_card"]); + assert.deepEqual(matchedIDs({ "resource-id": "summary_card" }), ["summary_card"]); + }); +}); + +test("accessibilityText names the element the accessible label names", () => { + const email = fakeElement({ + tag: "input", x: 0, y: 0, width: 120, height: 20, id: "login_email", + label: "login_email", editable: true, + }); + withFakeDocument([email], () => { + assert.deepEqual(matchedIDs({ accessibilityText: "login_email" }), ["login_email"]); + assert.deepEqual(matchedIDs({ "aria-label": "login_email" }), ["login_email"]); + }); +}); + +test("elementType names the elements class names", () => { + const badge = fakeElement({ + tag: "span", x: 0, y: 0, width: 40, height: 20, id: "status_badge", + attrs: { class: "status" }, text: "Sent", + }); + withFakeDocument([badge], () => { + assert.deepEqual(matchedIDs({ elementType: "status" }), ["status_badge"]); + assert.deepEqual(matchedIDs({ class: "status" }), ["status_badge"]); + }); +}); + test("attrs carries every other attribute alongside tag and aria-label", () => { const attrs = attrsOf( domElement({ tag: "input", attributes: { id: "txn-note", placeholder: "What's this for?" } }),