mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(spec): match idPrefix in the web runtime
The DOM has no package prefix, so the native rule reduces to [id^=]. Both prefix kinds now go through the one key table, which drops the separate descPrefix branch that string and object selectors each carried.
This commit is contained in:
1 parent
59975231ce
commit
a8444cc34f
3 files changed
+50
-14
No files matched your search
+16
-3
@@ -44,14 +44,27 @@ export interface KnownAttrSelectors {
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys that name a matching rule rather than an attribute a driver reports.
|
||||
* They belong to the selector surface only, which is why they are not part of
|
||||
* `KnownAttrSelectors` (and so never appear in `RawAttrs`).
|
||||
*/
|
||||
export interface PrefixSelectors {
|
||||
/** Identifier starts with this, after Android's "<package>:id/" if present. */
|
||||
idPrefix?: string;
|
||||
/** Accessibility description starts with this. */
|
||||
descPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object-form selector for `find` / `findAll`. Known attributes are typed
|
||||
* via `KnownAttrSelectors`; arbitrary string keys are still allowed for
|
||||
* raw driver attributes the typed surface doesn't yet cover.
|
||||
*/
|
||||
export type AttrSelector = KnownAttrSelectors & {
|
||||
[key: string]: string | boolean | undefined;
|
||||
};
|
||||
export type AttrSelector = KnownAttrSelectors &
|
||||
PrefixSelectors & {
|
||||
[key: string]: string | boolean | undefined;
|
||||
};
|
||||
|
||||
export type SelectorPath = readonly AttrSelector[];
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ function noopFormula(): unknown {
|
||||
const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
|
||||
id: (v) => `[id="${cssEscape(v)}"]`,
|
||||
"resource-id": (v) => `[id="${cssEscape(v)}"]`,
|
||||
// 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)}"]`,
|
||||
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
|
||||
// an id (what Compose Multiplatform does on web). Accept both here so the
|
||||
@@ -131,7 +135,6 @@ function selectorFromObject(selector: Record<string, string | boolean | undefine
|
||||
} {
|
||||
const parts: string[] = [];
|
||||
let textValue: string | undefined;
|
||||
let descPrefix: string | undefined;
|
||||
for (const key of Object.keys(selector)) {
|
||||
const raw = selector[key];
|
||||
if (raw === undefined) continue;
|
||||
@@ -140,10 +143,6 @@ function selectorFromObject(selector: Record<string, string | boolean | undefine
|
||||
textValue = value;
|
||||
continue;
|
||||
}
|
||||
if (key === "descPrefix") {
|
||||
descPrefix = value;
|
||||
continue;
|
||||
}
|
||||
const builder = KNOWN_KEY_TO_CSS[key];
|
||||
if (builder) {
|
||||
parts.push(builder(value));
|
||||
@@ -151,9 +150,6 @@ function selectorFromObject(selector: Record<string, string | boolean | undefine
|
||||
parts.push(`[${key}="${cssEscape(value)}"]`);
|
||||
}
|
||||
}
|
||||
if (descPrefix !== undefined) {
|
||||
parts.push(`[aria-label^="${cssEscape(descPrefix)}"]`);
|
||||
}
|
||||
if (textValue !== undefined && parts.length === 0) {
|
||||
return {
|
||||
xpath: `//*[normalize-space(text())=${xpathStringLiteral(textValue)}]`,
|
||||
@@ -182,9 +178,6 @@ function selectorFromString(selector: string): { css?: string; xpath?: string }
|
||||
if (kind === "text") {
|
||||
return { xpath: `//*[normalize-space(text())=${xpathStringLiteral(value)}]` };
|
||||
}
|
||||
if (kind === "descPrefix") {
|
||||
return { css: `[aria-label^="${cssEscape(value)}"]` };
|
||||
}
|
||||
return selectorFromObject({ [kind]: value });
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,36 @@ test("selectorFromObject composes testTag with a second key", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// A list whose rows are named <role>_<record id> is only reachable by the role
|
||||
// half. internal/driver/chrome/translate.go builds the same CSS for the same
|
||||
// selector, and internal/hierarchy resolves it against the dump of this page.
|
||||
test("selectorFromString routes idPrefix to a starts-with id match", () => {
|
||||
assert.deepEqual(selectorFromString("idPrefix:customer_row_"), {
|
||||
css: `[id^="customer_row_"]`,
|
||||
});
|
||||
});
|
||||
|
||||
test("selectorFromObject routes idPrefix to a starts-with id match", () => {
|
||||
assert.deepEqual(selectorFromObject({ idPrefix: "customer_row_" }), {
|
||||
css: `[id^="customer_row_"]`,
|
||||
});
|
||||
});
|
||||
|
||||
test("selectorFromString and selectorFromObject agree on descPrefix", () => {
|
||||
assert.deepEqual(selectorFromString("descPrefix:account:"), {
|
||||
css: `[aria-label^="account\\:"]`,
|
||||
});
|
||||
assert.deepEqual(selectorFromObject({ descPrefix: "account:" }), {
|
||||
css: `[aria-label^="account\\:"]`,
|
||||
});
|
||||
});
|
||||
|
||||
test("selectorFromObject composes idPrefix with a second key", () => {
|
||||
assert.deepEqual(selectorFromObject({ idPrefix: "customer_row_", "aria-label": "first" }), {
|
||||
css: `[id^="customer_row_"][aria-label="first"]`,
|
||||
});
|
||||
});
|
||||
|
||||
test("selectorFromObject falls back to a literal attribute for unknown keys", () => {
|
||||
assert.deepEqual(selectorFromObject({ "data-foo": "bar" }), {
|
||||
css: `[data-foo="bar"]`,
|
||||
|
||||
Reference in new issue
Block a user