//go:build browser // The browser drive: the whole app in a real Chromium, wired exactly as // cmd/calendar/main.go wires it — loud on any console error, failed // request, 4xx/5xx, or template value that leaked to the screen // unrendered. Not part of the plain suite: // // go test -tags browser ./... // // It needs a Chromium: on PATH, via RASTRILLO_CHROME, or in a // Playwright cache. A skip is not a pass — with no browser this fails, // unless RASTRILLO_BROWSER_OPTIONAL is set, which makes the skip a // deliberate visible choice rather than an accident. // // This gate exists because every other test in this repo lives inside // one layer and agrees with itself: the Go suite asserts on strings the // Go code produced. Sheets' rule is that at least one gate must cross // from a computed value to painted output, and this is that crossing — // a server-side packer emitting inline percentages is exactly the kind // of thing that can be arithmetically perfect and visually wrong. package calendartest import ( "io" "log/slog" "net/http" "path/filepath" "strings" "testing" "amadan.net/rastrillo/rastrillo/harness" "github.com/chromedp/chromedp" calendar "amadan.net/oficina/calendar/internal/calendar" ) // TestBrowserWalk drives the real screens in a real browser: sign in, // create an event through the form, and look at every view. func TestBrowserWalk(t *testing.T) { // The rig builds the app inside its own closure, because only it // knows the localhost origin it will serve on — and the origin is // load-bearing: it is the CSRF yardstick and the base of the // emailed sign-in link, so a guessed one breaks both. The handle // is captured on the way past so the test can read the mail // recorder afterwards. 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() }) // The sign-in page, signed out. rig.Run(chromedp.Navigate(rig.Origin + "/signin")) rig.Screen("body", "signin") // Sign in through the actual form, then follow the emailed link — // the same flow a person completes, rather than a forged cookie. rig.Run( chromedp.SendKeys(`input[name="address"]`, "someone@example.com", chromedp.ByQuery), chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), chromedp.WaitVisible("body", chromedp.ByQuery), ) // The link opens the confirm door; its one Sign in button signs in. link := signInLinkFrom(t, app, rig.Origin) rig.Run( chromedp.Navigate(link), chromedp.WaitVisible(`input[name="token"] ~ div button[type="submit"]`, chromedp.ByQuery), chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), chromedp.WaitVisible("body", chromedp.ByQuery), ) rig.Screen("body", "signed in") // Create an event through the form. rig.Run( chromedp.Navigate(rig.Origin+"/events/new?start=2026-09-02T09:30&end=2026-09-02T10:00"), chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery), chromedp.SendKeys(`input[name="title"]`, "Standup", chromedp.ByQuery), chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), chromedp.WaitVisible("body", chromedp.ByQuery), ) rig.Screen("body", "the event's own page") // Every view, painted. for _, page := range []struct{ path, note string }{ {"/day/2026-09-02", "day"}, {"/week/2026-09-02", "week"}, {"/month/2026-09", "month"}, {"/agenda?from=2026-09-02", "agenda"}, {"/calendars", "calendars"}, {"/settings", "settings"}, {"/import", "import"}, } { rig.Run( chromedp.Navigate(rig.Origin+page.path), chromedp.WaitVisible("body", chromedp.ByQuery), ) rig.Screen("body", page.note) } // The crossing that matters: the server packed this chip and gave // it a height in percent. Ask the BROWSER what it actually // measured. A packer that emitted 0%, or a stylesheet that never // loaded, produces a chip with no height — arithmetically fine and // invisible on screen. var height float64 rig.Run( chromedp.Navigate(rig.Origin+"/day/2026-09-02"), chromedp.WaitVisible("[cal-placed]", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('[cal-placed]').getBoundingClientRect().height`, &height), ) if height < 8 { t.Errorf("the packed event painted %vpx tall — the server's percentages are not reaching the screen", height) } // And the slot links are real, clickable targets rather than // zero-size boxes. var slotHeight float64 rig.Run(chromedp.Evaluate( `document.querySelector('[cal-slot]').getBoundingClientRect().height`, &slotHeight)) if slotHeight < 8 { t.Errorf("a half-hour slot link painted %vpx tall; it cannot be clicked", slotHeight) } } func signInLinkFrom(t *testing.T, app *calendar.App, origin string) string { t.Helper() body, err := app.LastMailForTest() if err != nil { t.Fatalf("LastMailForTest: %v", err) } i := strings.Index(body, origin+"/auth/verify?token=") if i < 0 { t.Fatalf("no sign-in link for %s in:\n%s", origin, body) } rest := body[i:] if j := strings.IndexAny(rest, " \r\n\t"); j >= 0 { rest = rest[:j] } return rest }