package calendartest
import (
"net/http"
"net/url"
"regexp"
"strings"
"testing"
)
// B8, first half. Each week day used to carry its own all-day strip
// above its hour column, so a day with a birthday started its timed
// grid ~42px lower than the others and its 10:00 meeting drew beside
// the 11:00 label. The all-day strip is now one row of the grid: every
// all-day cell comes before every timed column in the markup, and
// there is one cell per day whether that day has anything or not.
func TestWeekAllDayIsOneRowAboveEveryTimedColumn(t *testing.T) {
app, session := signedIn(t, "someone@example.com")
h := app.Handler()
createEvent(t, h, session, url.Values{
"title": {"Birthday"}, "all_day": {"1"}, "start": {"2026-10-07"}, "end": {"2026-10-08"},
"kind": {"note"}, "status": {"confirmed"}, "repeat": {"none"},
})
createEvent(t, h, session, url.Values{
"title": {"Focus time"}, "start": {"2026-10-07T10:00"}, "end": {"2026-10-07T11:00"},
"kind": {"commitment"}, "status": {"confirmed"}, "repeat": {"none"},
})
body := getAs(t, h, session, "/week/2026-10-07").Body.String()
cells := strings.Count(body, `cal-allday="week"`)
if cells != 7 {
t.Fatalf("want one all-day cell per day (7), got %d", cells)
}
lastCell := strings.LastIndex(body, `cal-allday="week"`)
firstColumn := strings.Index(body, "
")
if firstColumn < 0 || lastCell > firstColumn {
t.Fatalf("an all-day cell sits inside the timed grid (last cell at %d, first column at %d)", lastCell, firstColumn)
}
if !strings.Contains(body[:firstColumn], "Birthday") {
t.Error("the all-day event is not in the all-day row")
}
if !strings.Contains(body[firstColumn:], "Focus time") {
t.Error("the timed event is not in the timed grid")
}
// A week with nothing all-day draws no all-day row at all.
empty := getAs(t, h, session, "/week/2026-11-04").Body.String()
if strings.Contains(empty, `cal-allday`) {
t.Error("a week with no all-day events drew an all-day row")
}
}
// B8, second half. Under an hour the column has room for one line,
// and a stacked chip put the title on the second — the line the chip's
// overflow cut off. A short chip is marked so time and title share one
// line; an hour or more keeps the stacked layout.
func TestShortEventsKeepTheirTitleLine(t *testing.T) {
app, session := signedIn(t, "someone@example.com")
h := app.Handler()
createEvent(t, h, session, url.Values{
"title": {"Quick sync"}, "start": {"2026-10-07T14:00"}, "end": {"2026-10-07T14:30"},
"kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"none"},
})
createEvent(t, h, session, url.Values{
"title": {"Long review"}, "start": {"2026-10-07T09:00"}, "end": {"2026-10-07T10:00"},
"kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"none"},
})
placed := regexp.MustCompile(`(?s)
]*>.*?([^<]*)`)
for _, path := range []string{"/week/2026-10-07", "/day/2026-10-07"} {
body := getAs(t, h, session, path).Body.String()
got := map[string]string{}
for _, m := range placed.FindAllStringSubmatch(body, -1) {
got[strings.TrimSpace(m[2])] = m[1]
}
if v, ok := got["Quick sync"]; !ok || v != "short" {
t.Errorf("%s: a 30-minute chip is not marked short: %q (found %v)", path, v, got)
}
if v, ok := got["Long review"]; !ok || v != "" {
t.Errorf("%s: an hour-long chip is marked %q, want stacked", path, v)
}
}
}
// B9. An all-day event stores bare dates, and a datetime-local input
// refuses a bare date — the edit form opened with Starts and Ends
// blank and could not be saved without retyping both. The form now
// offers date inputs for an all-day event.
func TestEditingAnAllDayEventShowsItsDates(t *testing.T) {
app, session := signedIn(t, "someone@example.com")
h := app.Handler()
loc := createEvent(t, h, session, url.Values{
"title": {"Conference"}, "all_day": {"1"}, "start": {"2026-10-07"}, "end": {"2026-10-09"},
"kind": {"commitment"}, "status": {"confirmed"}, "repeat": {"none"},
})
body := getAs(t, h, session, loc+"/edit").Body.String()
for name, want := range map[string]string{"start": "2026-10-07", "end": "2026-10-09"} {
input := regexp.MustCompile(`]*\bname="` + name + `"[^>]*>`).FindString(body)
if !strings.Contains(input, `type="date"`) || !strings.Contains(input, `value="`+want+`"`) {
t.Errorf("%s input on an all-day edit form: %s\nwant type=date value=%s", name, input, want)
}
}
// A timed event keeps its datetime inputs.
timed := createEvent(t, h, session, url.Values{
"title": {"Standup"}, "start": {"2026-10-07T09:30"}, "end": {"2026-10-07T09:45"},
"kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"none"},
})
body = getAs(t, h, session, timed+"/edit").Body.String()
input := regexp.MustCompile(`]*\bname="start"[^>]*>`).FindString(body)
if !strings.Contains(input, `type="datetime-local"`) || !strings.Contains(input, `value="2026-10-07T09:30"`) {
t.Errorf("timed edit form start input: %s", input)
}
}
// Without script, ticking or unticking All day cannot swap the input
// under it, so each shape of form must accept the other's values: a
// datetime under All day means its date, and a bare date without it
// means midnight.
func TestAllDayToggleWithoutScriptStillSaves(t *testing.T) {
app, session := signedIn(t, "someone@example.com")
h := app.Handler()
loc := createEvent(t, h, session, url.Values{
"title": {"Offsite"}, "all_day": {"1"}, "start": {"2026-10-07T09:00"}, "end": {"2026-10-08T00:00"},
"kind": {"commitment"}, "status": {"confirmed"}, "repeat": {"none"},
})
body := getAs(t, h, session, loc+"/edit").Body.String()
if in := regexp.MustCompile(`]*\bname="start"[^>]*>`).FindString(body); !strings.Contains(in, `value="2026-10-07"`) {
t.Errorf("an all-day event submitted with a datetime did not store its date: %s", in)
}
cal := regexp.MustCompile(`