diff --git a/internal/driver/chrome/translate.go b/internal/driver/chrome/translate.go index 561e09f..091c0a7 100644 --- a/internal/driver/chrome/translate.go +++ b/internal/driver/chrome/translate.go @@ -42,7 +42,13 @@ func TranslateStringSelector(selector string) (string, bool, error) { return `[aria-label="` + cssEscape(value) + `"]`, false, nil case "descPrefix": return `[aria-label^="` + cssEscape(value) + `"]`, false, nil - case "testTag", "testID", "testid", "data-testid": + case "testTag": + // Mirrors the in-page table and the native resource-id alias: a + // testTag reaches the DOM as data-testid or as an id, depending on + // the toolkit. `:is()` keeps this one compound selector. + escaped := cssEscape(value) + return `:is([data-testid="` + escaped + `"], [id="` + escaped + `"])`, false, nil + case "testID", "testid", "data-testid": return `[data-testid="` + cssEscape(value) + `"]`, false, nil case "placeholder", "placeholderValue", "hintText": return `[placeholder="` + cssEscape(value) + `"]`, false, nil diff --git a/internal/driver/chrome/translate_test.go b/internal/driver/chrome/translate_test.go index f89be06..79b6931 100644 --- a/internal/driver/chrome/translate_test.go +++ b/internal/driver/chrome/translate_test.go @@ -21,7 +21,7 @@ func TestTranslateStringSelector_KnownKeys(t *testing.T) { {"accessibilityLabel:logout", `[aria-label="logout"]`, false}, {"aria-label:Sign in", `[aria-label="Sign in"]`, false}, {"descPrefix:account:", `[aria-label^="account:"]`, false}, - {"testTag:submit", `[data-testid="submit"]`, false}, + {"testTag:submit", `:is([data-testid="submit"], [id="submit"])`, false}, {"testID:submit", `[data-testid="submit"]`, false}, {"placeholder:Email", `[placeholder="Email"]`, false}, } diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index ca84236..ae3356a 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -85,7 +85,12 @@ function noopFormula(): unknown { const KNOWN_KEY_TO_CSS: Record string> = { id: (v) => `[id="${cssEscape(v)}"]`, "resource-id": (v) => `[id="${cssEscape(v)}"]`, - testTag: (v) => `[data-testid="${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 + // tables agree. `:is()` keeps this one compound, since a multi-key selector + // concatenates the parts. + testTag: (v) => `:is([data-testid="${cssEscape(v)}"], [id="${cssEscape(v)}"])`, testID: (v) => `[data-testid="${cssEscape(v)}"]`, "data-testid": (v) => `[data-testid="${cssEscape(v)}"]`, className: (v) => `[class~="${cssEscape(v)}"]`, diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 5703719..70c1160 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -310,6 +310,23 @@ test("selectorFromObject maps known keys to their canonical attribute", () => { }); }); +// Compose Multiplatform emits its testTag into `id`, which the native table +// already accepts via the resource-id alias. The web table must not be the one +// place that rejects it. +test("selectorFromObject resolves testTag through data-testid or id", () => { + assert.deepEqual(selectorFromObject({ testTag: "LoginSubmit" }), { + css: `:is([data-testid="LoginSubmit"], [id="LoginSubmit"])`, + }); +}); + +// Multi-key selectors concatenate their parts into one compound, so the +// two-attribute testTag match has to stay a single compound piece. +test("selectorFromObject composes testTag with a second key", () => { + assert.deepEqual(selectorFromObject({ testTag: "Row", "aria-label": "first" }), { + css: `:is([data-testid="Row"], [id="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"]`,