mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(replay-ui): the selector key list comes from the cross-runtime contract
The hand-copied list carried textPrefix and classPrefix, which are not keys any runtime emits, and omitted testTag, data-testid, identifier and accessibilityIdentifier. testTag is the primary Android identification route, so the most common action rendered as raw text. Importing pkg/spec/test/fixtures/selector-keys.json leaves no second copy to drift.
This commit is contained in:
1 parent
4619eb8bfb
commit
52625d95a7
2 files changed
+81
-14
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
|
SELECTOR_KEYS,
|
||||||
formatActionRow,
|
formatActionRow,
|
||||||
formatElapsed,
|
formatElapsed,
|
||||||
parseSelector,
|
parseSelector,
|
||||||
@@ -7,6 +9,34 @@ import {
|
|||||||
} from "../lib/action-format";
|
} from "../lib/action-format";
|
||||||
import { summary } from "./fixtures";
|
import { summary } from "./fixtures";
|
||||||
|
|
||||||
|
const contractPath = new URL(
|
||||||
|
"../../../pkg/spec/test/fixtures/selector-keys.json",
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
const contract: { keys: string[] } = JSON.parse(
|
||||||
|
readFileSync(contractPath, "utf8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The runtimes reject a selector key they do not know, so the key list the UI
|
||||||
|
// reads rows against has to be the same list. A key the UI omits renders a real
|
||||||
|
// action as raw text; a key only the UI carries formats a selector no runtime
|
||||||
|
// could ever emit. internal/hierarchy/selector_keys_test.go and
|
||||||
|
// pkg/spec/test/selector-keys.test.ts assert the SAME file.
|
||||||
|
describe("selector keys", () => {
|
||||||
|
it("is the cross-runtime list", () => {
|
||||||
|
expect([...SELECTOR_KEYS]).toEqual(contract.keys);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses every key the runtimes accept", () => {
|
||||||
|
for (const key of contract.keys) {
|
||||||
|
expect(parseSelector(`${key}:value`)).toEqual({
|
||||||
|
kind: key,
|
||||||
|
value: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Bug class: selector parsing mislabels every action row. Splitting on the
|
// Bug class: selector parsing mislabels every action row. Splitting on the
|
||||||
// wrong colon, treating a value-with-colon as the kind, or dropping the prefix
|
// wrong colon, treating a value-with-colon as the kind, or dropping the prefix
|
||||||
// ellipsis would render the wrong target tag for every step.
|
// ellipsis would render the wrong target tag for every step.
|
||||||
@@ -14,13 +44,31 @@ describe("parseSelector", () => {
|
|||||||
const cases: { input: string; out: ReturnType<typeof parseSelector> }[] = [
|
const cases: { input: string; out: ReturnType<typeof parseSelector> }[] = [
|
||||||
{ input: "id:login", out: { kind: "id", value: "login" } },
|
{ input: "id:login", out: { kind: "id", value: "login" } },
|
||||||
{ input: "text:Sign In", out: { kind: "text", value: "Sign In" } },
|
{ input: "text:Sign In", out: { kind: "text", value: "Sign In" } },
|
||||||
{ input: "textPrefix:Hello", out: { kind: "textPrefix", value: "Hello" } },
|
{
|
||||||
|
input: "testTag:AccountCard",
|
||||||
|
out: { kind: "testTag", value: "AccountCard" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "data-testid:ledger-row",
|
||||||
|
out: { kind: "data-testid", value: "ledger-row" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "accessibilityIdentifier:pay_now",
|
||||||
|
out: { kind: "accessibilityIdentifier", value: "pay_now" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "identifier:pay_now",
|
||||||
|
out: { kind: "identifier", value: "pay_now" },
|
||||||
|
},
|
||||||
|
{ input: "descPrefix:Hello", out: { kind: "descPrefix", value: "Hello" } },
|
||||||
{
|
{
|
||||||
input: "idPrefix:customer_row_",
|
input: "idPrefix:customer_row_",
|
||||||
out: { kind: "idPrefix", value: "customer_row_" },
|
out: { kind: "idPrefix", value: "customer_row_" },
|
||||||
},
|
},
|
||||||
{ input: "id:com.app:id/btn", out: { kind: "id", value: "com.app:id/btn" } },
|
{ input: "id:com.app:id/btn", out: { kind: "id", value: "com.app:id/btn" } },
|
||||||
{ input: "bogus:x", out: null },
|
{ input: "bogus:x", out: null },
|
||||||
|
{ input: "textPrefix:Hello", out: null },
|
||||||
|
{ input: "classPrefix:android.widget", out: null },
|
||||||
{ input: ":leading", out: null },
|
{ input: ":leading", out: null },
|
||||||
{ input: "no-colon", out: null },
|
{ input: "no-colon", out: null },
|
||||||
];
|
];
|
||||||
@@ -33,9 +81,10 @@ describe("parseSelector", () => {
|
|||||||
|
|
||||||
describe("tagFromSelector", () => {
|
describe("tagFromSelector", () => {
|
||||||
it("appends ellipsis only for prefix selectors", () => {
|
it("appends ellipsis only for prefix selectors", () => {
|
||||||
expect(tagFromSelector("textPrefix:Hel")).toBe("Hel...");
|
expect(tagFromSelector("descPrefix:Hel")).toBe("Hel...");
|
||||||
expect(tagFromSelector("idPrefix:customer_row_")).toBe("customer_row_...");
|
expect(tagFromSelector("idPrefix:customer_row_")).toBe("customer_row_...");
|
||||||
expect(tagFromSelector("text:Hello")).toBe("Hello");
|
expect(tagFromSelector("text:Hello")).toBe("Hello");
|
||||||
|
expect(tagFromSelector("testTag:AccountCard")).toBe("AccountCard");
|
||||||
expect(tagFromSelector("plain")).toBe("plain");
|
expect(tagFromSelector("plain")).toBe("plain");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -63,6 +112,30 @@ describe("formatActionRow", () => {
|
|||||||
).toEqual({ verb: "Click", target: "(10, 20)", targetIsTag: false });
|
).toEqual({ verb: "Click", target: "(10, 20)", targetIsTag: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// internal/verifier/worker.go labels android and ios taps with testTag,
|
||||||
|
// identifier and accessibilityIdentifier; pkg/spec/src/web-runtime.ts labels
|
||||||
|
// web taps with data-testid. testTag is the commonest label of the four.
|
||||||
|
it("formats the selector keys the verifier and the web runtime actually emit", () => {
|
||||||
|
expect(
|
||||||
|
formatActionRow(
|
||||||
|
summary({ action_kind: "Tap", action_label: "testTag:AccountCard" }),
|
||||||
|
),
|
||||||
|
).toEqual({ verb: "Click", target: "AccountCard", targetIsTag: true });
|
||||||
|
expect(
|
||||||
|
formatActionRow(
|
||||||
|
summary({ action_kind: "Tap", action_label: "data-testid:ledger-row" }),
|
||||||
|
),
|
||||||
|
).toEqual({ verb: "Click", target: "ledger-row", targetIsTag: true });
|
||||||
|
expect(
|
||||||
|
formatActionRow(
|
||||||
|
summary({
|
||||||
|
action_kind: "Tap",
|
||||||
|
action_label: "accessibilityIdentifier:pay_now",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual({ verb: "Click", target: "pay_now", targetIsTag: true });
|
||||||
|
});
|
||||||
|
|
||||||
it("maps known verbs and falls back to the raw kind", () => {
|
it("maps known verbs and falls back to the raw kind", () => {
|
||||||
expect(formatActionRow(summary({ action_kind: "InputText", action_label: "hi" })).verb).toBe("Type");
|
expect(formatActionRow(summary({ action_kind: "InputText", action_label: "hi" })).verb).toBe("Type");
|
||||||
expect(formatActionRow(summary({ action_kind: "Swipe" })).verb).toBe("Swipe");
|
expect(formatActionRow(summary({ action_kind: "Swipe" })).verb).toBe("Swipe");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import selectorKeysContract from "../../../pkg/spec/test/fixtures/selector-keys.json";
|
||||||
import type { StepSummary } from "../types";
|
import type { StepSummary } from "../types";
|
||||||
|
|
||||||
export interface FormattedRow {
|
export interface FormattedRow {
|
||||||
@@ -6,17 +7,10 @@ export interface FormattedRow {
|
|||||||
targetIsTag: boolean;
|
targetIsTag: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SELECTOR_PREFIXES = [
|
// Imported from the cross-runtime contract rather than restated, because a
|
||||||
"id",
|
// copy here goes stale silently: an unlisted key renders a real action as raw
|
||||||
"idPrefix",
|
// text instead of a formatted row, and nothing fails to tell anyone.
|
||||||
"text",
|
export const SELECTOR_KEYS: readonly string[] = selectorKeysContract.keys;
|
||||||
"textPrefix",
|
|
||||||
"desc",
|
|
||||||
"descPrefix",
|
|
||||||
"class",
|
|
||||||
"classPrefix",
|
|
||||||
"package",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function parseSelector(
|
export function parseSelector(
|
||||||
selector: string,
|
selector: string,
|
||||||
@@ -27,7 +21,7 @@ export function parseSelector(
|
|||||||
}
|
}
|
||||||
const kind = selector.slice(0, colonIndex);
|
const kind = selector.slice(0, colonIndex);
|
||||||
const value = selector.slice(colonIndex + 1);
|
const value = selector.slice(colonIndex + 1);
|
||||||
if (!SELECTOR_PREFIXES.includes(kind)) {
|
if (!SELECTOR_KEYS.includes(kind)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { kind, value };
|
return { kind, value };
|
||||||
|
|||||||
Reference in new issue
Block a user