oficina / home Public

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>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev d595a02b060d034d7c45c2b81a2302e747012b4a parent c5ed5ea
4 files changed, +141 −7
  • Makefile +7 −4
  • internal/home/chromefinder_test.go +103 −0
  • internal/home/templates/assertion.html +5 −3
  • internal/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) {