test(browser): drive a console error through to the spec

Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ
This commit is contained in:
pj committed 2026-08-16 01:23:58 +05:30
1 parent beb036d2c1
commit fdc616247a
3 files changed
+204

No files matched your search

+174
View File
@@ -0,0 +1,174 @@
//go:build browser
package browser_test
import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/sanderling/internal/driver/chrome"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/verifier"
)
// TestBrowserConsoleErrorReachesTheSpec drives a page that calls console.error
// and follows the entry the whole way a run does: the driver's log fetch at the
// runner's minimum level, then into the verifier state a property reads. The
// default noLogcatErrors counts entries whose level is "E", so a driver that
// spells the level any other way leaves the property permanently satisfied on
// web with nothing reporting that it never saw anything.
func TestBrowserConsoleErrorReachesTheSpec(t *testing.T) {
ctx, driverInstance, since := launchConsoleFixture(t)
entries, err := driverInstance.RecentLogs(ctx, since, "E")
if err != nil {
t.Fatalf("recent logs: %v", err)
}
if len(entries) != 1 {
t.Fatalf("the runner's error-level fetch returned %d entries, want the page's one console.error: %+v", len(entries), entries)
}
if entries[0].Level != "E" {
t.Errorf("console.error arrived as level %q, want %q", entries[0].Level, "E")
}
if !strings.Contains(entries[0].Message, "boom from the page") {
t.Errorf("console.error arrived as %q, want the page's message", entries[0].Message)
}
dump, err := driverInstance.Hierarchy(ctx)
if err != nil {
t.Fatalf("hierarchy: %v", err)
}
tree, err := hierarchy.Parse(dump)
if err != nil {
t.Fatalf("parse hierarchy: %v", err)
}
gojaBundle, _ := bundleSpec(t, filepath.Join(testdataDir(t), "console-levels", "spec.ts"))
verifierInstance, err := verifier.New(
verifier.WithSeed(fixtureSeed),
verifier.WithPlatform("web"),
)
if err != nil {
t.Fatalf("verifier: %v", err)
}
if err := verifierInstance.Load(string(gojaBundle)); err != nil {
t.Fatalf("load spec: %v", err)
}
if err := verifierInstance.PushSnapshot(verifier.SnapshotInput{
Tree: tree,
Logs: asVerifierLogs(entries),
}); err != nil {
t.Fatalf("push snapshot: %v", err)
}
verifierInstance.EvaluateProperties()
if violated := verifierInstance.NewlyViolatedProperties(); !slices.Contains(violated, "noLogcatErrors") {
t.Fatalf("a console.error on the page left noLogcatErrors satisfied; violations=%v", violated)
}
}
// TestBrowserConsoleLevelsMapToTheLogcatScale pins what each console verb
// becomes once it crosses the driver. driver.LogEntry.Level is the single-letter
// logcat scale on every platform, so a spec asking for warnings or debug lines
// by letter has to get the same answer on web as it does on Android, and a
// console verb the driver has no mapping for still has to arrive rather than be
// silently discarded.
func TestBrowserConsoleLevelsMapToTheLogcatScale(t *testing.T) {
ctx, driverInstance, since := launchConsoleFixture(t)
entries, err := driverInstance.RecentLogs(ctx, since, "V")
if err != nil {
t.Fatalf("recent logs: %v", err)
}
if len(entries) != 6 {
t.Fatalf("the page made 6 console calls, the driver kept %d: %+v", len(entries), entries)
}
wantLevels := map[string]string{
"boom from the page": "E",
"a warning": "W",
"a plain log": "I",
"a debug line": "D",
"an info line": "I",
}
seen := map[string]bool{}
for _, entry := range entries {
if !slices.Contains([]string{"V", "D", "I", "W", "E", "F"}, entry.Level) {
t.Errorf("entry %q carries level %q, which is not on the logcat scale a spec compares against", entry.Message, entry.Level)
}
for message, level := range wantLevels {
if !strings.Contains(entry.Message, message) {
continue
}
seen[message] = true
if entry.Level != level {
t.Errorf("console message %q arrived as level %q, want %q", message, entry.Level, level)
}
}
}
for message := range wantLevels {
if !seen[message] {
t.Errorf("console message %q never reached the driver's log buffer", message)
}
}
errorsOnly, err := driverInstance.RecentLogs(ctx, since, "E")
if err != nil {
t.Fatalf("recent logs: %v", err)
}
if len(errorsOnly) != 1 {
t.Fatalf("the error-level fetch kept %d of the 6 entries, want only the console.error: %+v", len(errorsOnly), errorsOnly)
}
warningsUp, err := driverInstance.RecentLogs(ctx, since, "W")
if err != nil {
t.Fatalf("recent logs: %v", err)
}
if len(warningsUp) != 2 {
t.Fatalf("the warning-level fetch kept %d entries, want the console.error and the console.warn: %+v", len(warningsUp), warningsUp)
}
}
// launchConsoleFixture serves the console-levels page and drives headless Chrome
// to it, returning the driver plus the instant before the page ran so a log
// fetch can ask for everything the page emitted.
func launchConsoleFixture(t *testing.T) (context.Context, *chrome.Driver, time.Time) {
t.Helper()
server := httptest.NewServer(http.FileServer(http.Dir(testdataDir(t))))
t.Cleanup(server.Close)
driverInstance := chrome.New()
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = driverInstance.Terminate(ctx)
})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
t.Cleanup(cancel)
since := time.Now()
if err := driverInstance.Launch(ctx, server.URL+"/console-levels/", false, nil); err != nil {
t.Fatalf("launch: %v", err)
}
return ctx, driverInstance, since
}
func asVerifierLogs(entries []driver.LogEntry) []verifier.LogEntry {
out := make([]verifier.LogEntry, 0, len(entries))
for _, entry := range entries {
out = append(out, verifier.LogEntry{
UnixMillis: entry.UnixMillis,
Level: entry.Level,
Tag: entry.Tag,
Message: entry.Message,
})
}
return out
}