diff --git a/internal/driver/chrome/selector_parity_test.go b/internal/driver/chrome/selector_parity_test.go new file mode 100644 index 0000000..6d4f1a9 --- /dev/null +++ b/internal/driver/chrome/selector_parity_test.go @@ -0,0 +1,124 @@ +//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 +} diff --git a/internal/driver/chrome/testdata/selector-parity.html b/internal/driver/chrome/testdata/selector-parity.html new file mode 100644 index 0000000..2892f46 --- /dev/null +++ b/internal/driver/chrome/testdata/selector-parity.html @@ -0,0 +1,15 @@ + + +
+ +