fix(chrome): emit data-testid so both resolvers name the same element

The V8 host names a web target by data-testid and TapSelector translates that
selector into a CSS attribute match, but the dump carried no such attribute
and no alias could supply one, since an alias only redirects to a key that
already holds the value. tree.Find was therefore always nil for exactly the
selectors examples/folio-web tags with.

Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
pj committed 2026-08-15 13:59:17 +05:30
1 parent 231a74b876
commit 6d03c5788a
3 files changed
+65 -3

No files matched your search

@@ -96,6 +96,12 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
object: objectSelector("desc", "customer_row_a1"),
want: []string{"customer_row_a1"},
},
{
name: "data-testid",
selector: "data-testid:customer-row",
object: objectSelector("data-testid", "customer-row"),
want: []string{"customer_row_a1", "customer_row_b2"},
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
@@ -123,6 +129,56 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
}
}
// A third resolver reads the same selector: TapSelector hands it to CDP as the
// CSS TranslateStringSelector builds. The runner uses both within one InputText
// step, resolving the target in the dump and tapping it over CDP, so a selector
// the two read differently taps one element and reads the text of another.
func TestSelectors_DataTestIDNamesTheSameElementInTheDumpAndOverCDP(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL+"/selector-parity.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
}
tree, err := hierarchy.Parse(dump)
if err != nil {
t.Fatalf("parse hierarchy: %v", err)
}
const selector = "data-testid:summary"
element := tree.Find(selector)
if element == nil {
t.Fatalf("the dump resolves %s to nothing, so every step that names a target this way loses it", selector)
}
css, isXPath, err := TranslateStringSelector(selector)
if err != nil {
t.Fatalf("TranslateStringSelector(%q): %v", selector, err)
}
if isXPath {
t.Fatalf("TranslateStringSelector(%q) returned an XPath, want CSS", selector)
}
var overCDP string
script := `(document.querySelector(` + jsArgument(css) + `) || {}).id || ""`
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &overCDP)); err != nil {
t.Fatalf("resolve %q over CDP: %v", css, err)
}
if overCDP == "" {
t.Fatalf("the CDP selector %q matched nothing", css)
}
if element.ResourceID != overCDP {
t.Errorf("the dump resolves %s to %q, the CDP selector %q to %q",
selector, element.ResourceID, css, overCDP)
}
}
func objectSelector(key, value string) hierarchy.Selector {
return hierarchy.Selector{Filters: []hierarchy.AttrFilter{{Attr: key, Value: value}}}
}