test(chrome): drive the live-page parity test through both selector forms

This commit is contained in:
pj committed 2026-08-13 00:41:23 +05:30
1 parent cb880edf44
commit 8a98915118
2 files changed
+92 -13

No files matched your search

+90 -11
View File
@@ -9,6 +9,7 @@ import (
"net/http/httptest" "net/http/httptest"
"path/filepath" "path/filepath"
"slices" "slices"
"strings"
"testing" "testing"
"time" "time"
@@ -54,17 +55,50 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
// want is stated here rather than derived, so a matcher that stops matching // want is stated here rather than derived, so a matcher that stops matching
// on both sides at once cannot pass this by agreeing on nothing. // on both sides at once cannot pass this by agreeing on nothing.
cases := []struct { cases := []struct {
name string
selector string selector string
object hierarchy.Selector
want []string want []string
}{ }{
{"idPrefix:customer_row_", []string{"customer_row_a1", "customer_row_b2"}}, {
{"idPrefix:customer_", []string{"customer_list", "customer_row_a1", "customer_row_b2"}}, name: "idPrefix",
{"idPrefix:invoice_row_", nil}, selector: "idPrefix:customer_row_",
{"id:summary_card", []string{"summary_card"}}, object: objectSelector("idPrefix", "customer_row_"),
{"descPrefix:customer_row_", []string{"customer_row_a1", "customer_row_b2"}}, want: []string{"customer_row_a1", "customer_row_b2"},
},
{
name: "idPrefix spanning the container",
selector: "idPrefix:customer_",
object: objectSelector("idPrefix", "customer_"),
want: []string{"customer_list", "customer_row_a1", "customer_row_b2"},
},
{
name: "idPrefix matching nothing",
selector: "idPrefix:invoice_row_",
object: objectSelector("idPrefix", "invoice_row_"),
want: nil,
},
{
name: "id",
selector: "id:summary_card",
object: objectSelector("id", "summary_card"),
want: []string{"summary_card"},
},
{
name: "descPrefix",
selector: "descPrefix:customer_row_",
object: objectSelector("descPrefix", "customer_row_"),
want: []string{"customer_row_a1", "customer_row_b2"},
},
{
name: "desc",
selector: "desc:customer_row_a1",
object: objectSelector("desc", "customer_row_a1"),
want: []string{"customer_row_a1"},
},
} }
for _, testCase := range cases { for _, testCase := range cases {
t.Run(testCase.selector, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
native := selectorIDsFromDump(tree, testCase.selector) native := selectorIDsFromDump(tree, testCase.selector)
web := selectorIDsFromWebRuntime(ctx, t, d, testCase.selector) web := selectorIDsFromWebRuntime(ctx, t, d, testCase.selector)
if !slices.Equal(native, testCase.want) { if !slices.Equal(native, testCase.want) {
@@ -73,10 +107,59 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
if !slices.Equal(web, testCase.want) { if !slices.Equal(web, testCase.want) {
t.Errorf("the web runtime matched %v, want %v", web, testCase.want) t.Errorf("the web runtime matched %v, want %v", web, testCase.want)
} }
// The object form has to mean the same thing as the string form on
// both sides. It is the form the typed API pushes authors toward and
// the one that used to fall through to a raw attribute lookup.
nativeObject := selectorIDsFromDumpObject(tree, testCase.object)
webObject := selectorIDsFromWebRuntime(ctx, t, d, objectSelectorJSON(testCase.object))
if !slices.Equal(nativeObject, testCase.want) {
t.Errorf("hierarchy object form matched %v, want %v", nativeObject, testCase.want)
}
if !slices.Equal(webObject, testCase.want) {
t.Errorf("the web runtime object form matched %v, want %v", webObject, testCase.want)
}
}) })
} }
} }
func objectSelector(key, value string) hierarchy.Selector {
return hierarchy.Selector{Filters: []hierarchy.AttrFilter{{Attr: key, Value: value}}}
}
func objectSelectorJSON(sel hierarchy.Selector) string {
fields := map[string]string{}
for _, filter := range sel.Filters {
fields[filter.Attr] = filter.Value
}
encoded, err := json.Marshal(fields)
if err != nil {
panic(err)
}
return string(encoded)
}
func selectorIDsFromDumpObject(tree *hierarchy.Tree, sel hierarchy.Selector) []string {
var ids []string
for _, node := range tree.Root.FindAllBySelector(sel) {
ids = append(ids, node.Element.ResourceID)
}
return ids
}
// jsArgument passes an object selector through as the JS object literal it
// already is, and quotes anything else as a string selector.
func jsArgument(selector string) string {
if strings.HasPrefix(selector, "{") {
return selector
}
quoted, err := json.Marshal(selector)
if err != nil {
return `""`
}
return string(quoted)
}
func selectorIDsFromDump(tree *hierarchy.Tree, selector string) []string { func selectorIDsFromDump(tree *hierarchy.Tree, selector string) []string {
var ids []string var ids []string
for _, element := range tree.FindAll(selector) { for _, element := range tree.FindAll(selector) {
@@ -107,12 +190,8 @@ func selectorIDsFromWebRuntime(
selector string, selector string,
) []string { ) []string {
t.Helper() t.Helper()
request, err := json.Marshal(selector)
if err != nil {
t.Fatalf("encode selector: %v", err)
}
var encoded string var encoded string
script := `JSON.stringify(window.__sanderlingSelectorMatches__(` + string(request) + `))` script := `JSON.stringify(window.__sanderlingSelectorMatches__(` + jsArgument(selector) + `))`
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &encoded)); err != nil { if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &encoded)); err != nil {
t.Fatalf("read web runtime matches for %q: %v", selector, err) t.Fatalf("read web runtime matches for %q: %v", selector, err)
} }
+2 -2
View File
@@ -21,13 +21,13 @@ interface Ax {
findAll(selector: unknown): Handle[]; findAll(selector: unknown): Handle[];
} }
function selectorMatches(selector: string): string[] { function selectorMatches(selector: unknown): string[] {
const ax = buildAx() as Ax; const ax = buildAx() as Ax;
return ax.findAll(selector).map((element) => element.id ?? ""); return ax.findAll(selector).map((element) => element.id ?? "");
} }
type SelectorGlobal = { type SelectorGlobal = {
__sanderlingSelectorMatches__: (selector: string) => string[]; __sanderlingSelectorMatches__: (selector: unknown) => string[];
}; };
(globalThis as unknown as SelectorGlobal).__sanderlingSelectorMatches__ = selectorMatches; (globalThis as unknown as SelectorGlobal).__sanderlingSelectorMatches__ = selectorMatches;