Files
8bitMyGoUI/datetime/views_test.go
T
ZacharyZhang-NYandClaude Opus 5.5 03e1f91706 Upgrade MyGo to v0.5.5
Elements are now pass-scoped value handles: tests that read bounds
after a frame bind a ui.Handle. pixel tells frames apart by the build
pass's root, since the Tester clock no longer moves on its own.

Fix the regressions the upgrade brought:
- Clipboard reads in input handlers go through c.Services().
- MyGo notes an overlay's opener after building it, so AutoFocus
  inside lost it; pixel.AutoFocus focuses through a Handle, and
  archtest bans the raw call. Dialogs hand the focus back again.
- Elements around a press stay hovered: Tooltip tells a press-given
  focus by focus arriving under the pointer.
- The web host moves the Tester clock with real time, at most 500 ms
  a call, and gives components the real clock.

The gallery's source index also accepts CRLF checkouts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:31:08 -04:00

288 lines
9.6 KiB
Go

package datetime_test
import (
"slices"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/datetime"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestSunDialDragsHoursAndShowsTheSky(t *testing.T) {
hour, changes := 9, 0
tt := view(400, 200, newClock(), false, func(c *ui.Context) {
if datetime.SunDial(c, "Hour", &hour, datetime.SunDialOptions{}).Changed {
changes++
}
})
r := find(t, tt, "Hour")
tt.Press(r.X+r.W/2+2, r.Y+20)
tt.Release(r.X+r.W/2+2, r.Y+20)
if hour != 12 {
t.Fatalf("a press at the middle set %d", hour)
}
// Noon puts the sun at the top of the arc.
if n := count(t, tt.Image(), ui.Rect{X: r.X + r.W/2 - 12, Y: r.Y + 2*(56-6-40) - 12, W: 24, H: 24}, pixel.Warn); n == 0 {
t.Fatal("no sun at the top at noon")
}
tt.Key(0, ui.KeyLeft)
tt.Key(0, ui.KeyLeft)
if hour != 10 || changes != 3 {
t.Fatalf("Left, Left gave %d, %d changes", hour, changes)
}
hour = 23
tt.Frame()
// At 23:00 the moon sits left of the top.
moon := ui.Rect{X: r.X + 76 - 12, Y: r.Y + 22 - 12, W: 24, H: 24}
if count(t, tt.Image(), moon, pixel.Warn) == 0 || count(t, tt.Image(), r, pixel.Shade) < 100 {
t.Fatal("no moon at eleven at night")
}
keeps(t, tt.Image())
}
func TestSeasonWheelPicksByClickAndKeys(t *testing.T) {
season := pixel.Spring
tt := view(300, 200, newClock(), false, func(c *ui.Context) {
datetime.SeasonWheel(c, "Season", &season, datetime.SeasonWheelOptions{})
})
r := find(t, tt, "Season")
tt.ClickAt(r.X+20, r.Y+r.H-20)
if season != pixel.Autumn {
t.Fatalf("a click at the bottom left chose %v", season)
}
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyRight)
if season != pixel.Spring {
t.Fatalf("Right twice from autumn chose %v", season)
}
keeps(t, tt.Image())
}
func TestWeekStripStepsAcrossWeeks(t *testing.T) {
date := time.Date(2026, 10, 17, 0, 0, 0, 0, time.UTC)
tt := view(400, 100, newClock(), false, func(c *ui.Context) {
datetime.WeekStrip(c, "Week", &date, datetime.WeekStripOptions{})
})
r := find(t, tt, "Week")
tt.ClickAt(r.X+4, r.Y+4)
if day(date) != "2026-10-11" {
t.Fatalf("a click on Sunday gave %s", day(date))
}
tt.Key(0, ui.KeyLeft)
if day(date) != "2026-10-10" {
t.Fatalf("Left from Sunday gave %s", day(date))
}
keeps(t, tt.Image())
}
var raid = time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC)
func TestAgendaGroupsByDay(t *testing.T) {
events := []datetime.Event{
{Start: raid.Add(26 * time.Hour), Title: "Shop"},
{Start: raid, Title: "Raid"},
{Start: raid.Add(3 * time.Hour), Title: "Lunch"},
}
tt := view(300, 300, newClock(), false, func(c *ui.Context) {
datetime.Agenda(c, events, datetime.AgendaOptions{})
})
a, b, s := find(t, tt, "SU 11 OCT"), find(t, tt, "09:00 Raid"), find(t, tt, "MO 12 OCT")
l, sh := find(t, tt, "12:00 Lunch"), find(t, tt, "11:00 Shop")
if !(a.Y < b.Y && b.Y < l.Y && l.Y < s.Y && s.Y < sh.Y) {
t.Fatalf("order: %q", tt.Texts())
}
keeps(t, tt.Image())
}
func TestAlarmLeverAndBadTimes(t *testing.T) {
alarms := []datetime.AlarmEntry{{Hour: 7, Minute: 30, Days: [7]bool{1: true, 2: true}}}
changes := 0
tt := view(400, 100, newClock(), false, func(c *ui.Context) {
if datetime.Alarm(c, "Alarms", alarms, datetime.AlarmOptions{}).Changed {
changes++
}
})
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeySpace)
if !alarms[0].On || changes != 1 {
t.Fatalf("the lever left On %v", alarms[0].On)
}
defer func() {
if recover() == nil {
t.Fatal("25:00 did not panic")
}
}()
view(100, 100, newClock(), false, func(c *ui.Context) {
datetime.Alarm(c, "x", []datetime.AlarmEntry{{Hour: 25}}, datetime.AlarmOptions{})
})
}
func TestYearViewMarksToday(t *testing.T) {
tt := view(600, 400, newClock(), false, func(c *ui.Context) {
datetime.YearView(c, 2026, datetime.YearViewOptions{})
})
r := find(t, tt, "2026")
if n := count(t, tt.Image(), r, pixel.Accent); n == 0 {
t.Fatal("today is not marked")
}
keeps(t, tt.Image())
}
func TestDaySchedulePlacesAndSplitsOverlaps(t *testing.T) {
events := []datetime.Event{
{Start: raid, Length: 2 * time.Hour, Title: "Raid"},
{Start: raid.Add(time.Hour), Length: time.Hour, Title: "Call"},
{Start: raid.Add(4 * time.Hour), Length: 30 * time.Minute, Title: "Lunch"},
}
tt := view(400, 500, newClock(), false, func(c *ui.Context) {
datetime.DaySchedule(c, events, datetime.DayScheduleOptions{})
})
tt.Frame()
a, b, l := find(t, tt, "09:00 Raid"), find(t, tt, "10:00 Call"), find(t, tt, "13:00 Lunch")
// Hours are 40 pixels; nine o'clock is the second row.
if a.Y != 40 || b.Y != 80 || b.X <= a.X || l.X != a.X || a.H != 80 || l.W < 2*a.W {
t.Fatalf("raid %v, call %v, lunch %v", a, b, l)
}
keeps(t, tt.Image())
}
func TestEventChipReadsTimeAndTitle(t *testing.T) {
tt := view(300, 60, newClock(), false, func(c *ui.Context) {
datetime.EventChip(c, datetime.Event{Start: raid, Title: "Raid"}, datetime.EventChipOptions{})
})
if !shows(tt, "09:00 Raid") {
t.Fatalf("texts %q", tt.Texts())
}
keeps(t, tt.Image())
}
func TestOffDateTimeControlsStayPut(t *testing.T) {
for _, inert := range []bool{false, true} {
off := !inert
tm := raid
week := raid
month, year, hour, season := raid, 2026, 9, pixel.Spring
alarms := []datetime.AlarmEntry{{Hour: 7}}
started := 0
tt := view(800, 600, newClock(), inert, func(c *ui.Context) {
ui.Row(c).Gap(8).Children(func() {
ui.Column(c).Gap(8).Children(func() {
datetime.TimeInput(c, "Time", &tm, datetime.TimeInputOptions{Disabled: off})
datetime.WeekStrip(c, "Week", &week, datetime.WeekStripOptions{Disabled: off})
datetime.MonthPicker(c, "Month", &month, datetime.MonthPickerOptions{Disabled: off})
datetime.YearPicker(c, "Year", &year, datetime.YearPickerOptions{Disabled: off})
})
ui.Column(c).Gap(8).Children(func() {
datetime.SunDial(c, "Hour", &hour, datetime.SunDialOptions{Disabled: off})
datetime.SeasonWheel(c, "Season", &season, datetime.SeasonWheelOptions{Disabled: off})
datetime.Alarm(c, "Alarms", alarms, datetime.AlarmOptions{Disabled: off})
if datetime.Stopwatch(c, "Run", datetime.StopwatchOptions{Disabled: off}).Started {
started++
}
})
})
})
for _, l := range []string{"Time", "Week", "Month", "Year", "Season", "START"} {
r := find(t, tt, l)
tt.ClickAt(r.X+r.W-4, r.Y+r.H-4)
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyUp)
tt.Key(0, ui.KeyEnter)
}
h := find(t, tt, "Hour")
tt.Press(h.X+4, h.Y+4)
tt.Release(h.X+4, h.Y+4)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeySpace)
if !tm.Equal(raid) || !week.Equal(raid) || !month.Equal(raid) || year != 2026 || hour != 9 || season != pixel.Spring || alarms[0].On || started != 0 {
t.Fatalf("inert %v: off controls changed", inert)
}
}
}
func TestDayScheduleKeepsLateBlocksInside(t *testing.T) {
late := time.Date(2026, 10, 11, 17, 55, 0, 0, time.UTC)
tt := view(400, 500, newClock(), false, func(c *ui.Context) {
datetime.DaySchedule(c, []datetime.Event{{Start: late, Length: 5 * time.Minute, Title: "Bell"}}, datetime.DayScheduleOptions{})
})
tt.Frame()
// Five minutes take the three-dot least: six pixels.
if b := find(t, tt, "17:55 Bell"); b.Y+b.H > 400 || b.H != 6 {
t.Fatalf("a late five-minute block is %v", b)
}
}
func TestMonthPickerFitsChineseNames(t *testing.T) {
month := raid
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: newClock().now, Locale: pixel.Zh})
datetime.MonthPicker(c, "Month", &month, datetime.MonthPickerOptions{})
}, 600, 300)
if w := find(t, tt, "Month").W; w < 4*float32(pixel.Measure("十二月")+2)*2 {
t.Fatalf("cells of %v pixels for four 十二月", w/4)
}
keeps(t, tt.Image())
}
func TestTodayMarksMoveAtMidnight(t *testing.T) {
clk := newClock()
clk.t = time.Date(2026, 10, 11, 23, 59, 59, 0, time.UTC)
date := clk.t
for name, build := range map[string]func(c *ui.Context){
"calendar": func(c *ui.Context) { datetime.Calendar(c, "Date", &date, datetime.CalendarOptions{}) },
"week": func(c *ui.Context) { datetime.WeekStrip(c, "Week", &date, datetime.WeekStripOptions{}) },
"year": func(c *ui.Context) { datetime.YearView(c, 2026, datetime.YearViewOptions{}) },
} {
tt := view(600, 400, clk, false, build)
pixel.ResetDue()
before := tt.Now()
tt.Frame()
if due := pixel.Due(); due.IsZero() || due.Sub(before) > 2*time.Second {
t.Fatalf("%s: no rebuild asked for midnight, due %v", name, due)
}
marks := accents(t, tt)
clk.t = clk.t.Add(2 * time.Second)
tt.Frame()
after := accents(t, tt)
clk.t = clk.t.Add(-2 * time.Second)
if len(marks) == 0 || len(after) == 0 || slices.Equal(marks, after) {
t.Fatalf("%s: the today mark did not move at midnight", name)
}
}
}
func TestRelativeTimeRoundsTheFutureUp(t *testing.T) {
clk := newClock()
for _, tc := range []struct {
ahead time.Duration
words string
wait time.Duration
}{{30 * time.Second, "IN 1 MIN", 30 * time.Second}, {90 * time.Second, "IN 2 MIN", 30 * time.Second}, {2 * time.Minute, "IN 2 MIN", time.Minute}} {
at := clk.t.Add(tc.ahead)
tt := view(300, 60, clk, false, func(c *ui.Context) { datetime.RelativeTime(c, at, datetime.RelativeTimeOptions{}) })
pixel.ResetDue()
before := tt.Now()
tt.Frame()
if wait := pixel.Due().Sub(before); !shows(tt, tc.words) || wait < tc.wait-time.Second || wait > tc.wait+time.Second {
t.Fatalf("%v ahead: %q, next repaint in %v", tc.ahead, tt.Texts(), wait)
}
}
}
// accents lists where Accent dots are.
func accents(t *testing.T, tt *ui.Tester) []int {
t.Helper()
img, out := tt.Image(), []int{}
for y := 0; y < img.Bounds().Dy(); y += 2 {
for x := 0; x < img.Bounds().Dx(); x += 2 {
if slot(t, img, x, y) == pixel.Accent {
out = append(out, y*img.Bounds().Dx()+x)
}
}
}
return out
}