139 lines
4.2 KiB
Go
139 lines
4.2 KiB
Go
package datetime
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||
)
|
||
|
||
func cronRuns(t *testing.T, expr string, from time.Time, loc *time.Location, n int) []CronRun {
|
||
t.Helper()
|
||
s, err := ParseCron(expr)
|
||
if err != nil {
|
||
t.Fatalf("%s: %v", expr, err)
|
||
}
|
||
return s.Next(from, loc, n)
|
||
}
|
||
|
||
func TestParseCronErrors(t *testing.T) {
|
||
for _, c := range []struct {
|
||
expr string
|
||
field int
|
||
p CronProblem
|
||
}{{"* * * *", -1, CronFieldCount}, {"60 * * * *", 0, CronOutOfRange}, {"* 9-5 * * *", 1, CronBackwards},
|
||
{"*/0 * * * *", 0, CronBadStep}, {"* * x * *", 2, CronNotNumber}, {"1,,2 * * * *", 0, CronEmptyItem}, {"* * * 13 *", 3, CronOutOfRange}} {
|
||
_, err := ParseCron(c.expr)
|
||
var ce *CronError
|
||
if !errors.As(err, &ce) || ce.Field != c.field || ce.Problem != c.p {
|
||
t.Errorf("%q: %v", c.expr, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCronNextRuns(t *testing.T) {
|
||
from := time.Date(2026, time.October, 5, 8, 50, 0, 0, calTokyo) // Monday
|
||
got := cronRuns(t, "*/15 9-10 * * MON-FRI", from, calTokyo, 3)
|
||
if len(got) != 3 || !got[0].At.Equal(calAt(t, 9, 0, 5)) || !got[2].At.Equal(calAt(t, 9, 30, 5)) {
|
||
t.Fatalf("weekday runs %v", got)
|
||
}
|
||
got = cronRuns(t, "0 12 1 * SUN", from, calTokyo, 2)
|
||
if !got[0].At.Equal(calAt(t, 12, 0, 11)) || !got[1].At.Equal(calAt(t, 12, 0, 18)) {
|
||
t.Fatalf("day-of-month or day-of-week: %v", got)
|
||
}
|
||
got = cronRuns(t, "0 0 29 2 *", from, calTokyo, 1)
|
||
if len(got) != 1 || got[0].At.Year() != 2028 {
|
||
t.Fatalf("leap day %v", got)
|
||
}
|
||
if got = cronRuns(t, "0 0 31 2 *", from, calTokyo, 1); len(got) != 0 {
|
||
t.Fatalf("Feb 31 never runs: %v", got)
|
||
}
|
||
}
|
||
|
||
func TestCronNextRunsAcrossDST(t *testing.T) {
|
||
ny, err := time.LoadLocation("America/New_York")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := cronRuns(t, "30 2 * * *", time.Date(2026, time.March, 7, 3, 0, 0, 0, ny), ny, 2)
|
||
if got[0].At.Day() != 9 {
|
||
t.Fatalf("02:30 on the gap day must not run: %v", got)
|
||
}
|
||
got = cronRuns(t, "30 1 * * *", time.Date(2026, time.October, 31, 3, 0, 0, 0, ny), ny, 1)
|
||
if !got[0].Repeated || got[0].At.Day() != 1 {
|
||
t.Fatalf("01:30 on fall-back is repeated: %v", got)
|
||
}
|
||
}
|
||
|
||
func cronTester(expr *string, opts CronEditorOptions, n *int) *ui.Tester {
|
||
opts.Location, opts.Now = calTokyo, time.Date(2026, time.October, 5, 8, 50, 0, 0, calTokyo)
|
||
return ui.NewTester(func(c *ui.Context) {
|
||
core.Use(c, core.Settings{})
|
||
ui.Column(c).Padding(16).Children(func() {
|
||
if CronEditor(c, expr, opts).Changed() {
|
||
*n++
|
||
}
|
||
})
|
||
}, 760, 520)
|
||
}
|
||
|
||
func TestCronEditorEditAndPreview(t *testing.T) {
|
||
expr, n := "0 9 * * MON-FRI", 0
|
||
tt := cronTester(&expr, CronEditorOptions{Runs: 3}, &n)
|
||
for _, s := range []string{"Mo 2026-10-05 09:00 JST", "Tu 2026-10-06 09:00 JST", "MON–FRI", "Every value"} {
|
||
if !tt.HasText(s) {
|
||
t.Fatalf("missing %q in %v", s, tt.Texts())
|
||
}
|
||
}
|
||
_ = tt.Click("Expression")
|
||
tt.Command("selectAll")
|
||
tt.Type("0 25 * * *")
|
||
if n == 0 || !tt.HasText("Hour: 25 is outside 0–23.") || tt.HasText("Next runs") {
|
||
t.Fatalf("error not shown: %v", tt.Texts())
|
||
}
|
||
tt.Command("selectAll")
|
||
tt.Type("*/30 * * * *")
|
||
if !tt.HasText("Every 30") || !tt.HasText("Mo 2026-10-05 09:00 JST") {
|
||
t.Fatalf("fixed expression: %v", tt.Texts())
|
||
}
|
||
}
|
||
|
||
func TestCronEditorDisabledAndLocale(t *testing.T) {
|
||
expr, n := "0 9 * * *", 0
|
||
tt := cronTester(&expr, CronEditorOptions{Disabled: true}, &n)
|
||
_ = tt.Click("Expression")
|
||
tt.Type("x")
|
||
if expr != "0 9 * * *" || n != 0 {
|
||
t.Fatal("disabled editor changed")
|
||
}
|
||
bad := "* *"
|
||
tt = ui.NewTester(func(c *ui.Context) {
|
||
core.Use(c, core.Settings{Locale: core.ZhCN})
|
||
CronEditor(c, &bad, CronEditorOptions{Location: calTokyo})
|
||
}, 760, 400)
|
||
if !tt.HasText("需要 5 个字段,而不是 2 个。") {
|
||
t.Fatalf("zh error: %v", tt.Texts())
|
||
}
|
||
defer func() {
|
||
if recover() == nil {
|
||
t.Fatal("expected panic without Location")
|
||
}
|
||
}()
|
||
ui.NewTester(func(c *ui.Context) {
|
||
core.Use(c, core.Settings{})
|
||
CronEditor(c, &expr, CronEditorOptions{})
|
||
}, 10, 10)
|
||
}
|
||
|
||
func TestCronEditorSnapshot(t *testing.T) {
|
||
expr := "*/15 9-17 * * MON-FRI"
|
||
snapshot(t, "cron_editor", 720, 400, func(c *ui.Context) {
|
||
ui.Column(c).Padding(16).Children(func() {
|
||
CronEditor(c, &expr, CronEditorOptions{Location: calTokyo, Now: time.Date(2026, time.October, 5, 8, 50, 0, 0, calTokyo)})
|
||
})
|
||
})
|
||
}
|