mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07: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 { readFileSync } from "node:fs";
|
||||
import {
|
||||
SELECTOR_KEYS,
|
||||
formatActionRow,
|
||||
formatElapsed,
|
||||
parseSelector,
|
||||
@@ -7,6 +9,34 @@ import {
|
||||
} from "../lib/action-format";
|
||||
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
|
||||
// wrong colon, treating a value-with-colon as the kind, or dropping the prefix
|
||||
// ellipsis would render the wrong target tag for every step.
|
||||
@@ -14,13 +44,31 @@ describe("parseSelector", () => {
|
||||
const cases: { input: string; out: ReturnType<typeof parseSelector> }[] = [
|
||||
{ input: "id:login", out: { kind: "id", value: "login" } },
|
||||
{ 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_",
|
||||
out: { kind: "idPrefix", value: "customer_row_" },
|
||||
},
|
||||
{ input: "id:com.app:id/btn", out: { kind: "id", value: "com.app:id/btn" } },
|
||||
{ input: "bogus:x", out: null },
|
||||
{ input: "textPrefix:Hello", out: null },
|
||||
{ input: "classPrefix:android.widget", out: null },
|
||||
{ input: ":leading", out: null },
|
||||
{ input: "no-colon", out: null },
|
||||
];
|
||||
@@ -33,9 +81,10 @@ describe("parseSelector", () => {
|
||||
|
||||
describe("tagFromSelector", () => {
|
||||
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("text:Hello")).toBe("Hello");
|
||||
expect(tagFromSelector("testTag:AccountCard")).toBe("AccountCard");
|
||||
expect(tagFromSelector("plain")).toBe("plain");
|
||||
});
|
||||
});
|
||||
@@ -63,6 +112,30 @@ describe("formatActionRow", () => {
|
||||
).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", () => {
|
||||
expect(formatActionRow(summary({ action_kind: "InputText", action_label: "hi" })).verb).toBe("Type");
|
||||
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";
|
||||
|
||||
export interface FormattedRow {
|
||||
@@ -6,17 +7,10 @@ export interface FormattedRow {
|
||||
targetIsTag: boolean;
|
||||
}
|
||||
|
||||
const SELECTOR_PREFIXES = [
|
||||
"id",
|
||||
"idPrefix",
|
||||
"text",
|
||||
"textPrefix",
|
||||
"desc",
|
||||
"descPrefix",
|
||||
"class",
|
||||
"classPrefix",
|
||||
"package",
|
||||
];
|
||||
// Imported from the cross-runtime contract rather than restated, because a
|
||||
// copy here goes stale silently: an unlisted key renders a real action as raw
|
||||
// text instead of a formatted row, and nothing fails to tell anyone.
|
||||
export const SELECTOR_KEYS: readonly string[] = selectorKeysContract.keys;
|
||||
|
||||
export function parseSelector(
|
||||
selector: string,
|
||||
@@ -27,7 +21,7 @@ export function parseSelector(
|
||||
}
|
||||
const kind = selector.slice(0, colonIndex);
|
||||
const value = selector.slice(colonIndex + 1);
|
||||
if (!SELECTOR_PREFIXES.includes(kind)) {
|
||||
if (!SELECTOR_KEYS.includes(kind)) {
|
||||
return null;
|
||||
}
|
||||
return { kind, value };
|
||||
|
||||
Reference in new issue
Block a user