test: add comprehensive tests for new UX features and fix ISO date timezone bug
Add 33 new test functions covering annotations, undo system, history formatting, relative date display, and weekday parsing pipeline. Fix ISO date parsing to use ParseInLocation instead of Parse to respect the parser's timezone context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormatRelativeDate(t *testing.T) {
|
||||
// Fix timeNow for deterministic tests
|
||||
origTimeNow := timeNow
|
||||
defer func() { timeNow = origTimeNow }()
|
||||
|
||||
// Wednesday, Feb 18, 2026 at 14:30 local time
|
||||
now := time.Date(2026, 2, 18, 14, 30, 0, 0, time.Local)
|
||||
timeNow = func() time.Time { return now }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input time.Time
|
||||
expected string
|
||||
}{
|
||||
// Core relative dates
|
||||
{"today", time.Date(2026, 2, 18, 0, 0, 0, 0, time.Local), "today"},
|
||||
{"today with time", time.Date(2026, 2, 18, 23, 59, 0, 0, time.Local), "today"},
|
||||
{"tomorrow", time.Date(2026, 2, 19, 0, 0, 0, 0, time.Local), "tomorrow"},
|
||||
{"yesterday", time.Date(2026, 2, 17, 0, 0, 0, 0, time.Local), "yesterday"},
|
||||
|
||||
// Near future
|
||||
{"in 2d", time.Date(2026, 2, 20, 0, 0, 0, 0, time.Local), "in 2d"},
|
||||
{"in 7d", time.Date(2026, 2, 25, 0, 0, 0, 0, time.Local), "in 7d"},
|
||||
{"in 14d", time.Date(2026, 3, 4, 0, 0, 0, 0, time.Local), "in 14d"},
|
||||
|
||||
// Near past
|
||||
{"2d ago", time.Date(2026, 2, 16, 0, 0, 0, 0, time.Local), "2d ago"},
|
||||
{"7d ago", time.Date(2026, 2, 11, 0, 0, 0, 0, time.Local), "7d ago"},
|
||||
{"14d ago", time.Date(2026, 2, 4, 0, 0, 0, 0, time.Local), "14d ago"},
|
||||
|
||||
// Beyond 14 days - same year
|
||||
{"15d future", time.Date(2026, 3, 5, 0, 0, 0, 0, time.Local), "Mar 5"},
|
||||
{"15d past", time.Date(2026, 2, 3, 0, 0, 0, 0, time.Local), "Feb 3"},
|
||||
|
||||
// Cross-year
|
||||
{"next year", time.Date(2027, 6, 15, 0, 0, 0, 0, time.Local), "Jun 15 2027"},
|
||||
{"last year", time.Date(2025, 12, 1, 0, 0, 0, 0, time.Local), "Dec 1 2025"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FormatRelativeDate(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("FormatRelativeDate(%v) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRelativeDate_WeekdayPipeline(t *testing.T) {
|
||||
// This test reproduces the reported bug:
|
||||
// On Wednesday, "due:friday" should show "in 2d", not "tomorrow"
|
||||
origTimeNow := timeNow
|
||||
defer func() { timeNow = origTimeNow }()
|
||||
|
||||
// Wednesday, Feb 18, 2026
|
||||
wednesday := time.Date(2026, 2, 18, 10, 0, 0, 0, time.Local)
|
||||
timeNow = func() time.Time { return wednesday }
|
||||
|
||||
parser := NewDateParser(wednesday, time.Monday)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
weekday string
|
||||
expectedRel string
|
||||
expectedDay time.Weekday
|
||||
}{
|
||||
{"friday from wednesday", "friday", "in 2d", time.Friday},
|
||||
{"fri from wednesday", "fri", "in 2d", time.Friday},
|
||||
{"thursday from wednesday", "thu", "tomorrow", time.Thursday},
|
||||
{"saturday from wednesday", "sat", "in 3d", time.Saturday},
|
||||
{"sunday from wednesday", "sun", "in 4d", time.Sunday},
|
||||
{"monday from wednesday", "mon", "in 5d", time.Monday},
|
||||
{"tuesday from wednesday", "tue", "in 6d", time.Tuesday},
|
||||
{"wednesday from wednesday", "wed", "in 7d", time.Wednesday},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parsed, err := parser.ParseDate(tt.weekday)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDate(%q) error: %v", tt.weekday, err)
|
||||
}
|
||||
|
||||
// Verify correct weekday
|
||||
if parsed.Weekday() != tt.expectedDay {
|
||||
t.Errorf("ParseDate(%q) weekday = %v, want %v", tt.weekday, parsed.Weekday(), tt.expectedDay)
|
||||
}
|
||||
|
||||
// Verify relative display
|
||||
rel := FormatRelativeDate(parsed)
|
||||
if rel != tt.expectedRel {
|
||||
t.Errorf("FormatRelativeDate(ParseDate(%q)) = %q, want %q (parsed date: %v)",
|
||||
tt.weekday, rel, tt.expectedRel, parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRelativeDate_AllWeekdaysFromAllDays(t *testing.T) {
|
||||
// Exhaustive: parse every weekday name from every starting day of the week
|
||||
origTimeNow := timeNow
|
||||
defer func() { timeNow = origTimeNow }()
|
||||
|
||||
// Week starting Monday Feb 16 2026
|
||||
weekStart := time.Date(2026, 2, 16, 12, 0, 0, 0, time.Local) // Monday
|
||||
|
||||
weekdays := []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"}
|
||||
targetWeekdays := []time.Weekday{
|
||||
time.Monday, time.Tuesday, time.Wednesday, time.Thursday,
|
||||
time.Friday, time.Saturday, time.Sunday,
|
||||
}
|
||||
|
||||
for fromOffset := 0; fromOffset < 7; fromOffset++ {
|
||||
fromDate := weekStart.AddDate(0, 0, fromOffset)
|
||||
fromName := fromDate.Weekday().String()
|
||||
|
||||
timeNow = func() time.Time { return fromDate }
|
||||
parser := NewDateParser(fromDate, time.Monday)
|
||||
|
||||
for i, dayName := range weekdays {
|
||||
t.Run(fromName+"_to_"+dayName, func(t *testing.T) {
|
||||
parsed, err := parser.ParseDate(dayName)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDate(%q) from %s: %v", dayName, fromName, err)
|
||||
}
|
||||
|
||||
// Must be the correct weekday
|
||||
if parsed.Weekday() != targetWeekdays[i] {
|
||||
t.Errorf("wrong weekday: got %v, want %v", parsed.Weekday(), targetWeekdays[i])
|
||||
}
|
||||
|
||||
// Must be in the future (1-7 days from now)
|
||||
diff := parsed.Sub(time.Date(fromDate.Year(), fromDate.Month(), fromDate.Day(), 0, 0, 0, 0, time.Local))
|
||||
days := int(diff.Hours() / 24)
|
||||
if days < 1 || days > 7 {
|
||||
t.Errorf("ParseDate(%q) from %s: expected 1-7 days ahead, got %d (parsed: %v)",
|
||||
dayName, fromName, days, parsed)
|
||||
}
|
||||
|
||||
// FormatRelativeDate must match the days offset
|
||||
rel := FormatRelativeDate(parsed)
|
||||
if days == 1 && rel != "tomorrow" {
|
||||
t.Errorf("1 day ahead should be 'tomorrow', got %q", rel)
|
||||
}
|
||||
if days > 1 && days <= 7 {
|
||||
expected := "in " + string(rune('0'+days)) + "d"
|
||||
if days >= 10 {
|
||||
// won't happen for weekdays (max 7)
|
||||
}
|
||||
if rel != expected {
|
||||
t.Errorf("from %s, %q: %d days ahead, got rel=%q, want %q",
|
||||
fromName, dayName, days, rel, expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRelativeDate_TimezoneConsistency(t *testing.T) {
|
||||
// Verify that dates in UTC vs Local don't produce wrong relative strings
|
||||
origTimeNow := timeNow
|
||||
defer func() { timeNow = origTimeNow }()
|
||||
|
||||
now := time.Date(2026, 2, 18, 14, 0, 0, 0, time.Local)
|
||||
timeNow = func() time.Time { return now }
|
||||
|
||||
// A date 2 days from now, but in UTC
|
||||
targetUTC := time.Date(2026, 2, 20, 0, 0, 0, 0, time.UTC)
|
||||
// Same date in Local
|
||||
targetLocal := time.Date(2026, 2, 20, 0, 0, 0, 0, time.Local)
|
||||
|
||||
relUTC := FormatRelativeDate(targetUTC)
|
||||
relLocal := FormatRelativeDate(targetLocal)
|
||||
|
||||
// Both should show "in 2d" - if UTC shows something different, that's a bug
|
||||
if relLocal != "in 2d" {
|
||||
t.Errorf("Local target: expected 'in 2d', got %q", relLocal)
|
||||
}
|
||||
|
||||
// Note: UTC target may differ depending on system timezone.
|
||||
// This test documents the behavior.
|
||||
t.Logf("Local timezone: now=%v", now)
|
||||
t.Logf("UTC target relative: %q, Local target relative: %q", relUTC, relLocal)
|
||||
|
||||
if relUTC != relLocal {
|
||||
t.Logf("WARNING: timezone mismatch detected — UTC shows %q vs Local shows %q", relUTC, relLocal)
|
||||
t.Logf("This could explain the 'due:friday shows tomorrow' bug if dates are stored/loaded in wrong timezone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDateWithRelative(t *testing.T) {
|
||||
origTimeNow := timeNow
|
||||
defer func() { timeNow = origTimeNow }()
|
||||
|
||||
now := time.Date(2026, 2, 18, 14, 0, 0, 0, time.Local)
|
||||
timeNow = func() time.Time { return now }
|
||||
|
||||
input := time.Date(2026, 2, 20, 15, 30, 0, 0, time.Local)
|
||||
result := FormatDateWithRelative(input)
|
||||
|
||||
// Should contain both absolute and relative
|
||||
if result != "2026-02-20 15:30 (in 2d)" {
|
||||
t.Errorf("FormatDateWithRelative = %q, want %q", result, "2026-02-20 15:30 (in 2d)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user