mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17: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);
|
const text = (el.textContent || '').trim().slice(0, 200);
|
||||||
if (text) attrs.text = text;
|
if (text) attrs.text = text;
|
||||||
if (el.id) attrs['resource-id'] = el.id;
|
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') || '';
|
const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || '';
|
||||||
if (label) attrs['content-desc'] = label;
|
if (label) attrs['content-desc'] = label;
|
||||||
const tag = (el.tagName || '').toLowerCase();
|
const tag = (el.tagName || '').toLowerCase();
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
|
|||||||
object: objectSelector("desc", "customer_row_a1"),
|
object: objectSelector("desc", "customer_row_a1"),
|
||||||
want: []string{"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 {
|
for _, testCase := range cases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
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 {
|
func objectSelector(key, value string) hierarchy.Selector {
|
||||||
return hierarchy.Selector{Filters: []hierarchy.AttrFilter{{Attr: key, Value: value}}}
|
return hierarchy.Selector{Filters: []hierarchy.AttrFilter{{Attr: key, Value: value}}}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
<title id="page-title">selector parity</title>
|
<title id="page-title">selector parity</title>
|
||||||
</head>
|
</head>
|
||||||
<body id="page-body">
|
<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_list" aria-label="customer_list">
|
||||||
<div id="customer_row_a1" aria-label="customer_row_a1, Alice">Alice</div>
|
<div id="customer_row_a1" data-testid="customer-row" 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_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 id="supplier_row_c3" aria-label="supplier_row_c3, Carol">Carol</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in new issue
Block a user