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
3package calendartest
4
5import (
6 "context"
7 "io"
8 "log/slog"
9 "net"
10 "net/http"
11 "net/url"
12 "os/exec"
13 "path/filepath"
14 "regexp"
15 "strings"
16 "testing"
17 "time"
18
19 calendar "amadan.net/oficina/calendar/internal/calendar"
20)
21
22// TestViewSwitchingHoldsStillInARealBrowser is Paul's "views jumping"
23// complaint as a gate: switching Day, Week, Month and Agenda, and stepping
24// each with Previous and Next, moves none of the page's controls and
25// shifts no layout, at 1440 and 390 wide. Each hop is a full document
26// load, so no single-page CLS figure sees the jump; the node script
27// compares boxes across documents. See test/browser/views.mjs.
28func TestViewSwitchingHoldsStillInARealBrowser(t *testing.T) {
29 ln, err := net.Listen("tcp", "127.0.0.1:0")
30 if err != nil {
31 t.Fatalf("listen: %v", err)
32 }
33 origin := "http://" + ln.Addr().String()
34 app, err := calendar.New(calendar.Config{
35 DBPath: filepath.Join(t.TempDir(), "calendar.db"),
36 Env: "development",
37 Origin: origin,
38 Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
39 })
40 if err != nil {
41 t.Fatalf("calendar.New: %v", err)
42 }
43 t.Cleanup(func() { app.Close() })
44 srv := &http.Server{Handler: app.Handler()}
45 go func() { _ = srv.Serve(ln) }()
46 t.Cleanup(func() { _ = srv.Close() })
47
48 session := signInOverHTTP(t, app, origin, "someone@example.com")
49 client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
50 do := func(method, path string, form url.Values) *http.Response {
51 t.Helper()
52 var body io.Reader
53 if form != nil {
54 body = strings.NewReader(form.Encode())
55 }
56 req, _ := http.NewRequest(method, origin+path, body)
57 if form != nil {
58 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
59 req.Header.Set("Origin", origin)
60 }
61 req.AddCookie(session)
62 res, err := client.Do(req)
63 if err != nil {
64 t.Fatal(err)
65 }
66 return res
67 }
68 res := do("GET", "/events/new", nil)
69 page, _ := io.ReadAll(res.Body)
70 res.Body.Close()
71 m := regexp.MustCompile(`<option value="(\d+)"[^>]*\bselected\b`).FindSubmatch(page)
72 if m == nil {
73 t.Fatal("no default calendar on the create form")
74 }
75 // Content in every view, including an all-day event (it adds a row
76 // in Day and Week) and a busy day (Month's "+N more").
77 for _, v := range []url.Values{
78 {"title": {"Team offsite"}, "all_day": {"1"}, "start": {"2026-09-23"}, "end": {"2026-09-24"}, "kind": {"commitment"}},
79 {"title": {"Design review"}, "start": {"2026-09-24T10:00"}, "end": {"2026-09-24T11:00"}, "kind": {"meeting"}},
80 {"title": {"Lunch with Ana"}, "start": {"2026-09-24T13:00"}, "end": {"2026-09-24T14:00"}, "kind": {"commitment"}},
81 {"title": {"Planning"}, "start": {"2026-09-22T15:00"}, "end": {"2026-09-22T17:00"}, "kind": {"meeting"}},
82 {"title": {"Standup"}, "start": {"2026-09-24T09:30"}, "end": {"2026-09-24T09:45"}, "kind": {"meeting"}},
83 {"title": {"Dentist"}, "start": {"2026-09-24T16:00"}, "end": {"2026-09-24T16:30"}, "kind": {"commitment"}},
84 } {
85 v.Set("calendar_id", string(m[1]))
86 v.Set("status", "confirmed")
87 v.Set("repeat", "none")
88 res := do("POST", "/events", v)
89 res.Body.Close()
90 if res.StatusCode != http.StatusSeeOther {
91 t.Fatalf("create %q: status %d", v.Get("title"), res.StatusCode)
92 }
93 }
94
95 ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
96 defer cancel()
97 cmd := exec.CommandContext(ctx, "node", "test/browser/views.mjs", origin, session.Name, session.Value)
98 cmd.Dir = repoRootForBrowser(t)
99 out, err := cmd.CombinedOutput()
100 t.Logf("node:\n%s", out)
101 if err != nil {
102 t.Fatalf("the browser checks failed: %v", err)
103 }
104}
105