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(`