oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// The browser drive: the whole app in a real Chromium, wired exactly as
4// cmd/calendar/main.go wires it — loud on any console error, failed
5// request, 4xx/5xx, or template value that leaked to the screen
6// unrendered. Not part of the plain suite:
7//
8// go test -tags browser ./...
9//
10// It needs a Chromium: on PATH, via RASTRILLO_CHROME, or in a
11// Playwright cache. A skip is not a pass — with no browser this fails,
12// unless RASTRILLO_BROWSER_OPTIONAL is set, which makes the skip a
13// deliberate visible choice rather than an accident.
14//
15// This gate exists because every other test in this repo lives inside
16// one layer and agrees with itself: the Go suite asserts on strings the
17// Go code produced. Sheets' rule is that at least one gate must cross
18// from a computed value to painted output, and this is that crossing —
19// a server-side packer emitting inline percentages is exactly the kind
20// of thing that can be arithmetically perfect and visually wrong.
21package calendartest
22
23import (
24 "io"
25 "log/slog"
26 "net/http"
27 "path/filepath"
28 "strings"
29 "testing"
30
31 "amadan.net/rastrillo/rastrillo/harness"
32 "github.com/chromedp/chromedp"
33
34 calendar "amadan.net/oficina/calendar/internal/calendar"
35)
36
37// TestBrowserWalk drives the real screens in a real browser: sign in,
38// create an event through the form, and look at every view.
39func TestBrowserWalk(t *testing.T) {
40 // The rig builds the app inside its own closure, because only it
41 // knows the localhost origin it will serve on — and the origin is
42 // load-bearing: it is the CSRF yardstick and the base of the
43 // emailed sign-in link, so a guessed one breaks both. The handle
44 // is captured on the way past so the test can read the mail
45 // recorder afterwards.
46 var app *calendar.App
47 rig := harness.New(t, func(origin string) http.Handler {
48 a, err := calendar.New(calendar.Config{
49 DBPath: filepath.Join(t.TempDir(), "calendar.db"),
50 Env: "development",
51 Origin: origin,
52 Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
53 })
54 if err != nil {
55 t.Fatalf("calendar.New: %v", err)
56 }
57 t.Cleanup(func() { a.Close() })
58 app = a
59 return a.Handler()
60 })
61
62 // The sign-in page, signed out.
63 rig.Run(chromedp.Navigate(rig.Origin + "/signin"))
64 rig.Screen("body", "signin")
65
66 // Sign in through the actual form, then follow the emailed link —
67 // the same flow a person completes, rather than a forged cookie.
68 rig.Run(
69 chromedp.SendKeys(`input[name="address"]`, "someone@example.com", chromedp.ByQuery),
70 chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
71 chromedp.WaitVisible("body", chromedp.ByQuery),
72 )
73 // The link opens the confirm door; its one Sign in button signs in.
74 link := signInLinkFrom(t, app, rig.Origin)
75 rig.Run(
76 chromedp.Navigate(link),
77 chromedp.WaitVisible(`input[name="token"] ~ div button[type="submit"]`, chromedp.ByQuery),
78 chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
79 chromedp.WaitVisible("body", chromedp.ByQuery),
80 )
81 rig.Screen("body", "signed in")
82
83 // Create an event through the form.
84 rig.Run(
85 chromedp.Navigate(rig.Origin+"/events/new?start=2026-09-02T09:30&end=2026-09-02T10:00"),
86 chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery),
87 chromedp.SendKeys(`input[name="title"]`, "Standup", chromedp.ByQuery),
88 chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
89 chromedp.WaitVisible("body", chromedp.ByQuery),
90 )
91 rig.Screen("body", "the event's own page")
92
93 // Every view, painted.
94 for _, page := range []struct{ path, note string }{
95 {"/day/2026-09-02", "day"},
96 {"/week/2026-09-02", "week"},
97 {"/month/2026-09", "month"},
98 {"/agenda?from=2026-09-02", "agenda"},
99 {"/calendars", "calendars"},
100 {"/settings", "settings"},
101 {"/import", "import"},
102 } {
103 rig.Run(
104 chromedp.Navigate(rig.Origin+page.path),
105 chromedp.WaitVisible("body", chromedp.ByQuery),
106 )
107 rig.Screen("body", page.note)
108 }
109
110 // The crossing that matters: the server packed this chip and gave
111 // it a height in percent. Ask the BROWSER what it actually
112 // measured. A packer that emitted 0%, or a stylesheet that never
113 // loaded, produces a chip with no height — arithmetically fine and
114 // invisible on screen.
115 var height float64
116 rig.Run(
117 chromedp.Navigate(rig.Origin+"/day/2026-09-02"),
118 chromedp.WaitVisible("[cal-placed]", chromedp.ByQuery),
119 chromedp.Evaluate(`document.querySelector('[cal-placed]').getBoundingClientRect().height`, &height),
120 )
121 if height < 8 {
122 t.Errorf("the packed event painted %vpx tall — the server's percentages are not reaching the screen", height)
123 }
124
125 // And the slot links are real, clickable targets rather than
126 // zero-size boxes.
127 var slotHeight float64
128 rig.Run(chromedp.Evaluate(
129 `document.querySelector('[cal-slot]').getBoundingClientRect().height`, &slotHeight))
130 if slotHeight < 8 {
131 t.Errorf("a half-hour slot link painted %vpx tall; it cannot be clicked", slotHeight)
132 }
133}
134
135func signInLinkFrom(t *testing.T, app *calendar.App, origin string) string {
136 t.Helper()
137 body, err := app.LastMailForTest()
138 if err != nil {
139 t.Fatalf("LastMailForTest: %v", err)
140 }
141 i := strings.Index(body, origin+"/auth/verify?token=")
142 if i < 0 {
143 t.Fatalf("no sign-in link for %s in:\n%s", origin, body)
144 }
145 rest := body[i:]
146 if j := strings.IndexAny(rest, " \r\n\t"); j >= 0 {
147 rest = rest[:j]
148 }
149 return rest
150}
151