oficina / calendar Public

TestFormFootOnANarrowScreen: the booker's buttons squeeze on a phone (red)

v0.26's form-foot draws its buttons at the lg size. On the stranger's
booking form, "Hold this time" and "Pick another time" no longer fit
side by side at 390 wide, and the row squeezes them into two-line
labels (three at 320, where the old size already broke them over two).
The test measures, at 320 and 390, that no form-foot label breaks over
lines and no button leaves its form. Red on purpose; output in
polish-audit/r3/scripts/migrate-calendar-formfoot-red.txt. make browser
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 570e90ce9ed458fffd3c962b80382370eee4ebfe parent c974f55
2 files changed, +113 −4
  • Makefile +5 −4
  • internal/calendartest/formfoot_browser_test.go +108 −0
diff --git a/Makefile b/Makefile
index 877d7dd..e74f5f8 100644
--- a/Makefile
+++ b/Makefile
@@ -123,12 +123,13 @@ ci: vet fmt-check test migration-check
# toolchain the app itself does not use. Run it before shipping a
# change to the box.
#
-# TestNoZoomOnTap (the iOS zoom-on-tap check) drives Chromium through
-# chromedp instead, and finds it through RASTRILLO_CHROME; on a Mac the
-# Playwright cache's Chromium is used when that is unset.
+# TestNoZoomOnTap (the iOS zoom-on-tap check) and
+# TestFormFootOnANarrowScreen drive Chromium through chromedp instead,
+# and find it through RASTRILLO_CHROME; on a Mac the Playwright cache's
+# Chromium is used when that is unset.
browser:
npm install --no-save playwright
- RASTRILLO_CHROME="$(RASTRILLO_CHROME)" go test -tags browser -run 'InARealBrowser|OnTap' ./internal/calendartest/
+ RASTRILLO_CHROME="$(RASTRILLO_CHROME)" go test -tags browser -run 'InARealBrowser|OnTap|OnANarrowScreen' ./internal/calendartest/
RASTRILLO_CHROME ?= $(firstword $(wildcard $(HOME)/Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium))
diff --git a/internal/calendartest/formfoot_browser_test.go b/internal/calendartest/formfoot_browser_test.go
new file mode 100644
index 0000000..65d1509
--- /dev/null
+++ b/internal/calendartest/formfoot_browser_test.go
@@ -0,0 +1,108 @@
+//go:build browser
+
+package calendartest
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "amadan.net/rastrillo/rastrillo/harness"
+ "github.com/chromedp/cdproto/emulation"
+ "github.com/chromedp/chromedp"
+
+ calendar "amadan.net/oficina/calendar/internal/calendar"
+)
+
+// TestFormFootOnANarrowScreen: rastrillo's form-foot draws its two
+// buttons at the lg size (v0.26). On a phone the booker's pair, "Hold
+// this time" and "Pick another time", no longer fit side by side, and the
+// row squeezed them instead of wrapping: two lines of label each at 390
+// wide, up to three at 320 (where the old size already broke them over
+// two). calendar.css lets the row wrap. This checks
+// at 320 and 390 that no form-foot label breaks across lines and no
+// button leaves the form, on the stranger's booking form, the one
+// most often opened on a phone.
+func TestFormFootOnANarrowScreen(t *testing.T) {
+ var app *calendar.App
+ rig := harness.New(t, func(origin string) http.Handler {
+ a, err := calendar.New(calendar.Config{
+ DBPath: filepath.Join(t.TempDir(), "calendar.db"),
+ Env: "development",
+ Origin: origin,
+ Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
+ })
+ if err != nil {
+ t.Fatalf("calendar.New: %v", err)
+ }
+ t.Cleanup(func() { a.Close() })
+ app = a
+ return a.Handler()
+ })
+ signInOverHTTP(t, app, rig.Origin, "host@example.com")
+ ctx := context.Background()
+ st := app.StoreForTest()
+ host := app.PersonByAddressForTest(t, "host@example.com")
+ if err := st.SaveHandle(ctx, host.ID, "paul"); err != nil {
+ t.Fatal(err)
+ }
+ ws, _ := calendar.ParseAvailability("every day 9-5")
+ page, err := st.CreateBookingPage(ctx, host.ID, calendar.BookingPageInput{
+ Title: "Quick chat", Description: "Half an hour.", Availability: "every day 9-5",
+ DurationMin: 30, HorizonDays: 30}, ws)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := st.PublishBookingPage(ctx, host.ID, page.ID, true); err != nil {
+ t.Fatal(err)
+ }
+ public := "/book/paul/" + page.Code
+
+ for _, width := range []int64{320, 390} {
+ var day string
+ var problems []string
+ ctx, cancel := context.WithTimeout(rig.Context(), 20*time.Second)
+ err := chromedp.Run(ctx,
+ emulation.SetDeviceMetricsOverride(width, 844, 1, true),
+ chromedp.Navigate(rig.Origin+public),
+ chromedp.WaitVisible(`select[name="zone"]`, chromedp.ByQuery),
+ chromedp.Evaluate(`(document.querySelector('a[href*="day="]') || {}).href || ''`, &day),
+ )
+ if err == nil && day == "" {
+ err = fmt.Errorf("the booking page offers no day to pick")
+ }
+ if err == nil {
+ err = chromedp.Run(ctx,
+ chromedp.Navigate(day),
+ chromedp.WaitVisible(`[cal-book-slot]`, chromedp.ByQuery),
+ chromedp.Click(`[cal-book-slot]`, chromedp.ByQuery),
+ chromedp.WaitVisible(`[cal-book-details] [rst-form-foot]`, chromedp.ByQuery),
+ chromedp.Evaluate(footProblems, &problems),
+ )
+ }
+ cancel()
+ if err != nil {
+ t.Fatalf("%dpx: %v", width, err)
+ }
+ for _, p := range problems {
+ t.Errorf("%dpx, the booker's form: %s", width, p)
+ }
+ }
+}
+
+// footProblems lists every form-foot button whose label breaks across
+// lines, or which reaches past its form's edge.
+const footProblems = `[...document.querySelectorAll('[rst-form-foot] [rst-btn]')].flatMap(b => {
+ const out = [], r = document.createRange()
+ r.selectNodeContents(b)
+ const lines = new Set([...r.getClientRects()].filter(x => x.width > 0).map(x => Math.round(x.top))).size
+ if (lines > 1) out.push('"' + b.textContent.trim() + '" breaks over ' + lines + ' lines')
+ const f = b.closest('form').getBoundingClientRect(), bb = b.getBoundingClientRect()
+ if (bb.right > f.right + 0.5 || bb.left < f.left - 0.5) out.push('"' + b.textContent.trim() + '" spans ' + Math.round(bb.left) + '-' + Math.round(bb.right) + ', its form ' + Math.round(f.left) + '-' + Math.round(f.right))
+ return out
+})`