mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
test(chrome): compare both selector matchers over one live page
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.
This commit is contained in:
1 parent
bf3f230b1b
commit
c77b78d63b
4 files changed
+173
No files matched your search
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html id="page" lang="en">
|
||||||
|
<head id="page-head">
|
||||||
|
<meta id="page-charset" charset="utf-8" />
|
||||||
|
<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="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="supplier_row_c3" aria-label="supplier_row_c3, Carol">Carol</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -589,6 +589,7 @@ installRuntime(
|
|||||||
// so these are stripped from production output; they only exist for unit tests.
|
// so these are stripped from production output; they only exist for unit tests.
|
||||||
export const __testing__ = {
|
export const __testing__ = {
|
||||||
host,
|
host,
|
||||||
|
buildAx,
|
||||||
seedBigInt,
|
seedBigInt,
|
||||||
collectTargets,
|
collectTargets,
|
||||||
targetElements,
|
targetElements,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/// <reference lib="dom" />
|
||||||
|
|
||||||
|
// The V8 host's selector matcher, read back match by match.
|
||||||
|
//
|
||||||
|
// internal/driver/chrome/selector_parity_test.go bundles this probe into a live
|
||||||
|
// page and compares what it returns against what internal/hierarchy resolves
|
||||||
|
// from the dump of the SAME page. Selector matching is written once per runtime,
|
||||||
|
// and only a comparison over one page can say whether the two mean the same
|
||||||
|
// thing by one selector. buildAx is the shipped `state.ax`, so the answer comes
|
||||||
|
// from production code rather than a copy of it.
|
||||||
|
|
||||||
|
import { __testing__ } from "../src/web-runtime.ts";
|
||||||
|
|
||||||
|
const { buildAx } = __testing__;
|
||||||
|
|
||||||
|
interface Handle {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Ax {
|
||||||
|
findAll(selector: unknown): Handle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectorMatches(selector: string): string[] {
|
||||||
|
const ax = buildAx() as Ax;
|
||||||
|
return ax.findAll(selector).map((element) => element.id ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectorGlobal = {
|
||||||
|
__sanderlingSelectorMatches__: (selector: string) => string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
(globalThis as unknown as SelectorGlobal).__sanderlingSelectorMatches__ = selectorMatches;
|
||||||
Reference in new issue
Block a user