237 lines
7.0 KiB
Go
237 lines
7.0 KiB
Go
package mujica
|
||
|
||
import (
|
||
"image/color"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"mujicaui/theme"
|
||
)
|
||
|
||
// datePickerLive builds a DatePicker whose options a test changes between frames.
|
||
func datePickerLive(value *Date, opts *DatePickerOptions, changes *int) *ui.Tester {
|
||
return ui.NewTester(func(c *ui.Context) {
|
||
Use(c, Settings{Mode: Light})
|
||
ui.Column(c).Padding(16).Gap(8).Children(func() {
|
||
o := *opts
|
||
o.Today, o.Label = calendarToday, "Due"
|
||
if DatePicker(c, value, o).Changed() {
|
||
*changes++
|
||
}
|
||
})
|
||
}, 420, 460)
|
||
}
|
||
|
||
func TestDatePickerDisablingClosesPopup(t *testing.T) {
|
||
v, n := Date{2026, time.October, 5}, 0
|
||
opts := DatePickerOptions{}
|
||
tt := datePickerLive(&v, &opts, &n)
|
||
_ = tt.Click("Open calendar")
|
||
day := datetimeTestLabel(oct(5))
|
||
if !tt.Focused(day) {
|
||
t.Fatal("setup: the popup should focus the chosen day")
|
||
}
|
||
opts.Disabled = true
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyRight)
|
||
tt.Key(0, ui.KeyEnter)
|
||
_ = tt.Click(datetimeTestLabel(oct(20)))
|
||
if v != oct(5) || n != 0 {
|
||
t.Fatalf("disabled picker committed %v (%d events)", v, n)
|
||
}
|
||
if tt.HasText("October 2026") || tt.Focused(datetimeTestLabel(oct(6))) || tt.Focused("Due") {
|
||
t.Fatal("the popup should close and nothing in the disabled picker keep focus")
|
||
}
|
||
}
|
||
|
||
func TestDatePickerDisabledBlocksInput(t *testing.T) {
|
||
v, n := Date{2026, time.October, 5}, 0
|
||
tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Disabled: true}, &n)
|
||
_ = tt.Click("Due")
|
||
tt.Type("2027-01-01")
|
||
tt.Key(0, ui.KeyEnter)
|
||
tt.Key(0, ui.KeyDown)
|
||
_ = tt.Click("Open calendar")
|
||
if v != oct(5) || n != 0 || tt.Focused("Due") || tt.HasText("October 2026") {
|
||
t.Fatalf("disabled picker took input: %v (%d)", v, n)
|
||
}
|
||
}
|
||
|
||
func TestTimePickerKeepsInvalidHour(t *testing.T) {
|
||
v, n := TimeOfDay{9, 30, 0}, 0
|
||
tt := timePickerTester(&v, TimePickerOptions{}, &n)
|
||
_ = tt.Click("Alarm: Hours")
|
||
tt.Key(0, ui.Key2)
|
||
tt.Key(0, ui.Key9)
|
||
if v != (TimeOfDay{2, 30, 0}) || n != 1 {
|
||
t.Fatalf("first digit should apply: %v", v)
|
||
}
|
||
if !tt.HasText("29") || !tt.HasText("Hours must be from 0 to 23.") || !tt.Focused("Alarm: Hours") {
|
||
t.Fatalf("29 should stay shown with an error: %v", tt.Texts())
|
||
}
|
||
tt.Key(0, ui.Key1)
|
||
tt.Key(0, ui.Key5)
|
||
if v != (TimeOfDay{15, 30, 0}) || n != 3 || tt.HasText("Hours must be") {
|
||
t.Fatalf("fixing the hour: %v", v)
|
||
}
|
||
}
|
||
|
||
func TestTimeSegmentDescription(t *testing.T) {
|
||
var got []string
|
||
ui.NewTester(func(c *ui.Context) {
|
||
Use(c, Settings{})
|
||
got = []string{timeSegmentDescription(c, timeHourPart, " 9", 1, 12), timeSegmentDescription(c, timePeriodPart, "PM", 0, 59)}
|
||
}, 100, 100)
|
||
if got[0] != "9, from 1 to 12" || got[1] != "PM" {
|
||
t.Fatalf("descriptions %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTimePickerSegmentFocusRing(t *testing.T) {
|
||
v := TimeOfDay{9, 30, 0}
|
||
tt := timePickerTester(&v, TimePickerOptions{}, new(int))
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
tt.Key(0, ui.KeyTab)
|
||
r, ok := tt.Find("Alarm: Hours")
|
||
if !ok || !tt.Focused("Alarm: Hours") {
|
||
t.Fatal("setup: hours not focused")
|
||
}
|
||
want := theme.Light().Focus
|
||
img := tt.Image()
|
||
found := false
|
||
for dy := float32(-4); dy <= -2 && !found; dy++ {
|
||
c := img.RGBAAt(int(r.X+r.W/2), int(r.Y+dy))
|
||
found = datetimeNear(c, want)
|
||
}
|
||
if !found {
|
||
t.Fatal("no focus ring above the focused segment")
|
||
}
|
||
}
|
||
|
||
func datetimeNear(c color.RGBA, w ui.Color) bool {
|
||
d := func(a, b uint8) int {
|
||
if a > b {
|
||
return int(a - b)
|
||
}
|
||
return int(b - a)
|
||
}
|
||
return d(c.R, w.R)+d(c.G, w.G)+d(c.B, w.B) < 60
|
||
}
|
||
|
||
func TestDatePickerLayoutChangeReformats(t *testing.T) {
|
||
v, n := Date{2026, time.October, 5}, 0
|
||
opts := DatePickerOptions{}
|
||
tt := datePickerLive(&v, &opts, &n)
|
||
opts.Layout = "02/01/2006"
|
||
tt.Frame()
|
||
_ = tt.Click("Due")
|
||
tt.Key(0, ui.KeyEnter)
|
||
if v != oct(5) || n != 0 || tt.HasText("Enter a date like") {
|
||
t.Fatalf("committed text kept the old layout: %v", tt.Texts())
|
||
}
|
||
_ = tt.Click("Due")
|
||
tt.Command("selectAll")
|
||
tt.Type("12/10")
|
||
opts.Layout = "2006-01-02"
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyEnter)
|
||
if !tt.HasText("Enter a date like 2026-10-05.") || v != oct(5) {
|
||
t.Fatal("a draft being edited should be kept across a layout change")
|
||
}
|
||
}
|
||
|
||
func TestYearPickerHomeEndStayOnPage(t *testing.T) {
|
||
v := 2026
|
||
tt := yearPickerTester(&v, YearPickerOptions{}, new(int))
|
||
tt.Key(0, ui.KeyTab)
|
||
tt.Key(0, ui.KeyDown)
|
||
tt.Key(0, ui.KeyHome)
|
||
if !tt.HasText("2020 – 2029") || !tt.Focused("2020") {
|
||
t.Fatalf("Home left the page: %v", tt.Texts()[:4])
|
||
}
|
||
tt.Key(0, ui.KeyEnd)
|
||
if !tt.HasText("2020 – 2029") || !tt.Focused("2029") {
|
||
t.Fatal("End left the page")
|
||
}
|
||
}
|
||
|
||
func TestDatePickerReadOnly(t *testing.T) {
|
||
v, n := Date{2026, time.October, 5}, 0
|
||
tt := datePickerTester(&v, DatePickerOptions{Label: "Due", ReadOnly: true, Clearable: true}, &n)
|
||
_ = tt.Click("Due")
|
||
if !tt.Focused("Due") {
|
||
t.Fatal("read-only field should take focus")
|
||
}
|
||
tt.Command("selectAll")
|
||
tt.Command("copy")
|
||
if tt.Clipboard() != "2026-10-05" {
|
||
t.Fatalf("copied %q", tt.Clipboard())
|
||
}
|
||
tt.Type("2027-01-01")
|
||
tt.Key(0, ui.KeyEnter)
|
||
tt.Key(0, ui.KeyDown)
|
||
if v != oct(5) || n != 0 || tt.HasText("October 2026") || tt.HasText("Open calendar") || tt.HasText("Clear") {
|
||
t.Fatalf("read-only picker changed: %v %v", v, tt.Texts())
|
||
}
|
||
tt.Key(0, ui.KeyTab)
|
||
tt.Key(ui.Shift, ui.KeyTab)
|
||
tt.Command("selectAll")
|
||
tt.Command("copy")
|
||
if tt.Clipboard() != "2026-10-05" {
|
||
t.Fatalf("typed text leaked into the field: %q", tt.Clipboard())
|
||
}
|
||
}
|
||
|
||
func TestTimePickerReadOnly(t *testing.T) {
|
||
v, n := TimeOfDay{9, 30, 0}, 0
|
||
tt := timePickerTester(&v, TimePickerOptions{ReadOnly: true}, &n)
|
||
tt.Key(0, ui.KeyTab)
|
||
tt.Key(0, ui.KeyUp)
|
||
tt.Key(0, ui.Key1)
|
||
tt.Key(ui.Alt, ui.KeyDown)
|
||
if !tt.Focused("Alarm: Hours") || v != (TimeOfDay{9, 30, 0}) || n != 0 || tt.HasText("Choose time") {
|
||
t.Fatalf("read-only time changed: %v", v)
|
||
}
|
||
tt.Key(ui.Cmd, ui.KeyC)
|
||
if tt.Clipboard() != "09:30" {
|
||
t.Fatalf("copied %q", tt.Clipboard())
|
||
}
|
||
}
|
||
|
||
func TestMonthPickerReadOnly(t *testing.T) {
|
||
v, n := YearMonth{2026, time.May}, 0
|
||
tt := monthPickerTester(&v, MonthPickerOptions{ReadOnly: true, Clearable: true}, &n)
|
||
_ = tt.Click("Billing month")
|
||
tt.Key(0, ui.KeyDown)
|
||
if tt.HasText("January") || tt.HasText("Clear") || !tt.Focused("Billing month") {
|
||
t.Fatalf("read-only month opened: %v", tt.Texts())
|
||
}
|
||
tt.Key(ui.Cmd, ui.KeyC)
|
||
if tt.Clipboard() != "May 2026" || v != (YearMonth{2026, time.May}) || n != 0 {
|
||
t.Fatalf("copied %q, value %v", tt.Clipboard(), v)
|
||
}
|
||
}
|
||
|
||
func TestDateTimePickerLayout(t *testing.T) {
|
||
v := time.Time{}
|
||
n, x := 0, 0
|
||
maxT := time.Date(2026, time.October, 10, 12, 0, 0, 0, time.UTC)
|
||
tt := dateTimeTester(t, &v, DateTimePickerOptions{Location: time.UTC, Max: maxT, Layout: "02/01/2006"}, &n, &x)
|
||
_ = tt.Click("Meeting")
|
||
_ = tt.Click(datetimeTestLabel(oct(10)))
|
||
_ = tt.Click("Time: Hours")
|
||
tt.Key(0, ui.Key1)
|
||
tt.Key(0, ui.Key3)
|
||
_ = tt.Click("Confirm")
|
||
if n != 0 || !tt.HasText("Choose 10/10/2026 12:00 UTC or earlier.") {
|
||
t.Fatalf("error ignores the layout: %v", tt.Texts())
|
||
}
|
||
_ = tt.Click(datetimeTestLabel(oct(9)))
|
||
_ = tt.Click("Confirm")
|
||
if n != 1 || !tt.HasText("09/10/2026 13:00 UTC") {
|
||
t.Fatalf("display ignores the layout: %v", tt.Texts())
|
||
}
|
||
}
|