mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
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:
1 parent
231a74b876
commit
6d03c5788a
3 files changed
+65
-3
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user