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

+6
View File
@@ -456,6 +456,12 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
const text = (el.textContent || '').trim().slice(0, 200);
if (text) attrs.text = text;
if (el.id) attrs['resource-id'] = el.id;
// The V8 host names a target by data-testid (IDENTITY_KEYS in
// pkg/spec/src/web-runtime.ts) and TapSelector translates the selector into
// a CSS attribute match, so a dump without this attribute leaves the goja
// host unable to resolve a target the other two resolve fine.
const testid = el.getAttribute('data-testid');
if (testid) attrs['data-testid'] = testid;
const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || '';
if (label) attrs['content-desc'] = label;
const tag = (el.tagName || '').toLowerCase();
@@ -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}}}
}
+3 -3
View File
@@ -5,10 +5,10 @@
<title id="page-title">selector parity</title>
</head>
<body id="page-body">
<div id="summary_card" aria-label="summary_card, 3 customers">summary</div>
<div id="summary_card" data-testid="summary" aria-label="summary_card, 3 customers">summary</div>
<div id="customer_list" aria-label="customer_list">
<div id="customer_row_a1" aria-label="customer_row_a1, Alice">Alice</div>
<div id="customer_row_b2" aria-label="customer_row_b2, Bob">Bob</div>
<div id="customer_row_a1" data-testid="customer-row" aria-label="customer_row_a1, Alice">Alice</div>
<div id="customer_row_b2" data-testid="customer-row" aria-label="customer_row_b2, Bob">Bob</div>
<div id="supplier_row_c3" aria-label="supplier_row_c3, Carol">Carol</div>
</div>
</body>