diff --git a/internal/driver/chrome/translate.go b/internal/driver/chrome/translate.go index d9f29fb..75ff571 100644 --- a/internal/driver/chrome/translate.go +++ b/internal/driver/chrome/translate.go @@ -40,7 +40,12 @@ func TranslateStringSelector(selector string) (string, bool, error) { return cssEscape(value), false, nil case "text": return `//*[normalize-space(text())=` + xpathStringLiteral(value) + `]`, true, nil - case "desc", "label", "content-desc", "accessibilityLabel", "accessibilityText", "ariaLabel", "aria-label": + case "desc": + // Mirrors the native rule: the label itself, or the label at the head of + // an iOS merged label ("account_card:7, Tim, $100"). + escaped := cssEscape(value) + return `:is([aria-label="` + escaped + `"], [aria-label^="` + escaped + `, "])`, false, nil + case "label", "content-desc", "accessibilityLabel", "accessibilityText", "ariaLabel", "aria-label": return `[aria-label="` + cssEscape(value) + `"]`, false, nil case "descPrefix": return `[aria-label^="` + cssEscape(value) + `"]`, false, nil diff --git a/internal/driver/chrome/translate_test.go b/internal/driver/chrome/translate_test.go index a7a4449..695ba00 100644 --- a/internal/driver/chrome/translate_test.go +++ b/internal/driver/chrome/translate_test.go @@ -16,7 +16,8 @@ func TestTranslateStringSelector_KnownKeys(t *testing.T) { {`text:Say "hi"`, `//*[normalize-space(text())='Say "hi"']`, true}, {`text:it's`, `//*[normalize-space(text())="it's"]`, true}, {`text:it's "fine"`, `//*[normalize-space(text())=concat("it's ", '"', "fine", '"', "")]`, true}, - {"desc:logout", `[aria-label="logout"]`, false}, + // desc also accepts an iOS merged label, the way internal/hierarchy does. + {"desc:logout", `:is([aria-label="logout"], [aria-label^="logout, "])`, false}, {"label:logout", `[aria-label="logout"]`, false}, {"accessibilityLabel:logout", `[aria-label="logout"]`, false}, {"aria-label:Sign in", `[aria-label="Sign in"]`, false}, diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index f4c60f1..e355bc1 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -94,7 +94,10 @@ const KNOWN_KEY_TO_CSS: Record string> = { // 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)}"]`, - desc: (v) => `[aria-label="${cssEscape(v)}"]`, + // The native rule accepts the label itself or the label at the head of an + // iOS merged label ("account_card:7, Tim, $100"). `:is()` keeps that one + // compound piece, since a multi-key selector concatenates the parts. + desc: (v) => `:is([aria-label="${cssEscape(v)}"], [aria-label^="${cssEscape(v)}, "])`, descPrefix: (v) => `[aria-label^="${cssEscape(v)}"]`, // The native table aliases testTag onto resource-id, which the host DOM walk // fills from el.id, so the native path already accepts a testTag emitted as diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 831152c..a2e94f6 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -342,6 +342,14 @@ test("selectorFromObject routes idPrefix to a starts-with id match", () => { }); }); +// The native rule matches an iOS merged label ("account_card:7, Tim, $100") by +// its leading name, and the web table has to mean the same thing by desc. +test("selectorFromObject matches a merged label by its leading name", () => { + assert.deepEqual(selectorFromObject({ desc: "account_card" }), { + css: `:is([aria-label="account_card"], [aria-label^="account_card, "])`, + }); +}); + test("selectorFromString and selectorFromObject agree on descPrefix", () => { assert.deepEqual(selectorFromString("descPrefix:account:"), { css: `[aria-label^="account\\:"]`,