package main import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" ) // These tests pin the calendar-grid layout contract, written against the // second bidbuddy.com.au report (2026-06-10, 20:59): the grid renders fixed // Sun–Sat column headers but poured consecutive day buttons in from column 1 // with no offset, so June 10 (a Wednesday) sat under "Sun" and tapping the // cell under "Tue" (showing 12) selected Friday, June 12. The first date must // be preceded by int(weekday) spacer cells so every button lands under its // real weekday header. "Today" (window start + IsPast/IsToday flags) must be // resolved in the visitor/site timezone, not the server clock's. // divergentZone returns an IANA zone whose current calendar date differs from // the server-local date right now. Pacific/Kiritimati (UTC+14) and // Pacific/Pago_Pago (UTC-11) are 25 hours apart, so at any instant at least // one of them is on a different date than the server. func divergentZone(t *testing.T) (*time.Location, string) { t.Helper() serverToday := time.Now().Format("2006-01-02") for _, name := range []string{"Pacific/Kiritimati", "Pacific/Pago_Pago"} { loc, err := time.LoadLocation(name) if err != nil { t.Fatalf("LoadLocation(%s): %v", name, err) } if time.Now().In(loc).Format("2006-01-02") != serverToday { return loc, name } } t.Fatal("no zone diverges from server-local date — zones are 25h apart, impossible") return nil, "" } // firstDateParam extracts the YYYY-MM-DD value of the first date= query param // in rendered HTML — i.e. the first day button of the grid. func firstDateParam(t *testing.T, html string) string { t.Helper() i := strings.Index(html, "date=") if i < 0 || i+15 > len(html) { t.Fatalf("no date= param found in rendered HTML") } return html[i+5 : i+15] } func TestLeadingBlanks(t *testing.T) { cell := func(date string) DateCell { return DateCell{Date: date} } cases := []struct { name string dates []DateCell want int }{ {"wednesday start needs 3 pads", []DateCell{cell("2026-06-10")}, 3}, {"sunday start needs none", []DateCell{cell("2026-06-14")}, 0}, {"saturday start needs 6", []DateCell{cell("2026-06-13")}, 6}, {"empty grid", nil, 0}, {"unparseable date", []DateCell{cell("garbage")}, 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := leadingBlanks(tc.dates); got != tc.want { t.Errorf("leadingBlanks(%v) = %d, want %d", tc.dates, got, tc.want) } }) } } // TestHandleGetDateGrid_AlignsFirstDateToWeekdayColumn drives /date-grid with // a future Wednesday weekStart and asserts exactly 3 spacer cells render // before the first day button, so the 7-column grid lines dates up with the // Sun–Sat headers. func TestHandleGetDateGrid_AlignsFirstDateToWeekdayColumn(t *testing.T) { router := newTestRouter(t) // Next Wednesday at least a week out, so it is never clamped to today. start := time.Now().AddDate(0, 0, 7) for start.Weekday() != time.Wednesday { start = start.AddDate(0, 0, 1) } req := httptest.NewRequest(http.MethodGet, "/date-grid?blockId=b-align&username=alice&eventType=30min&weeks=2&weekStart="+start.Format("2006-01-02"), nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d (body: %s)", rec.Code, rec.Body.String()) } body := rec.Body.String() if got := strings.Count(body, "calcom-date-pad"); got != 3 { t.Errorf("Wednesday window: want 3 leading pad cells, got %d", got) } padIdx := strings.Index(body, "calcom-date-pad") btnIdx := strings.Index(body, "date=") if padIdx < 0 || btnIdx < 0 || padIdx > btnIdx { t.Errorf("pad cells must precede the first day button (padIdx=%d, btnIdx=%d)", padIdx, btnIdx) } if !strings.Contains(body, `calcom-date-pad" aria-hidden="true"`) { t.Errorf("pad cells must be aria-hidden") } } // TestHandleGetDateGrid_SundayStartHasNoPads is the zero case: a window // starting on a Sunday begins flush in column 1. func TestHandleGetDateGrid_SundayStartHasNoPads(t *testing.T) { router := newTestRouter(t) start := time.Now().AddDate(0, 0, 7) for start.Weekday() != time.Sunday { start = start.AddDate(0, 0, 1) } req := httptest.NewRequest(http.MethodGet, "/date-grid?blockId=b-sun&username=alice&eventType=30min&weeks=2&weekStart="+start.Format("2006-01-02"), nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if got := strings.Count(rec.Body.String(), "calcom-date-pad"); got != 0 { t.Errorf("Sunday window: want 0 pad cells, got %d", got) } } // TestHandleGetDateGrid_TodayResolvedInEventTimezone pins the clamp rule to // the MEETING's calendar, not the server's: a past weekStart must clamp to // today in the event's schedule timezone. Pre-fix the handler clamped to the // server-local date, so a UTC container made a Perth schedule's grid start on // yesterday until 08:00 AWST. func TestHandleGetDateGrid_TodayResolvedInEventTimezone(t *testing.T) { loc, name := divergentZone(t) fakeCalcom(t, name, nil) h, _ := newKeyedHandler(t) wantToday := time.Now().In(loc).Format("2006-01-02") req := httptest.NewRequest(http.MethodGet, "/date-grid?blockId=b-tz&username=alice&eventType=30min&weeks=2&weekStart=2020-01-01", nil) rec := httptest.NewRecorder() h.HandleGetDateGrid(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d (body: %s)", rec.Code, rec.Body.String()) } if got := firstDateParam(t, rec.Body.String()); got != wantToday { t.Errorf("clamped window must start at today in %s (%s), got %s", name, wantToday, got) } } // TestCalcomBookingFunc_InitialGridUsesSiteTimezone covers the initial // server-side block render with a cold event-timezone cache: the window must // start at today in the SITE timezone (Site Settings → Localization), // resolved via the settings manager stashed at router-mount time — not at // the server clock's date. Page render never calls Cal.com. func TestCalcomBookingFunc_InitialGridUsesSiteTimezone(t *testing.T) { s, q := newTestSettings() loc, name := divergentZone(t) seedSiteTimezone(q, name) renderSettings = s resetEventTZCache() t.Cleanup(func() { renderSettings = nil }) html := CalcomBookingFunc(context.Background(), map[string]any{ "_block_id": "b-init", "username": "alice", "eventTypeSlug": "30min", }) wantToday := time.Now().In(loc).Format("2006-01-02") if got := firstDateParam(t, html); got != wantToday { t.Errorf("initial grid must start at today in site tz %s (%s), got %s", name, wantToday, got) } wantPads := int(time.Now().In(loc).Weekday()) if got := strings.Count(html, "calcom-date-pad"); got != wantPads { t.Errorf("initial grid: want %d pad cells for %s, got %d", wantPads, wantToday, got) } } // TestCalcomBookingFunc_InitialGridPrefersCachedEventTimezone asserts the // initial render uses the meeting timezone over the site setting once the // cache is warm (any visitor's first HTMX interaction warms it). func TestCalcomBookingFunc_InitialGridPrefersCachedEventTimezone(t *testing.T) { s, q := newTestSettings() loc, name := divergentZone(t) seedSiteTimezone(q, "UTC") // site setting present but unset-equivalent storeEventTZ("alice", "30min", name) renderSettings = s t.Cleanup(func() { renderSettings = nil resetEventTZCache() }) html := CalcomBookingFunc(context.Background(), map[string]any{ "_block_id": "b-init2", "username": "alice", "eventTypeSlug": "30min", }) wantToday := time.Now().In(loc).Format("2006-01-02") if got := firstDateParam(t, html); got != wantToday { t.Errorf("initial grid must start at today in cached event tz %s (%s), got %s", name, wantToday, got) } if !strings.Contains(html, name) { t.Errorf("ShowTimezone label should carry the event tz %s", name) } }