test(chrome): one launch preamble, and parity now pins ground truth

48 of 51 call sites shared one eight-line block that opened a fixture server,
launched chrome and checked the error. The three left are the launch tests
themselves, where what the helper hardcodes is the subject.

The audit called four tests superseded by fact_parity_test.go. They were not:
parity asserts the two producers agree and that each fact appears both ways,
never that either reading is right, so deleting them would have removed the only
thing anchoring the pair to reality. The values they pinned are now asserted
inside parity against both producers, and the shadow fixture has ground truth
where it had none.
This commit is contained in:
pj committed 2026-08-22 21:31:13 +05:30
1 parent 5ae825109e
commit 3caf53cfb6
5 files changed
+273 -791

No files matched your search

+76 -560
View File
@@ -23,6 +23,47 @@ import (
"github.com/priyanshujain/sanderling/internal/verifier"
)
// launchChrome starts a browser on target and returns it with the context every
// later driver call must use. The browser, and the deadline that bounds a call
// against a wedged one, are torn down when the test ends.
//
// A test whose subject is the launch itself (a clearState wipe, a caller
// deadline, a browser outliving its caller's context) builds this by hand: the
// flag, the timeout and the cancel are what it is measuring.
func launchChrome(t *testing.T, target string) (*Driver, context.Context) {
t.Helper()
d := New()
t.Cleanup(func() { _ = d.Terminate(context.Background()) })
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
t.Cleanup(cancel)
if err := d.Launch(ctx, target, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
return d, ctx
}
// servePage serves html at a real http origin for the duration of the test. A
// data: URL carries the same markup but has an opaque origin, where storage,
// routing and everything else keyed by origin behaves as it never would in an
// app.
func servePage(t *testing.T, html string) *httptest.Server {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(html))
}))
t.Cleanup(server.Close)
return server
}
// testdataServer serves the fixture pages in testdata, each at its own path.
func testdataServer(t *testing.T) *httptest.Server {
t.Helper()
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
t.Cleanup(server.Close)
return server
}
// TestLaunch_ClearStateWipesStorageForTheTargetOrigin covers the CLI's default
// path (--clear-data). The tab sits on about:blank when Launch runs, an opaque
// origin that denies storage access, so clearing by script there throws
@@ -33,11 +74,7 @@ func TestLaunch_ClearStateWipesStorageForTheTargetOrigin(t *testing.T) {
localStorage.setItem("visits", String(visits));
sessionStorage.setItem("tab", "dirty");
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
server := servePage(t, page)
d := New()
defer d.Terminate(context.Background())
@@ -76,13 +113,7 @@ func TestLaunch_ClearStateWipesStorageForTheTargetOrigin(t *testing.T) {
// refuses the software WebGL backend: getContext returns null, so a
// canvas-rendered app paints nothing and every screenshot is identical black.
func TestLaunch_WebGLContextIsAvailable(t *testing.T) {
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,<body></body>", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, _ := launchChrome(t, "data:text/html,<body></body>")
var hasContext bool
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(
`!!document.createElement("canvas").getContext("webgl2")`, &hasContext)); err != nil {
@@ -98,13 +129,7 @@ func TestLaunch_WebGLContextIsAvailable(t *testing.T) {
// round-trip instead of blocking on d.tabCtx. Without this a hung browser would
// ignore step deadlines and Ctrl-C.
func TestActionMethods_HonorCallerCancellation(t *testing.T) {
d := New()
defer d.Terminate(context.Background())
launchCtx, launchCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer launchCancel()
if err := d.Launch(launchCtx, "data:text/html,<body><button id=go>go</button></body>", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, _ := launchChrome(t, "data:text/html,<body><button id=go>go</button></body>")
cancelled, cancel := context.WithCancel(context.Background())
cancel()
@@ -134,185 +159,6 @@ func TestActionMethods_HonorCallerCancellation(t *testing.T) {
}
}
// TestHierarchy_EditableFlag confirms the injected hierarchy script marks text
// inputs, textareas, and contenteditable elements editable while leaving
// buttons and non-text inputs alone.
func TestHierarchy_EditableFlag(t *testing.T) {
const html = `<body>` +
`<input id="name">` +
`<textarea id="bio"></textarea>` +
`<button id="go">go</button>` +
`<div id="rich" contenteditable="true">x</div>` +
`<input id="chk" type="checkbox">` +
`</body>`
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,"+html, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
}
type node struct {
Attributes map[string]string `json:"attributes"`
Children []node `json:"children"`
Editable *bool `json:"editable"`
}
var root node
if err := json.Unmarshal([]byte(dump), &root); err != nil {
t.Fatalf("unmarshal hierarchy: %v", err)
}
editableByID := map[string]*bool{}
var walk func(n node)
walk = func(n node) {
if id := n.Attributes["resource-id"]; id != "" {
editableByID[id] = n.Editable
}
for _, c := range n.Children {
walk(c)
}
}
walk(root)
isEditable := func(id string) bool {
return editableByID[id] != nil && *editableByID[id]
}
for _, id := range []string{"name", "bio", "rich"} {
if !isEditable(id) {
t.Errorf("%q: editable = %v, want true", id, editableByID[id])
}
}
for _, id := range []string{"go", "chk"} {
if isEditable(id) {
t.Errorf("%q: editable = true, want false/absent", id)
}
}
}
// TestHierarchy_ClickableMatchesTheWebRuntimeSelector pins clickable to the
// same membership test pkg/spec/src/web-runtime.ts applies. The dump used to
// test el.onclick, which React sets on its root container for event delegation,
// so the whole viewport became a tap target in this dump and in no other
// enumeration of the same page.
func TestHierarchy_ClickableMatchesTheWebRuntimeSelector(t *testing.T) {
const html = `<body>` +
`<div id="root"><button id="go">go</button></div>` +
`<textarea id="bio"></textarea>` +
`<div id="plain">text</div>` +
`<div id="rolebutton" role="button">act</div>` +
`<script>document.getElementById("root").onclick = function () {};</script>` +
`</body>`
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,"+html, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
}
type node struct {
Attributes map[string]string `json:"attributes"`
Children []node `json:"children"`
Clickable *bool `json:"clickable"`
}
var root node
if err := json.Unmarshal([]byte(dump), &root); err != nil {
t.Fatalf("unmarshal hierarchy: %v", err)
}
clickableByID := map[string]*bool{}
var walk func(n node)
walk = func(n node) {
if id := n.Attributes["resource-id"]; id != "" {
clickableByID[id] = n.Clickable
}
for _, c := range n.Children {
walk(c)
}
}
walk(root)
isClickable := func(id string) bool {
return clickableByID[id] != nil && *clickableByID[id]
}
for _, id := range []string{"go", "bio", "rolebutton"} {
if !isClickable(id) {
t.Errorf("%q: clickable = %v, want true", id, clickableByID[id])
}
}
for _, id := range []string{"root", "plain"} {
if isClickable(id) {
t.Errorf("%q: clickable = true, want false/absent (an onclick property is not a target)", id)
}
}
}
// TestHierarchy_ScrollableAttribute covers the fact the goja host reads off the
// attributes map. The V8 picker computes the same overflow test in
// web-runtime.ts, so leaving it out of the dump made the two enumerations
// disagree on web: the model policy could never be offered a scroll the seeded
// policy could draw.
func TestHierarchy_ScrollableAttribute(t *testing.T) {
const html = `<body style="margin:0">` +
`<div id="overflowing" style="width:100px;height:100px;overflow:auto">` +
`<div style="width:100px;height:900px"></div></div>` +
`<div id="sideways" style="width:100px;height:50px;overflow:auto">` +
`<div style="width:900px;height:20px"></div></div>` +
`<div id="fits" style="width:100px;height:100px;overflow:auto">` +
`<div style="width:50px;height:50px"></div></div>` +
`</body>`
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,"+html, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
}
type node struct {
Attributes map[string]string `json:"attributes"`
Children []node `json:"children"`
}
var root node
if err := json.Unmarshal([]byte(dump), &root); err != nil {
t.Fatalf("unmarshal hierarchy: %v", err)
}
scrollableByID := map[string]string{}
var walk func(n node)
walk = func(n node) {
if id := n.Attributes["resource-id"]; id != "" {
scrollableByID[id] = n.Attributes["scrollable"]
}
for _, c := range n.Children {
walk(c)
}
}
walk(root)
for _, id := range []string{"overflowing", "sideways"} {
if scrollableByID[id] != "true" {
t.Errorf("%q: scrollable = %q, want \"true\"", id, scrollableByID[id])
}
}
if scrollableByID["fits"] != "" {
t.Errorf("%q: scrollable = %q, want absent", "fits", scrollableByID["fits"])
}
}
// TestHierarchy_HintTextNamesAnEditableField covers the attribute visibleLabel
// (internal/verifier/llm.go) reads FIRST for an editable element. Without it a
// web field reached the model named by its CSS class, an identifier no user can
@@ -330,19 +176,7 @@ func TestHierarchy_HintTextNamesAnEditableField(t *testing.T) {
`<input id="agree" class="checkbox" type="checkbox" placeholder="ignored">` +
`<button id="go" class="button" placeholder="ignored">go</button>` +
`</body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(html))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, html).URL)
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -447,13 +281,7 @@ func TestHierarchy_RootsAtDocumentElementWithoutHead(t *testing.T) {
`<script>window.marker = 1;</script></head>` +
`<body><div id="content">hello</div></body></html>`
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,"+html, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, "data:text/html,"+html)
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -595,19 +423,7 @@ func TestInputText_ReplacesTextInsideAShadowRoot(t *testing.T) {
root.getElementById("surface").addEventListener("click", function () { proxy.focus(); });
proxy.addEventListener("input", function () { field.textContent = proxy.value; });
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
if err := d.Tap(ctx, 40, 40); err != nil {
t.Fatalf("Tap: %v", err)
}
@@ -644,11 +460,7 @@ func TestInputText_ReplacesTextInsideAShadowRoot(t *testing.T) {
// uses), so every screen looked like the same screen and no route-scoped
// property or action could tell them apart.
func TestHierarchy_ScreenFallsBackToThePathname(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<body><div id="app">app</div></body>`))
}))
defer server.Close()
server := servePage(t, `<body><div id="app">app</div></body>`)
for _, testCase := range []struct {
name string
@@ -660,13 +472,7 @@ func TestHierarchy_ScreenFallsBackToThePathname(t *testing.T) {
{"root", "/", "/"},
} {
t.Run(testCase.name, func(t *testing.T) {
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL+testCase.path, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, server.URL+testCase.path)
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -698,19 +504,7 @@ func TestWaitForIdle_WaitsForWorkTheActionKickedOff(t *testing.T) {
setTimeout(function () { root.getElementById("out").textContent = "settled"; }, 100);
});
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
if err := d.Tap(ctx, 40, 40); err != nil {
t.Fatalf("Tap: %v", err)
}
@@ -735,19 +529,7 @@ func TestWaitForIdle_ReturnsOnABusyPage(t *testing.T) {
let n = 0;
setInterval(function () { document.getElementById("tick").textContent = String(++n); }, 15);
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
start := time.Now()
if err := d.WaitForIdle(ctx, time.Second); err != nil {
t.Fatalf("WaitForIdle: %v", err)
@@ -792,19 +574,7 @@ func TestWaitForIdle_WaitsOutARouteTransition(t *testing.T) {
setTimeout(function () { root.getElementById("LedgerScreen").remove(); }, 1000);
});
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
if err := d.Tap(ctx, 40, 40); err != nil {
t.Fatalf("Tap: %v", err)
}
@@ -830,19 +600,7 @@ func TestWaitForIdle_WaitsOutARouteTransition(t *testing.T) {
// whole step budget on every step.
func TestWaitForIdle_BoundsTheTransitionWait(t *testing.T) {
const page = `<body><div id="HomeScreen">home</div><div id="LedgerScreen">ledger</div></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
start := time.Now()
if err := d.WaitForIdle(ctx, 10*time.Second); err != nil {
t.Fatalf("WaitForIdle: %v", err)
@@ -874,19 +632,7 @@ func TestEvaluateExtractors_WaitsOutARouteTransition(t *testing.T) {
setTimeout(function () { root.getElementById("LedgerScreen").remove(); }, 400);
};
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(`window.startTransition()`, nil)); err != nil {
t.Fatalf("start transition: %v", err)
}
@@ -915,21 +661,9 @@ func TestSetLastAction_ReportsAPageThatCannotTakeIt(t *testing.T) {
window.__sanderlingSetLastAction__ = function (value) { window.__lastActionSeen = value; };
</script></body>`
const withoutSetter = `<body><div id="app">no sanderling runtime here</div></body>`
pages := map[string]string{"/with": withSetter, "/without": withoutSetter}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(pages[r.URL.Path]))
}))
defer server.Close()
without := servePage(t, withoutSetter)
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL+"/with", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, withSetter).URL)
action := json.RawMessage(`{"kind":"Tap","on":"id:TxnSubmit"}`)
if err := d.SetLastAction(ctx, action); err != nil {
t.Fatalf("SetLastAction on a page that defines the setter: %v", err)
@@ -943,7 +677,7 @@ func TestSetLastAction_ReportsAPageThatCannotTakeIt(t *testing.T) {
t.Errorf("the page received %v, want the action the runner applied", seen)
}
if err := d.Launch(ctx, server.URL+"/without", false, nil); err != nil {
if err := d.Launch(ctx, without.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
if err := d.SetLastAction(ctx, action); err == nil {
@@ -963,21 +697,9 @@ func TestSetLogs_ReportsAPageThatCannotTakeThem(t *testing.T) {
window.__sanderlingSetLogs__ = function (value) { window.__logsSeen = value; };
</script></body>`
const withoutSetter = `<body><div id="app">no sanderling runtime here</div></body>`
pages := map[string]string{"/with": withSetter, "/without": withoutSetter}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(pages[r.URL.Path]))
}))
defer server.Close()
without := servePage(t, withoutSetter)
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL+"/with", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, withSetter).URL)
logs := json.RawMessage(`[{"unixMillis":1,"level":"E","tag":"console","message":"boom"}]`)
if err := d.SetLogs(ctx, logs); err != nil {
t.Fatalf("SetLogs on a page that defines the setter: %v", err)
@@ -991,7 +713,7 @@ func TestSetLogs_ReportsAPageThatCannotTakeThem(t *testing.T) {
t.Errorf("the page received %v, want the error-level entry the driver captured", seen)
}
if err := d.Launch(ctx, server.URL+"/without", false, nil); err != nil {
if err := d.Launch(ctx, without.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
if err := d.SetLogs(ctx, logs); err == nil {
@@ -1007,19 +729,7 @@ func TestSetLogs_ReportsAPageThatCannotTakeThem(t *testing.T) {
// dump-derived values while believing they came from the page.
func TestEvaluateExtractors_ReportsAMissingTable(t *testing.T) {
const page = `<body><div id="app">no sanderling runtime here</div></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
values, err := d.EvaluateExtractors(ctx)
if err == nil {
t.Errorf("EvaluateExtractors returned %v and no error on a page with no "+
@@ -1041,19 +751,7 @@ func TestEvaluateExtractors_KeepsUndefinedApartFromNull(t *testing.T) {
return {0: {}, 1: {value: null}, 2: {value: {balance: 7}}};
};
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
values, err := d.EvaluateExtractors(ctx)
if err != nil {
@@ -1082,19 +780,7 @@ func TestEvaluateExtractors_RejectsAnUnenvelopedReading(t *testing.T) {
const page = `<body><script>
window.__sanderlingExtractors__ = function () { return {0: "home"}; };
</script></body>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}))
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
values, err := d.EvaluateExtractors(ctx)
if err == nil {
@@ -1117,13 +803,7 @@ func TestHierarchy_CarriesEveryMarkupAttribute(t *testing.T) {
`<div id="card" data-testid="account-card" data-account-id="acct-7" data-balance="4200">Tim</div>` +
`</body>`
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, "data:text/html,"+html, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, "data:text/html,"+html)
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -1177,21 +857,7 @@ document.getElementById('grow').addEventListener('click', function() {
// where it is; a click dispatched there is hit-tested to the document root and
// the element never sees it, with no error anywhere.
func TestTap_ActuatesAnElementBelowTheLaunchViewport(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(growingPage))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, growingPage).URL)
tapByID := func(id string) {
t.Helper()
@@ -1247,21 +913,7 @@ func TestTap_ReportsAGestureThatReachesNoElement(t *testing.T) {
<div style="height:80px">top</div>
<div id="rest" style="height:900px;overflow:hidden"></div>
<style>html{overflow:hidden}</style></body>`
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
err := d.Tap(ctx, 100, 5000)
if !errors.Is(err, driver.ErrGestureUndelivered) {
t.Fatalf(
@@ -1283,21 +935,7 @@ func TestTapSelector_ReportsASelectorThatMatchesNothing(t *testing.T) {
document.getElementById('status').textContent = 'present tapped';
});
</script></body>`
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
missCtx, missCancel := context.WithTimeout(ctx, 10*time.Second)
defer missCancel()
@@ -1359,21 +997,7 @@ func doubleClickStatus(t *testing.T, d *Driver, ctx context.Context) string {
// clicks: every double-click affordance on the web (an editable list row, a
// canvas, a table cell) was unreachable, with no error on any layer.
func TestDoubleTap_ReachesADoubleClickHandler(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(doubleClickPage))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, doubleClickPage).URL)
if err := d.DoubleTap(ctx, 100, 30); err != nil {
t.Fatalf("DoubleTap: %v", err)
}
@@ -1390,21 +1014,7 @@ func TestDoubleTap_ReachesADoubleClickHandler(t *testing.T) {
// the path the runner takes when the action names its target rather than a
// point.
func TestDoubleTapSelector_ReachesADoubleClickHandler(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(doubleClickPage))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, doubleClickPage).URL)
if err := d.DoubleTapSelector(ctx, "id:target"); err != nil {
t.Fatalf("DoubleTapSelector: %v", err)
}
@@ -1417,25 +1027,6 @@ func TestDoubleTapSelector_ReachesADoubleClickHandler(t *testing.T) {
}
}
// gesturesServer serves the fixture both gesture tests measure against: a
// document taller than the emulated viewport, a scrollable container inside it,
// and a row that dismisses on a horizontal drag.
func gesturesServer(t *testing.T) *httptest.Server {
t.Helper()
body, err := os.ReadFile("testdata/gestures.html")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write(body)
}),
)
t.Cleanup(server.Close)
return server
}
// TestScroll_MovesThePageAndAScrollableContainer covers the verb the runner
// lowers every Scroll action onto. Script-dispatched pointer events are
// untrusted and a browser never scrolls on them, so the web Scroll used to
@@ -1443,14 +1034,7 @@ func gesturesServer(t *testing.T) *httptest.Server {
// step that ran. The repeat also pins the distance: a run that scrolls a
// different amount each time explores differently on the same seed.
func TestScroll_MovesThePageAndAScrollableContainer(t *testing.T) {
server := gesturesServer(t)
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, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/gestures.html")
read := func(expression string) int {
t.Helper()
@@ -1524,21 +1108,7 @@ func TestScroll_ReportsAGestureThatReachesNoElement(t *testing.T) {
const page = `<body style="margin:0;overflow:hidden">
<div style="height:80px">top</div>
<style>html{overflow:hidden}</style></body>`
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(page))
}),
)
defer server.Close()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, page).URL)
err := d.Scroll(ctx, 100, 5000, 100, 4800, 300*time.Millisecond)
if !errors.Is(err, driver.ErrGestureUndelivered) {
t.Fatalf(
@@ -1554,14 +1124,7 @@ func TestScroll_ReportsAGestureThatReachesNoElement(t *testing.T) {
// the page's own input pipeline saw it, so scrolling, touch-action and any
// handler that filters on trust behave as if the finger never moved.
func TestSwipe_DeliversATrustedDragToARowHandler(t *testing.T) {
server := gesturesServer(t)
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, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/gestures.html")
if err := d.Swipe(ctx, 300, 40, 100, 40, 300*time.Millisecond); err != nil {
t.Fatalf("Swipe: %v", err)
}
@@ -1584,19 +1147,7 @@ func TestSwipe_DeliversATrustedDragToARowHandler(t *testing.T) {
// wrong gesture, and the run reports a full step count of results that mean
// nothing.
func TestInstallBundle_RefusesARuntimeThatDeclaresADifferentActionEncoding(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<body><div id="app">app</div></body>`))
}))
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, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, `<body><div id="app">app</div></body>`).URL)
const legacyRuntime = `window.__sanderlingNextAction__ = function () { return null; };`
if err := d.InstallBundle(ctx, []byte(legacyRuntime)); err == nil {
@@ -1644,19 +1195,7 @@ export const properties = {};
t.Fatalf("BundleWeb: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<body><div id="app">app</div></body>`))
}))
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, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, `<body><div id="app">app</div></body>`).URL)
if err := d.InstallBundle(ctx, bundle.JavaScript); err != nil {
t.Fatalf("InstallBundle rejected this checkout's own web runtime: %v", err)
}
@@ -1667,19 +1206,7 @@ export const properties = {};
// generates no actions, so it has no encoding to disagree about and demanding a
// declaration from it would refuse a spec that was never going to dispatch.
func TestInstallBundle_AcceptsAPageThatInstallsNoPicker(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<body><div id="app">app</div></body>`))
}))
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, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, `<body><div id="app">app</div></body>`).URL)
const pickerFree = `window.__sanderlingBundleCheck__ = true;`
if err := d.InstallBundle(ctx, []byte(pickerFree)); err != nil {
@@ -1692,18 +1219,7 @@ func TestInstallBundle_AcceptsAPageThatInstallsNoPicker(t *testing.T) {
// and zero is the honest answer there, so a round trip that never happened has
// to be an error or a run records a memory reading it never took.
func TestMetrics_AFailedRoundTripIsNotAPageWithoutTheAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<body><div id="app">app</div></body>`))
}))
defer server.Close()
d := New()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, servePage(t, `<body><div id="app">app</div></body>`).URL)
if _, err := d.Metrics(ctx, ""); err != nil {
t.Fatalf("Metrics on a live page: %v", err)
}
+6 -100
View File
@@ -5,12 +5,9 @@ package chrome
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"slices"
"testing"
"time"
"github.com/chromedp/chromedp"
@@ -31,16 +28,7 @@ import (
// reading element.getAttribute("checked") reports the starting value forever and
// passes any test that only reads a freshly loaded page.
func TestElementState_ChecksTrackTheLiveDOM(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+"/element-state.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/element-state.html")
installStateProbe(ctx, t, d)
requireChecked(ctx, t, d, "toggle-all", false)
@@ -96,16 +84,7 @@ func requireChecked(
// the same reason `checked` is: the attribute records only where the page
// started.
func TestElementState_ReportsTheOtherDocumentedBooleans(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+"/element-state.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/element-state.html")
installStateProbe(ctx, t, d)
requireBoolean(
@@ -191,43 +170,6 @@ func TestElementState_ReportsTheOtherDocumentedBooleans(t *testing.T) {
)
}
// Every editable field states `secure`, false included. internal/verifier
// redacts a typed value whenever the target does not positively report "not a
// secure entry", so a dump that omitted the key on an ordinary text field would
// redact the whole recent-action memory on web.
func TestElementState_SecureStatesEveryEditableField(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+"/element-state.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
for _, testCase := range []struct {
selector string
want bool
}{
{"id:secret", true},
{"id:editing", false},
} {
element := elementInHierarchyDump(ctx, t, d, testCase.selector)
if !element.SecureReported() {
t.Errorf("the dump states no secure fact for %s", testCase.selector)
}
if element.Secure != testCase.want {
t.Errorf("%s secure = %v, want %v", testCase.selector, element.Secure, testCase.want)
}
}
if button := elementInHierarchyDump(ctx, t, d, "id:save"); button.SecureReported() {
t.Error("a button is not a text entry and states nothing")
}
}
// Focus belongs to the node the user is typing into, not to the element the
// shadow tree is mounted on.
//
@@ -237,16 +179,7 @@ func TestElementState_SecureStatesEveryEditableField(t *testing.T) {
// to type when the field it tapped is not the one holding focus, so every
// InputText step on such an app failed and the run aborted.
func TestElementState_FocusDescendsIntoTheShadowRoot(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+"/shadow-focus.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/shadow-focus.html")
field := elementInHierarchyDump(ctx, t, d, "id:shadow-field")
x, y := field.Bounds.Center()
@@ -274,16 +207,7 @@ func TestElementState_FocusDescendsIntoTheShadowRoot(t *testing.T) {
// Both fields are tapped, because reporting the first editable in the tree
// would satisfy the email half of this and still type into the wrong field.
func TestElementState_FocusFollowsTheCaretToItsField(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+"/compose-backing-input.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/compose-backing-input.html")
for _, field := range []string{"EmailField", "PasswordField"} {
tapped := elementInHierarchyDump(ctx, t, d, "id:"+field)
@@ -466,16 +390,7 @@ func installStateProbe(ctx context.Context, t *testing.T, d *Driver) {
// green having actuated nothing. The page records its own keydown events, so
// what is asserted here is what the DOM received, not what the driver sent.
func TestPressKey_ArrivesAtThePage(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+"/element-state.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/element-state.html")
for _, keyCase := range []struct{ key, want string }{
{"enter", "Enter"},
@@ -544,16 +459,7 @@ func keysSeenByThePage(ctx context.Context, t *testing.T, d *Driver) []string {
// pkg/spec/src/web-runtime.ts already answers `state.selected === true`, so the
// two hosts also disagreed about the same fact on the same page.
func TestElementState_AComponentPropertyDoesNotBlankTheTree(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+"/custom-element-flags.html", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/custom-element-flags.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
+179 -25
View File
@@ -5,14 +5,11 @@ package chrome
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"runtime"
"slices"
"strconv"
"testing"
"time"
"github.com/chromedp/chromedp"
@@ -89,19 +86,114 @@ type factRow struct {
// producers have to descend into or they enumerate one node for an entire app.
var parityPages = []string{"fact-parity.html", "fact-parity-shadow.html"}
// booleanFacts is every boolean fact both producers derive, with the reader
// that takes it off an element. It is ordered so a page reports its facts in
// the same sequence every run.
var booleanFacts = []struct {
name string
read func(elementFacts) bool
}{
{"clickable", func(f elementFacts) bool { return f.clickable }},
{"enabled", func(f elementFacts) bool { return f.enabled }},
{"editable", func(f elementFacts) bool { return f.editable }},
{"scrollable", func(f elementFacts) bool { return f.scrollable }},
{"hintText", func(f elementFacts) bool { return f.hintText != "" }},
{"checked", func(f elementFacts) bool { return f.checked }},
{"selected", func(f elementFacts) bool { return f.selected }},
{"focused", func(f elementFacts) bool { return f.focused }},
{"positiveBounds", func(f elementFacts) bool { return f.positiveBounds }},
}
// statedBoolean is one reading a fixture page was built to produce, named by
// the element that holds it.
type statedBoolean struct {
id string
fact string
want bool
}
// statedFacts is what a page states, as opposed to what the two producers
// happen to agree on.
type statedFacts struct {
booleans []statedBoolean
// secure is three-valued, so it is stated apart from the booleans. A field
// the producer says nothing about is not a field it says is not a password:
// internal/verifier redacts a typed value unless the target positively
// reports "not a secure entry", so a producer that leaves the key off an
// ordinary text field redacts the whole recent-action memory on web.
secure map[string]string
}
// What each fixture page derives, pinned as values. compareDerivedFacts passes
// whenever the two producers read a fact the same wrong way, which is the shape
// of every producer bug in the header, so the readings themselves are held
// here: an onclick PROPERTY (which React assigns to its root container) is not
// a tap target where an onclick attribute is, a role the ARIA contract says a
// user activates is, contenteditable makes the container typeable and not the
// span that merely inherits it, a checkbox is not a text field, a container is
// scrollable only where its content overflows its box on either axis, and a
// field that is not a password entry states so rather than staying silent.
var factsEachPageStates = map[string]statedFacts{
"fact-parity.html": {
booleans: []statedBoolean{
{"amount", "editable", true},
{"notes", "editable", true},
{"bio", "editable", true},
{"bio-word", "editable", false},
{"agree", "editable", false},
{"save", "editable", false},
{"save", "clickable", true},
{"notes", "clickable", true},
{"menu", "clickable", true},
{"attribute-click", "clickable", true},
{"delegating-root", "clickable", false},
{"plain", "clickable", false},
{"scroller", "scrollable", true},
{"sideways", "scrollable", true},
{"fits", "scrollable", false},
},
secure: map[string]string{
"password": "true",
"amount": "false",
"notes": "false",
"save": "",
},
},
"fact-parity-shadow.html": {
booleans: []statedBoolean{
{"shadow-amount", "editable", true},
{"shadow-agree", "editable", false},
{"shadow-save", "editable", false},
{"shadow-save", "clickable", true},
{"shadow-plain", "clickable", false},
{"shadow-scroller", "scrollable", true},
},
secure: map[string]string{
"shadow-password": "true",
"shadow-amount": "false",
"shadow-save": "",
},
},
}
func TestHierarchy_DerivesTheSameFactsAsTheWebRuntime(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
defer server.Close()
server := testdataServer(t)
for _, page := range parityPages {
t.Run(page, func(t *testing.T) {
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+"/"+page, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
stated, ok := factsEachPageStates[page]
if !ok {
t.Fatalf(
"testdata/%s states no facts of its own, so its run compares the "+
"two producers to each other and to nothing else",
page,
)
}
d, ctx := launchChrome(t, server.URL+"/"+page)
dump, err := d.Hierarchy(ctx)
if err != nil {
@@ -115,12 +207,87 @@ func TestHierarchy_DerivesTheSameFactsAsTheWebRuntime(t *testing.T) {
requireBothPolarities(t, fromWebRuntime)
requireEverySecureState(t, fromWebRuntime)
requireTheHandleAgreesWithTheEnumeration(t, fromWebRuntime)
requireTheFactsThePageStates(t, "the hierarchy dump", stated, fromDump)
requireTheFactsThePageStates(t, "the web runtime", stated, fromWebRuntime)
compareEnumeratedElements(t, fromDump, fromWebRuntime)
compareDerivedFacts(t, fromDump, fromWebRuntime)
})
}
}
// requireTheFactsThePageStates holds one producer to the page rather than to
// the other producer, which is what keeps the comparison anchored to something
// outside the pair.
func requireTheFactsThePageStates(
t *testing.T,
producer string,
stated statedFacts,
rows []factRow,
) {
t.Helper()
byID := map[string]elementFacts{}
for _, row := range rows {
byID[row.id] = row.facts
}
for _, boolean := range stated.booleans {
facts, enumerated := byID[boolean.id]
if !enumerated {
t.Errorf(
"%s never enumerated %q, which the fixture states %s=%v for",
producer,
boolean.id,
boolean.fact,
boolean.want,
)
continue
}
if got := readBooleanFact(t, boolean.fact)(facts); got != boolean.want {
t.Errorf(
"%s derives %s=%v for %q (<%s>), want %v",
producer,
boolean.fact,
got,
boolean.id,
facts.tag,
boolean.want,
)
}
}
for id, want := range stated.secure {
facts, enumerated := byID[id]
if !enumerated {
t.Errorf(
"%s never enumerated %q, which the fixture states secure=%q for",
producer,
id,
want,
)
continue
}
if facts.secure != want {
t.Errorf(
"%s derives secure=%q for %q (<%s>), want %q",
producer,
facts.secure,
id,
facts.tag,
want,
)
}
}
}
func readBooleanFact(t *testing.T, name string) func(elementFacts) bool {
t.Helper()
for _, fact := range booleanFacts {
if fact.name == name {
return fact.read
}
}
t.Fatalf("%q is not a fact either producer derives", name)
return nil
}
// factsFromHierarchyDump reads the dump the way the goja host does: parse it,
// then take exactly the fields internal/verifier/worker.go targets() reads off
// each element.
@@ -275,20 +442,7 @@ func requireEveryElementNamed(t *testing.T, producer string, rows []factRow) {
// false on every element and pass while proving nothing about that fact.
func requireBothPolarities(t *testing.T, rows []factRow) {
t.Helper()
for _, fact := range []struct {
name string
read func(elementFacts) bool
}{
{"clickable", func(f elementFacts) bool { return f.clickable }},
{"enabled", func(f elementFacts) bool { return f.enabled }},
{"editable", func(f elementFacts) bool { return f.editable }},
{"scrollable", func(f elementFacts) bool { return f.scrollable }},
{"hintText", func(f elementFacts) bool { return f.hintText != "" }},
{"checked", func(f elementFacts) bool { return f.checked }},
{"selected", func(f elementFacts) bool { return f.selected }},
{"focused", func(f elementFacts) bool { return f.focused }},
{"positiveBounds", func(f elementFacts) bool { return f.positiveBounds }},
} {
for _, fact := range booleanFacts {
var sawTrue, sawFalse bool
for _, row := range rows {
if fact.read(row.facts) {
+4 -23
View File
@@ -4,12 +4,9 @@ package chrome
import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/chromedp/chromedp"
@@ -28,9 +25,7 @@ const streamSeed = 1
// on 200 of 200 steps and created no todo at all. The run reported clean, so
// nothing but this distinguishes it from a seed that chose badly.
func TestNextActionFromV8_ReloadDoesNotRestartTheSeedStream(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
defer server.Close()
url := server.URL + "/picker-stream.html"
url := testdataServer(t).URL + "/picker-stream.html"
const calls = 8
uninterrupted := pickerStream(t, url, calls, calls)
@@ -50,17 +45,9 @@ func TestNextActionFromV8_ReloadDoesNotRestartTheSeedStream(t *testing.T) {
// A navigation the trace cannot see is a run nobody can read: an analysis has
// no way to tell an app that reloaded from a generator that repeated itself.
func TestNavigations_ReportTheDocumentThatReplacedThePage(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
defer server.Close()
url := server.URL + "/picker-stream.html"
url := testdataServer(t).URL + "/picker-stream.html"
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := d.Launch(ctx, url, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, url)
opening, err := d.Navigations(ctx)
if err != nil {
@@ -101,13 +88,7 @@ func TestNavigations_ReportTheDocumentThatReplacedThePage(t *testing.T) {
// calls. Reloading past the call count leaves the stream uninterrupted.
func pickerStream(t *testing.T, url string, calls, reloadAfter int) []string {
t.Helper()
d := New()
defer d.Terminate(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
if err := d.Launch(ctx, url, false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
d, ctx := launchChrome(t, url)
installPickerStreamProbe(ctx, t, d)
chosen := make([]string, 0, calls)
+8 -83
View File
@@ -5,13 +5,10 @@ package chrome
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/chromedp/chromedp"
@@ -31,16 +28,7 @@ import (
// 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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
@@ -381,16 +369,7 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) {
// turned the whole selector into a parse error: querySelectorAll throws, and
// what a spec sees is an exception out of the extractor rather than an element.
func TestSelectors_SecureCombinesWithAnotherKey(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -426,16 +405,7 @@ func TestSelectors_SecureCombinesWithAnotherKey(t *testing.T) {
// has an opinion about (everything is enabled, almost nothing is checked)
// answers with most of the document and a want list nobody can check by reading.
func TestSelectors_BooleanStatesNameWhatBothProducersReport(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -619,16 +589,7 @@ func TestSelectors_BooleanStatesNameWhatBothProducersReport(t *testing.T) {
// '[id="state_month"]select' and querySelectorAll threw a SyntaxError. Which of
// the two a spec got depended on the order its author wrote the keys in.
func TestSelectors_TagCombinesWithAnotherKey(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -661,16 +622,7 @@ func TestSelectors_TagCombinesWithAnotherKey(t *testing.T) {
// ancestors up to the document root read as matches too, and the deepest one is
// the element the author meant.
func TestSelectors_TextNamesTheInnermostMatchInEveryResolver(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -825,16 +777,7 @@ func TestSelectors_TextNamesTheInnermostMatchInEveryResolver(t *testing.T) {
// "January" is not clickable: resolving text to its own innermost match before
// the other keys narrow anything answers with nothing in both.
func TestSelectors_TextCombinesWithAnotherKey(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -971,16 +914,7 @@ func xpathIDsOverCDP(
// 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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)
@@ -1026,16 +960,7 @@ func TestSelectors_DataTestIDNamesTheSameElementInTheDumpAndOverCDP(t *testing.T
// already does. placeholder keeps its CSS: it is the attribute the markup
// writes, which is what all three resolvers read it as.
func TestSelectors_HintSelectorsResolveOverCDPToWhatBothMatchersName(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)
}
d, ctx := launchChrome(t, testdataServer(t).URL+"/selector-parity.html")
dump, err := d.Hierarchy(ctx)
if err != nil {
t.Fatalf("Hierarchy: %v", err)