mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 19:47:10 +00:00
Selector matching is written once per runtime: internal/hierarchy over the dump, web-runtime.ts over the DOM. Nothing made the two agree, and a selector that resolves on one and not the other is silent, since an empty match yields no action and the run still passes.
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
//go:build browser
|
|
|
|
package chrome
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/chromedp/chromedp"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/bundler"
|
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
|
)
|
|
|
|
// One page, two selector matchers.
|
|
//
|
|
// A selector means whatever the runtime resolving it decides. The goja host
|
|
// resolves it against the hierarchy dump through internal/hierarchy; the V8 host
|
|
// resolves it against the live DOM through pkg/spec/src/web-runtime.ts. Nothing
|
|
// makes the two agree, so a spec that targets a list of rows can act on them on
|
|
// one platform and find nothing on the other, and finding nothing is silent: the
|
|
// generator yields no action and the run still passes.
|
|
//
|
|
// This test starts from one real page in a real browser and asks both matchers
|
|
// the same questions.
|
|
func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(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)
|
|
}
|
|
installSelectorProbe(ctx, t, d)
|
|
|
|
// 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.
|
|
cases := []struct {
|
|
selector string
|
|
want []string
|
|
}{
|
|
{"idPrefix:customer_row_", []string{"customer_row_a1", "customer_row_b2"}},
|
|
{"idPrefix:customer_", []string{"customer_list", "customer_row_a1", "customer_row_b2"}},
|
|
{"idPrefix:invoice_row_", nil},
|
|
{"id:summary_card", []string{"summary_card"}},
|
|
{"descPrefix:customer_row_", []string{"customer_row_a1", "customer_row_b2"}},
|
|
}
|
|
for _, testCase := range cases {
|
|
t.Run(testCase.selector, func(t *testing.T) {
|
|
native := selectorIDsFromDump(tree, testCase.selector)
|
|
web := selectorIDsFromWebRuntime(ctx, t, d, testCase.selector)
|
|
if !slices.Equal(native, testCase.want) {
|
|
t.Errorf("hierarchy matched %v, want %v", native, testCase.want)
|
|
}
|
|
if !slices.Equal(web, testCase.want) {
|
|
t.Errorf("the web runtime matched %v, want %v", web, testCase.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func selectorIDsFromDump(tree *hierarchy.Tree, selector string) []string {
|
|
var ids []string
|
|
for _, element := range tree.FindAll(selector) {
|
|
ids = append(ids, element.ResourceID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func installSelectorProbe(ctx context.Context, t *testing.T, d *Driver) {
|
|
t.Helper()
|
|
specSource := filepath.Join(repoRootDir(t), "pkg", "spec")
|
|
probe, err := bundler.BundleWeb(bundler.WebOptions{
|
|
EntryFile: filepath.Join(specSource, "test", "dom-selector-probe.ts"),
|
|
WebRuntimeFile: filepath.Join(specSource, "src", "web-runtime.ts"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bundle dom selector probe: %v", err)
|
|
}
|
|
if err := d.InstallBundle(ctx, probe.JavaScript); err != nil {
|
|
t.Fatalf("install dom selector probe: %v", err)
|
|
}
|
|
}
|
|
|
|
func selectorIDsFromWebRuntime(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
d *Driver,
|
|
selector string,
|
|
) []string {
|
|
t.Helper()
|
|
request, err := json.Marshal(selector)
|
|
if err != nil {
|
|
t.Fatalf("encode selector: %v", err)
|
|
}
|
|
var encoded string
|
|
script := `JSON.stringify(window.__sanderlingSelectorMatches__(` + string(request) + `))`
|
|
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &encoded)); err != nil {
|
|
t.Fatalf("read web runtime matches for %q: %v", selector, err)
|
|
}
|
|
var ids []string
|
|
if err := json.Unmarshal([]byte(encoded), &ids); err != nil {
|
|
t.Fatalf("decode web runtime matches: %v", err)
|
|
}
|
|
return ids
|
|
}
|