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,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user