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:
2026-02-19 16:42:49 +01:00
parent 07d1a78dfc
commit 04fa9222d8
6 changed files with 1117 additions and 1 deletions
+328
View File
@@ -0,0 +1,328 @@
package engine
import (
"strings"
"testing"
"time"
)
func TestParseChangeData(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
"basic key-value pairs",
"description: Buy groceries\nstatus: pending\npriority: H",
map[string]string{"description": "Buy groceries", "status": "pending", "priority": "H"},
},
{
"empty string",
"",
map[string]string{},
},
{
"whitespace only",
" \n \n ",
map[string]string{},
},
{
"value with colon",
"description: Fix bug: crash on startup\nstatus: pending",
map[string]string{"description": "Fix bug: crash on startup", "status": "pending"},
},
{
"trailing newlines",
"description: Test\nstatus: pending\n\n",
map[string]string{"description": "Test", "status": "pending"},
},
{
"no separator",
"this line has no colon-space separator",
map[string]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseChangeData(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("len = %d, want %d; got %v", len(result), len(tt.expected), result)
return
}
for k, v := range tt.expected {
if result[k] != v {
t.Errorf("key %q = %q, want %q", k, result[k], v)
}
}
})
}
}
func TestDiffFields(t *testing.T) {
tests := []struct {
name string
prev map[string]string
curr map[string]string
expectChanges int
expectSubstr []string // substrings that should appear in changes
}{
{
"no changes",
map[string]string{"description": "Test", "status": "pending"},
map[string]string{"description": "Test", "status": "pending"},
0, nil,
},
{
"status change",
map[string]string{"status": "pending"},
map[string]string{"status": "completed"},
1, []string{"status: pending → completed"},
},
{
"field added",
map[string]string{"description": "Test"},
map[string]string{"description": "Test", "priority": "H"},
1, []string{"priority: (none) → H"},
},
{
"field removed",
map[string]string{"description": "Test", "priority": "H"},
map[string]string{"description": "Test"},
1, []string{"priority: H → (none)"},
},
{
"skips uuid and timestamps",
map[string]string{"uuid": "abc", "created": "123", "modified": "456", "status": "pending"},
map[string]string{"uuid": "def", "created": "789", "modified": "012", "status": "completed"},
1, []string{"status"},
},
{
"multiple changes",
map[string]string{"description": "Old", "status": "pending", "priority": "L"},
map[string]string{"description": "New", "status": "active", "priority": "H"},
3, nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := diffFields(tt.prev, tt.curr)
if len(changes) != tt.expectChanges {
t.Errorf("got %d changes, want %d: %v", len(changes), tt.expectChanges, changes)
}
for _, substr := range tt.expectSubstr {
found := false
for _, c := range changes {
if strings.Contains(c, substr) {
found = true
break
}
}
if !found {
t.Errorf("expected change containing %q, got %v", substr, changes)
}
}
})
}
}
func TestFormatFieldValue(t *testing.T) {
tests := []struct {
name string
key string
value string
expected string
}{
{"status passthrough", "status", "pending", "pending"},
{"description passthrough", "description", "Buy milk", "Buy milk"},
{"due as unix timestamp", "due", "1771977600", "2026-02-25"},
{"invalid timestamp", "due", "not-a-number", "not-a-number"},
{"scheduled as timestamp", "scheduled", "1771977600", "2026-02-25"},
{"start as timestamp", "start", "1771977600", "2026-02-25"},
{"end as timestamp", "end", "1771977600", "2026-02-25"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatFieldValue(tt.key, tt.value)
if result != tt.expected {
t.Errorf("formatFieldValue(%q, %q) = %q, want %q", tt.key, tt.value, result, tt.expected)
}
})
}
}
func TestFormatTaskHistory_Empty(t *testing.T) {
result := FormatTaskHistory(nil)
if result != "No history found.\n" {
t.Errorf("expected 'No history found.\\n', got %q", result)
}
result = FormatTaskHistory([]HistoryEntry{})
if result != "No history found.\n" {
t.Errorf("expected 'No history found.\\n', got %q", result)
}
}
func TestFormatTaskHistory_CreateEntry(t *testing.T) {
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC),
ChangeType: "create",
Data: "description: Buy groceries\nstatus: pending\npriority: H\ntags: errand,shopping",
},
}
result := FormatTaskHistory(entries)
if !strings.Contains(result, "created") {
t.Error("expected 'created' in output")
}
if !strings.Contains(result, "Buy groceries") {
t.Error("expected description in output")
}
if !strings.Contains(result, "priority:H") {
t.Error("expected priority in output")
}
if !strings.Contains(result, "+errand") {
t.Error("expected +errand tag in output")
}
if !strings.Contains(result, "+shopping") {
t.Error("expected +shopping tag in output")
}
}
func TestFormatTaskHistory_CreateWithDefaultPriority(t *testing.T) {
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC),
ChangeType: "create",
Data: "description: Simple task\nstatus: pending\npriority: D",
},
}
result := FormatTaskHistory(entries)
// Default priority "D" should not be shown
if strings.Contains(result, "priority") {
t.Errorf("default priority should not appear in output: %s", result)
}
}
func TestFormatTaskHistory_UpdateDiff(t *testing.T) {
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC),
ChangeType: "create",
Data: "description: Buy groceries\nstatus: pending\npriority: D",
},
{
ID: 2,
Timestamp: time.Date(2026, 2, 18, 11, 0, 0, 0, time.UTC),
ChangeType: "update",
Data: "description: Buy groceries\nstatus: pending\npriority: H",
},
}
result := FormatTaskHistory(entries)
if !strings.Contains(result, "modified") {
t.Error("expected 'modified' in output for update with diff")
}
// Should show priority change
if !strings.Contains(result, "priority") {
t.Errorf("expected priority change in diff output: %s", result)
}
}
func TestFormatTaskHistory_DeleteEntry(t *testing.T) {
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC),
ChangeType: "create",
Data: "description: Task\nstatus: pending",
},
{
ID: 2,
Timestamp: time.Date(2026, 2, 18, 12, 0, 0, 0, time.UTC),
ChangeType: "delete",
Data: "",
},
}
result := FormatTaskHistory(entries)
if !strings.Contains(result, "deleted") {
t.Error("expected 'deleted' in output")
}
}
func TestFormatTaskHistory_UpdateWithNoPrev(t *testing.T) {
// Update entry without a preceding create (edge case)
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC),
ChangeType: "update",
Data: "description: Task\nstatus: completed",
},
}
result := FormatTaskHistory(entries)
if !strings.Contains(result, "updated") {
t.Errorf("expected 'updated' for update with no prev: %s", result)
}
}
func TestFormatTaskHistory_TimestampFormat(t *testing.T) {
entries := []HistoryEntry{
{
ID: 1,
Timestamp: time.Date(2026, 2, 18, 14, 30, 0, 0, time.UTC),
ChangeType: "create",
Data: "description: Test\nstatus: pending",
},
}
result := FormatTaskHistory(entries)
if !strings.Contains(result, "2026-02-18 14:30") {
t.Errorf("expected timestamp '2026-02-18 14:30' in output: %s", result)
}
}
func TestParseUnixString(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
year int
}{
{"valid timestamp", "1771977600", false, 2026},
{"zero", "0", false, 1970},
{"invalid", "abc", true, 0},
{"empty", "", true, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseUnixString(tt.input)
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.Year() != tt.year {
t.Errorf("year = %d, want %d", result.Year(), tt.year)
}
})
}
}