Find Playwright's macOS Chromium; run every browser test
make test-browser could not start on a Mac: rastrillo's harness looks for Playwright's Linux layout only, so with no Chromium on PATH it failed with "no chromium found" while ~/Library/Caches/ms-playwright held three. The browser tests now hand the harness the newest Chromium in either cache layout (Chrome for Testing.app, the older Chromium.app, or Linux's), when RASTRILLO_CHROME is unset and nothing is on PATH. The finder is in an untagged file so the plain gate tests it. test-browser ran -run 'OnTap' only, which would silently skip any browser test added under another name. It now runs the whole package with the browser tag. Also fix assertion.html's comment, which cited shared-chrome §9.3 for the bar and sky placeholder as well as the hidden card; §9.3 is the card rule only, and the comment had an unwrapped line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+141
−7
Makefile+7 −4internal/home/chromefinder_test.go+103 −0internal/home/templates/assertion.html+5 −3internal/home/zoom_browser_test.go+26 −0
CI passed — run details
CI log
amadan: .amadan/timeout: this job's budget is 15m0s (this runner allows up to 30m0s) === vet === CGO_ENABLED=0 go vet ./... CGO_ENABLED=0 go vet -tags browser ./internal/home/ cd handoff && CGO_ENABLED=0 go vet ./... cd roster && CGO_ENABLED=0 go vet ./... cd connect && CGO_ENABLED=0 go vet ./... === fmt === === build === CGO_ENABLED=0 go build ./... CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./cmd/home ./cmd/oficina ./internal/operatorlaunch cd handoff && CGO_ENABLED=0 go build ./... cd roster && CGO_ENABLED=0 go build ./... cd connect && CGO_ENABLED=0 go build ./... === go-test === CGO_ENABLED=0 go test -count=1 ./... ? amadan.net/oficina/home/cmd/home [no test files] ok amadan.net/oficina/home/cmd/home-placeholder 0.625s ? amadan.net/oficina/home/cmd/oficina [no test files] ok amadan.net/oficina/home/internal/guardstest 0.038s ok amadan.net/oficina/home/internal/home 11.367s ok amadan.net/oficina/home/internal/home/directorydata 0.982s ok amadan.net/oficina/home/internal/home/logtest 0.005s ok amadan.net/oficina/home/internal/home/seal 0.005s ok amadan.net/oficina/home/internal/home/sealtest 0.004s ok amadan.net/oficina/home/internal/operator 5.061s ok amadan.net/oficina/home/internal/operatorlaunch 4.148s ok amadan.net/oficina/home/internal/provisioner 42.197s cd handoff && CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/home/handoff 0.371s ok amadan.net/oficina/home/handoff/seal 0.002s cd roster && CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/home/roster 5.077s cd connect && CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/home/connect 0.146s ok amadan.net/oficina/home/connect/linkstore 0.213s === race === go test -race -count=1 ./internal/home/ ./internal/operator/ ./internal/operatorlaunch/ ./internal/provisioner/ ./internal/home/directorydata/ ok amadan.net/oficina/home/internal/home 121.229s ok amadan.net/oficina/home/internal/operator 65.351s ok amadan.net/oficina/home/internal/operatorlaunch 22.567s ok amadan.net/oficina/home/internal/provisioner 338.367s ok amadan.net/oficina/home/internal/home/directorydata 19.273s cd handoff && CGO_ENABLED=1 go test -race -count=1 ./... ok amadan.net/oficina/home/handoff 9.160s ok amadan.net/oficina/home/handoff/seal 1.008s cd roster && CGO_ENABLED=1 go test -race -count=1 ./... ok amadan.net/oficina/home/roster 6.625s cd connect && CGO_ENABLED=1 go test -race -count=1 ./... ok amadan.net/oficina/home/connect 2.132s ok amadan.net/oficina/home/connect/linkstore 5.855s
diff --git a/Makefile b/Makefile| index b3c941b..47c04de 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -126,13 +126,16 @@ test-race: |
| cd roster && CGO_ENABLED=1 go test -race -count=1 ./... |
| cd connect && CGO_ENABLED=1 go test -race -count=1 ./... |
| -# The browser checks, in a real Chromium (on PATH, RASTRILLO_CHROME, or a |
| -# Playwright cache). Deliberately NOT part of `test`: the gate CI runs on |
| +# The browser checks, in a real Chromium (RASTRILLO_CHROME, on PATH, or |
| +# the newest in a Playwright cache: Linux's ~/.cache or macOS's |
| +# ~/Library/Caches). Deliberately NOT part of `test`: the gate CI runs on |
| # every push must not need a browser. Run it before shipping a change to |
| # the CSS or to rastrillo's pin — TestNoZoomOnTap is the iOS zoom-on-tap |
| -# check. test-vet vets these files with the tag, so they keep compiling. |
| +# check. Every test in the package runs, the browser ones (the `browser` |
| +# tag) with them, so none is left out by a -run filter that predates it. |
| +# test-vet vets these files with the tag, so they keep compiling. |
| test-browser: |
| - CGO_ENABLED=0 go test -tags browser -count=1 -run 'OnTap' ./internal/home/ |
| + CGO_ENABLED=0 go test -tags browser -count=1 ./internal/home/ |
| .PHONY: credits |
| credits: |
diff --git a/internal/home/chromefinder_test.go b/internal/home/chromefinder_test.go| new file mode 100644 |
| index 0000000..32c1e97 |
| --- /dev/null |
| +++ b/internal/home/chromefinder_test.go |
| @@ -0,0 +1,103 @@ |
| +package home |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "sort" |
| + "strconv" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// playwrightChrome is the newest Chromium in a Playwright browser cache |
| +// under home, or "". rastrillo's harness looks in Linux's cache alone |
| +// (~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome), so on a Mac |
| +// `make test-browser` found nothing unless RASTRILLO_CHROME was set by |
| +// hand. The browser tests (zoom_browser_test.go) hand it this. It lives |
| +// in an untagged file so the plain suite tests the finder itself. |
| +func playwrightChrome(home string) string { |
| + var hits []string |
| + for _, layout := range []string{ |
| + // Linux, as the harness already knows it. |
| + filepath.Join(".cache", "ms-playwright", "chromium-*", "chrome-linux64", "chrome"), |
| + // macOS since Playwright moved to Chrome for Testing. |
| + filepath.Join("Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"), |
| + // macOS before it. |
| + filepath.Join("Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Chromium.app", "Contents", "MacOS", "Chromium"), |
| + } { |
| + m, _ := filepath.Glob(filepath.Join(home, layout)) |
| + hits = append(hits, m...) |
| + } |
| + if len(hits) == 0 { |
| + return "" |
| + } |
| + sort.SliceStable(hits, func(i, j int) bool { return chromiumRevision(hits[i]) > chromiumRevision(hits[j]) }) |
| + return hits[0] |
| +} |
| + |
| +// chromiumRevision is the N of the chromium-N directory in path, or -1. |
| +func chromiumRevision(path string) int { |
| + for _, part := range strings.Split(filepath.ToSlash(path), "/") { |
| + if rest, ok := strings.CutPrefix(part, "chromium-"); ok { |
| + if n, err := strconv.Atoi(rest); err == nil { |
| + return n |
| + } |
| + } |
| + } |
| + return -1 |
| +} |
| + |
| +func TestPlaywrightChromeFindsEveryCacheLayout(t *testing.T) { |
| + touch := func(t *testing.T, path string) { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, nil, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + cft := func(home, rev, arch string) string { |
| + return filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-"+rev, "chrome-"+arch, "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing") |
| + } |
| + |
| + t.Run("none", func(t *testing.T) { |
| + if got := playwrightChrome(t.TempDir()); got != "" { |
| + t.Errorf("empty home: %q", got) |
| + } |
| + }) |
| + t.Run("macOS, Chrome for Testing", func(t *testing.T) { |
| + home := t.TempDir() |
| + want := cft(home, "1234", "mac-arm64") |
| + touch(t, want) |
| + if got := playwrightChrome(home); got != want { |
| + t.Errorf("got %q, want %q", got, want) |
| + } |
| + }) |
| + t.Run("macOS, newest revision wins", func(t *testing.T) { |
| + home := t.TempDir() |
| + touch(t, filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-1148", "chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium")) |
| + touch(t, cft(home, "1228", "mac-arm64")) |
| + want := cft(home, "1234", "mac-arm64") |
| + touch(t, want) |
| + if got := playwrightChrome(home); got != want { |
| + t.Errorf("got %q, want %q", got, want) |
| + } |
| + }) |
| + t.Run("macOS, older Chromium.app", func(t *testing.T) { |
| + home := t.TempDir() |
| + want := filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-1148", "chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium") |
| + touch(t, want) |
| + if got := playwrightChrome(home); got != want { |
| + t.Errorf("got %q, want %q", got, want) |
| + } |
| + }) |
| + t.Run("Linux", func(t *testing.T) { |
| + home := t.TempDir() |
| + want := filepath.Join(home, ".cache", "ms-playwright", "chromium-1234", "chrome-linux64", "chrome") |
| + touch(t, want) |
| + if got := playwrightChrome(home); got != want { |
| + t.Errorf("got %q, want %q", got, want) |
| + } |
| + }) |
| +} |
diff --git a/internal/home/templates/assertion.html b/internal/home/templates/assertion.html| index 3c7887a..723462b 100644 |
| --- a/internal/home/templates/assertion.html |
| +++ b/internal/home/templates/assertion.html |
| @@ -1,9 +1,11 @@ |
| {{define "title"}}Continue to Oficina · Oficina{{end}} |
| {{define "head"}} |
| {{/* The page submits itself (static/assertion.js), so its card stays |
| - hidden for 1.5 s and shows only if the submit stalls; the bar and the |
| - sky's placeholder show at once (layout.html, hop). With scripts off nothing submits, so noscript shows it |
| - at once. Spec shared-chrome §9.3. */}} |
| + hidden for 1.5 s and shows only if the submit stalls. With scripts |
| + off nothing submits, so noscript shows it at once. That rule is spec |
| + shared-chrome §9.3. The bar and the sky's placeholder sit outside the |
| + card and show at once, so the hop is never a blank page: that is |
| + layout.html's hop handling (review r3 C2), not §9.3. */}} |
| <style> |
| [estilo-handoff] { animation: estilo-reveal 0s 1.5s both; } |
| @keyframes estilo-reveal { from { visibility: hidden } to { visibility: visible } } |
diff --git a/internal/home/zoom_browser_test.go b/internal/home/zoom_browser_test.go| index 9fb14ad..23b5f9d 100644 |
| --- a/internal/home/zoom_browser_test.go |
| +++ b/internal/home/zoom_browser_test.go |
| @@ -14,6 +14,8 @@ import ( |
| "context" |
| "fmt" |
| "net/http" |
| + "os" |
| + "os/exec" |
| "strings" |
| "testing" |
| "time" |
| @@ -29,6 +31,29 @@ import ( |
| // zooms for them. |
| const textControls = `input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=submit]):not([type=button]):not([type=color]):not([type=range]), select, textarea` |
| +// useChrome points rastrillo's harness at a Chromium it would not find by |
| +// itself: when RASTRILLO_CHROME is unset and no Chromium is on PATH, the |
| +// newest one in a Playwright cache, the macOS layouts included |
| +// (playwrightChrome, chromefinder_test.go). Call it before harness.New. |
| +func useChrome(t *testing.T) { |
| + t.Helper() |
| + if os.Getenv("RASTRILLO_CHROME") != "" { |
| + return |
| + } |
| + for _, name := range []string{"chromium", "chromium-browser", "google-chrome", "google-chrome-stable"} { |
| + if _, err := exec.LookPath(name); err == nil { |
| + return |
| + } |
| + } |
| + home, err := os.UserHomeDir() |
| + if err != nil { |
| + return |
| + } |
| + if p := playwrightChrome(home); p != "" { |
| + t.Setenv("RASTRILLO_CHROME", p) |
| + } |
| +} |
| + |
| // asHost serves h as if every request had arrived for host. The rig's |
| // browser talks to http://localhost:<port>, and Home answers only its two |
| // configured hosts (serveKnownHost); this is the browser-side half of the |
| @@ -83,6 +108,7 @@ func TestNoZoomOnTap(t *testing.T) { |
| {docsEntryPath, `button[name="team"]`}, |
| }}, |
| } |
| + useChrome(t) |
| for _, host := range hosts { |
| rig := harness.New(t, host.handler) |
| run := func(what string, actions ...chromedp.Action) { |