fix(spec): match a merged label by its leading name on web too

The native desc rule accepts the label or the label at the head of an iOS
merged label; both web translators compared the whole string, so the same
selector matched natively and missed on web. The live-page parity test
caught it.
This commit is contained in:
pj committed 2026-08-13 00:41:23 +05:30
1 parent b8bb44cf61
commit cb880edf44
4 files changed
+20 -3

No files matched your search

+6 -1
View File
@@ -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
+2 -1
View File
@@ -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},
+4 -1
View File
@@ -94,7 +94,10 @@ const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
// The native rule also accepts the local name after Android's "<package>: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
+8
View File
@@ -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\\:"]`,