Files
joakim 04fa9222d8 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>
2026-02-19 16:42:49 +01:00

382 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package engine
import (
"testing"
"time"
)
func TestParseMonthName(t *testing.T) {
// Base: Jan 5, 2026
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"jan (passed)", "jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
{"january (passed)", "january", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
{"feb (future)", "feb", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)},
{"february (future)", "february", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)},
{"mar", "mar", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)},
{"march", "march", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)},
{"dec", "dec", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)},
{"december", "december", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := parser.parseMonthName(tt.input)
if !ok {
t.Fatalf("Failed to parse month name: %s", tt.input)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParseDayMonth(t *testing.T) {
// Base: Jan 5, 2026
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"21jan (future this year)", "21jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
{"21Jan", "21Jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
{"Jan21", "Jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
{"jan21", "jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
{"1jan (passed)", "1jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
{"30dec", "30dec", time.Date(2026, 12, 30, 0, 0, 0, 0, time.UTC)},
{"15feb", "15feb", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)},
{"15February", "15February", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := parser.parseDayMonth(tt.input)
if !ok {
t.Fatalf("Failed to parse day+month: %s", tt.input)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParsePeriodBoundary(t *testing.T) {
// Base: Monday, Jan 5, 2026, 14:30:00
base := time.Date(2026, 1, 5, 14, 30, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"sod", "sod", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)},
{"eod", "eod", time.Date(2026, 1, 5, 23, 59, 59, 0, time.UTC)},
{"sow", "sow", time.Date(2026, 1, 12, 0, 0, 0, 0, time.UTC)}, // Next Monday
{"eow", "eow", time.Date(2026, 1, 11, 23, 59, 59, 0, time.UTC)}, // Next Sunday
{"som", "som", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)}, // Next month (Jan 1 passed)
{"eom", "eom", time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC)}, // End of Jan
{"soy", "soy", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, // Next year (Jan 1 passed)
{"eoy", "eoy", time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)}, // End of 2026
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := parser.parsePeriodBoundary(tt.input)
if !ok {
t.Fatalf("Failed to parse period boundary: %s", tt.input)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParseSpecialKeyword(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"later", "later", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)},
{"someday", "someday", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := parser.parseSpecialKeyword(tt.input)
if !ok {
t.Fatalf("Failed to parse special keyword: %s", tt.input)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParseDurationAsDate(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"2d", "2d", base.AddDate(0, 0, 2)},
{"3w", "3w", base.AddDate(0, 0, 21)},
{"1m", "1m", base.AddDate(0, 0, 30)},
{"1y", "1y", base.AddDate(0, 0, 365)},
{"daily", "daily", base.AddDate(0, 0, 1)},
{"weekly", "weekly", base.AddDate(0, 0, 7)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := parser.parseDurationAsDate(tt.input)
if !ok {
t.Fatalf("Failed to parse duration as date: %s", tt.input)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParseTimeOfDay(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
date time.Time
timeStr string
expected time.Time
wantErr bool
}{
{"HH:MM format", base, "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC), false},
{"HHMM format", base, "0800", time.Date(2026, 1, 5, 8, 0, 0, 0, time.UTC), false},
{"midnight", base, "00:00", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC), false},
{"end of day", base, "23:59", time.Date(2026, 1, 5, 23, 59, 0, 0, time.UTC), false},
{"invalid hour", base, "25:00", time.Time{}, true},
{"invalid minute", base, "12:65", time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parser.parseTimeOfDay(tt.date, tt.timeStr)
if tt.wantErr {
if err == nil {
t.Error("Expected error but got none")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestParseDateWithTime(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) // Monday
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
expected time.Time
}{
{"mon:15:35", "mon:15:35", time.Date(2026, 1, 12, 15, 35, 0, 0, time.UTC)}, // Next Monday at 15:35
{"tomorrow:0800", "tomorrow:0800", time.Date(2026, 1, 6, 8, 0, 0, 0, time.UTC)},
{"21jan:1430", "21jan:1430", time.Date(2026, 1, 21, 14, 30, 0, 0, time.UTC)},
{"just time 15:35", "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC)}, // Today at 15:35
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parser.ParseDate(tt.input)
if err != nil {
t.Fatalf("Failed to parse date with time: %v", err)
}
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestSplitDateTime(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
tests := []struct {
name string
input string
wantDate string
wantTime string
wantHas bool
}{
{"plain weekday", "mon", "mon", "", false},
{"plain date", "2026-01-15", "2026-01-15", "", false},
{"weekday+HHMM", "mon:0800", "mon", "0800", true},
{"weekday+HH:MM", "mon:15:35", "mon", "15:35", true},
{"date+HHMM", "21jan:1430", "21jan", "1430", true},
{"just time HH:MM", "15:35", "", "15:35", true},
{"tomorrow+time", "tomorrow:0800", "tomorrow", "0800", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dateStr, timeStr, hasTime := parser.splitDateTime(tt.input)
if hasTime != tt.wantHas {
t.Errorf("hasTime = %v, want %v", hasTime, tt.wantHas)
}
if hasTime {
if dateStr != tt.wantDate {
t.Errorf("dateStr = %q, want %q", dateStr, tt.wantDate)
}
if timeStr != tt.wantTime {
t.Errorf("timeStr = %q, want %q", timeStr, tt.wantTime)
}
}
})
}
}
func TestParseISODate(t *testing.T) {
// Use a non-UTC timezone to verify ISO dates respect the parser's location
loc := time.FixedZone("UTC+10", 10*60*60)
base := time.Date(2026, 1, 5, 12, 0, 0, 0, loc)
parser := NewDateParser(base, time.Monday)
result, err := parser.ParseDate("2026-02-20")
if err != nil {
t.Fatalf("Failed to parse ISO date: %v", err)
}
expected := time.Date(2026, 2, 20, 0, 0, 0, 0, loc)
if !result.Equal(expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
if result.Location() != loc {
t.Errorf("Expected location %v, got %v", loc, result.Location())
}
}
func TestParseDateInvalid(t *testing.T) {
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
parser := NewDateParser(base, time.Monday)
invalids := []string{
"notadate",
"xyz123",
"",
"32jan",
"feb30",
}
for _, input := range invalids {
t.Run(input, func(t *testing.T) {
_, err := parser.ParseDate(input)
if err == nil && input != "" {
// Some of these might parse as durations or keywords
// but truly invalid ones should error
t.Logf("ParseDate(%q) did not error (may be valid as keyword/duration)", input)
}
})
}
}
func TestNextWeekday_Exhaustive(t *testing.T) {
// Test all 7 starting days × 7 target days
// Mon Jan 5 2026
monday := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
allDays := []time.Weekday{
time.Sunday, time.Monday, time.Tuesday, time.Wednesday,
time.Thursday, time.Friday, time.Saturday,
}
for fromOffset := 0; fromOffset < 7; fromOffset++ {
from := monday.AddDate(0, 0, fromOffset)
parser := NewDateParser(from, time.Monday)
for _, target := range allDays {
t.Run(from.Weekday().String()+"_to_"+target.String(), func(t *testing.T) {
result := parser.nextWeekday(target)
// Must land on the correct weekday
if result.Weekday() != target {
t.Errorf("weekday = %v, want %v", result.Weekday(), target)
}
// Must be 1-7 days in the future
fromMidnight := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, from.Location())
days := int(result.Sub(fromMidnight).Hours() / 24)
if days < 1 || days > 7 {
t.Errorf("days ahead = %d, want 1-7 (from %v to %v)", days, from, result)
}
// Same weekday should always be 7 days ahead
if from.Weekday() == target && days != 7 {
t.Errorf("same weekday should be 7 days, got %d", days)
}
})
}
}
}
func TestExpandedDurationFormats(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
}{
{"5sec", "5sec", 5 * time.Second},
{"5seconds", "5seconds", 5 * time.Second},
{"10min", "10min", 10 * time.Minute},
{"10minutes", "10minutes", 10 * time.Minute},
{"2hrs", "2hrs", 2 * time.Hour},
{"2hours", "2hours", 2 * time.Hour},
{"3days", "3days", 3 * 24 * time.Hour},
{"2weeks", "2weeks", 2 * 7 * 24 * time.Hour},
{"1month", "1month", 30 * 24 * time.Hour},
{"1year", "1year", 365 * 24 * time.Hour},
{"hour (no number)", "hour", time.Hour},
{"day (no number)", "day", 24 * time.Hour},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseRecurrencePattern(tt.input)
if err != nil {
t.Fatalf("Failed to parse duration: %v", err)
}
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}