cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 447 lines 12 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 "github.com/stormlightlabs/noteleaf/internal/repo" 13) 14 15type mockTaskRepository struct { 16 tasks []*models.Task 17 err error 18} 19 20func (m *mockTaskRepository) List(ctx context.Context, options repo.TaskListOptions) ([]*models.Task, error) { 21 if m.err != nil { 22 return nil, m.err 23 } 24 25 var filtered []*models.Task 26 for _, task := range m.tasks { 27 if options.Status == "pending" && task.Status == "completed" { 28 continue 29 } else if options.Status != "" && options.Status != "pending" && task.Status != options.Status { 30 continue 31 } 32 33 if options.Priority != "" && task.Priority != options.Priority { 34 continue 35 } 36 37 if options.Project != "" && task.Project != options.Project { 38 continue 39 } 40 41 filtered = append(filtered, task) 42 43 if options.Limit > 0 && len(filtered) >= options.Limit { 44 break 45 } 46 } 47 48 return filtered, nil 49} 50 51func (m *mockTaskRepository) Update(ctx context.Context, task *models.Task) error { 52 if m.err != nil { 53 return m.err 54 } 55 for i, t := range m.tasks { 56 if t.ID == task.ID { 57 m.tasks[i] = task 58 break 59 } 60 } 61 return nil 62} 63 64func TestTaskAdapter(t *testing.T) { 65 now := time.Now() 66 task := &models.Task{ 67 ID: 1, 68 UUID: "test-uuid-123", 69 Description: "Test task", 70 Status: "todo", 71 Priority: "high", 72 Project: "work", 73 Tags: []string{"urgent", "review"}, 74 Entry: now, 75 Modified: now.Add(time.Hour), 76 Annotations: []string{"First note", "Second note"}, 77 } 78 79 t.Run("TaskRecord", func(t *testing.T) { 80 record := &TaskRecord{Task: task} 81 82 t.Run("GetField", func(t *testing.T) { 83 tests := []struct { 84 field string 85 expected any 86 name string 87 }{ 88 {"id", int64(1), "should return task ID"}, 89 {"uuid", "test-uuid-123", "should return task UUID"}, 90 {"description", "Test task", "should return task description"}, 91 {"status", "todo", "should return task status"}, 92 {"priority", "high", "should return task priority"}, 93 {"project", "work", "should return task project"}, 94 {"tags", []string{"urgent", "review"}, "should return task tags"}, 95 {"entry", now, "should return entry time"}, 96 {"modified", now.Add(time.Hour), "should return modified time"}, 97 {"annotations", []string{"First note", "Second note"}, "should return annotations"}, 98 {"unknown", "", "should return empty string for unknown field"}, 99 } 100 101 for _, tt := range tests { 102 t.Run(tt.name, func(t *testing.T) { 103 result := record.GetField(tt.field) 104 105 switch expected := tt.expected.(type) { 106 case []string: 107 resultSlice, ok := result.([]string) 108 if !ok || len(resultSlice) != len(expected) { 109 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 110 return 111 } 112 for i, item := range expected { 113 if resultSlice[i] != item { 114 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 115 return 116 } 117 } 118 case time.Time: 119 if resultTime, ok := result.(time.Time); !ok || !resultTime.Equal(expected) { 120 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 121 } 122 default: 123 if result != tt.expected { 124 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 125 } 126 } 127 }) 128 } 129 }) 130 131 t.Run("Model interface", func(t *testing.T) { 132 if record.GetID() != 1 { 133 t.Errorf("GetID() = %d, want 1", record.GetID()) 134 } 135 136 if record.GetTableName() != "tasks" { 137 t.Errorf("GetTableName() = %q, want 'tasks'", record.GetTableName()) 138 } 139 }) 140 }) 141 142 t.Run("TaskDataSource", func(t *testing.T) { 143 tasks := []*models.Task{ 144 { 145 ID: 1, 146 Description: "Todo task", 147 Status: "todo", 148 Priority: "high", 149 Project: "work", 150 Entry: now, 151 Modified: now, 152 }, 153 { 154 ID: 2, 155 Description: "In progress task", 156 Status: "in-progress", 157 Priority: "medium", 158 Project: "personal", 159 Entry: now, 160 Modified: now, 161 }, 162 { 163 ID: 3, 164 Description: "Completed task", 165 Status: "completed", 166 Priority: "low", 167 Project: "work", 168 Entry: now, 169 Modified: now, 170 }, 171 } 172 173 t.Run("Load", func(t *testing.T) { 174 repo := &mockTaskRepository{tasks: tasks} 175 source := &TaskDataSource{repo: repo, showAll: true} 176 177 records, err := source.Load(context.Background(), DataOptions{}) 178 if err != nil { 179 t.Fatalf("Load() failed: %v", err) 180 } 181 182 if len(records) != 3 { 183 t.Errorf("Load() returned %d records, want 3", len(records)) 184 } 185 186 if records[0].GetField("description") != "Todo task" { 187 t.Errorf("First record description = %v, want 'Todo task'", records[0].GetField("description")) 188 } 189 }) 190 191 t.Run("Load with pending filter", func(t *testing.T) { 192 repo := &mockTaskRepository{tasks: tasks} 193 source := &TaskDataSource{repo: repo, showAll: false} 194 195 records, err := source.Load(context.Background(), DataOptions{}) 196 if err != nil { 197 t.Fatalf("Load() failed: %v", err) 198 } 199 200 if len(records) != 2 { 201 t.Errorf("Load() with pending filter returned %d records, want 2", len(records)) 202 } 203 }) 204 205 t.Run("Load with status filter", func(t *testing.T) { 206 repo := &mockTaskRepository{tasks: tasks} 207 source := &TaskDataSource{repo: repo, status: "completed"} 208 209 records, err := source.Load(context.Background(), DataOptions{}) 210 if err != nil { 211 t.Fatalf("Load() failed: %v", err) 212 } 213 214 if len(records) != 1 { 215 t.Errorf("Load() with status filter returned %d records, want 1", len(records)) 216 } 217 if records[0].GetField("status") != "completed" { 218 t.Errorf("Filtered record status = %v, want 'completed'", records[0].GetField("status")) 219 } 220 }) 221 222 t.Run("Load with priority filter", func(t *testing.T) { 223 repo := &mockTaskRepository{tasks: tasks} 224 source := &TaskDataSource{repo: repo, priority: "high", showAll: true} 225 226 records, err := source.Load(context.Background(), DataOptions{}) 227 if err != nil { 228 t.Fatalf("Load() failed: %v", err) 229 } 230 231 if len(records) != 1 { 232 t.Errorf("Load() with priority filter returned %d records, want 1", len(records)) 233 } 234 if records[0].GetField("priority") != "high" { 235 t.Errorf("Filtered record priority = %v, want 'high'", records[0].GetField("priority")) 236 } 237 }) 238 239 t.Run("Load with project filter", func(t *testing.T) { 240 repo := &mockTaskRepository{tasks: tasks} 241 source := &TaskDataSource{repo: repo, project: "work", showAll: true} 242 243 records, err := source.Load(context.Background(), DataOptions{}) 244 if err != nil { 245 t.Fatalf("Load() failed: %v", err) 246 } 247 248 if len(records) != 2 { 249 t.Errorf("Load() with project filter returned %d records, want 2", len(records)) 250 } 251 if records[0].GetField("project") != "work" { 252 t.Errorf("Filtered record project = %v, want 'work'", records[0].GetField("project")) 253 } 254 }) 255 256 t.Run("Load error", func(t *testing.T) { 257 testErr := fmt.Errorf("test error") 258 repo := &mockTaskRepository{err: testErr} 259 source := &TaskDataSource{repo: repo} 260 261 _, err := source.Load(context.Background(), DataOptions{}) 262 if err != testErr { 263 t.Errorf("Load() error = %v, want %v", err, testErr) 264 } 265 }) 266 267 t.Run("Count", func(t *testing.T) { 268 repo := &mockTaskRepository{tasks: tasks} 269 source := &TaskDataSource{repo: repo, showAll: true} 270 271 count, err := source.Count(context.Background(), DataOptions{}) 272 if err != nil { 273 t.Fatalf("Count() failed: %v", err) 274 } 275 276 if count != 3 { 277 t.Errorf("Count() = %d, want 3", count) 278 } 279 }) 280 281 t.Run("Count error", func(t *testing.T) { 282 testErr := fmt.Errorf("test error") 283 repo := &mockTaskRepository{err: testErr} 284 source := &TaskDataSource{repo: repo} 285 286 _, err := source.Count(context.Background(), DataOptions{}) 287 if err != testErr { 288 t.Errorf("Count() error = %v, want %v", err, testErr) 289 } 290 }) 291 }) 292 293 t.Run("NewTaskDataTable", func(t *testing.T) { 294 repo := &mockTaskRepository{ 295 tasks: []*models.Task{ 296 { 297 ID: 1, 298 Description: "Test task", 299 Status: "todo", 300 Priority: "high", 301 Project: "work", 302 Entry: time.Now(), 303 Modified: time.Now(), 304 }, 305 }, 306 } 307 308 opts := DataTableOptions{ 309 Output: &bytes.Buffer{}, 310 Input: strings.NewReader("q\n"), 311 Static: true, 312 } 313 314 table := NewTaskDataTable(repo, opts, false, "", "", "") 315 if table == nil { 316 t.Fatal("NewTaskDataTable() returned nil") 317 } 318 319 err := table.Browse(context.Background()) 320 if err != nil { 321 t.Errorf("Browse() failed: %v", err) 322 } 323 }) 324 325 t.Run("NewTaskListFromTable", func(t *testing.T) { 326 repo := &mockTaskRepository{ 327 tasks: []*models.Task{ 328 { 329 ID: 1, 330 Description: "Test task", 331 Status: "todo", 332 Priority: "high", 333 Project: "work", 334 Entry: time.Now(), 335 Modified: time.Now(), 336 }, 337 }, 338 } 339 340 output := &bytes.Buffer{} 341 input := strings.NewReader("q\n") 342 343 table := NewTaskListFromTable(repo, output, input, true, false, "", "", "") 344 if table == nil { 345 t.Fatal("NewTaskListFromTable() returned nil") 346 } 347 348 err := table.Browse(context.Background()) 349 if err != nil { 350 t.Errorf("Browse() failed: %v", err) 351 } 352 353 outputStr := output.String() 354 if !strings.Contains(outputStr, "Tasks") { 355 t.Error("Output should contain 'Tasks' title") 356 } 357 if !strings.Contains(outputStr, "Test task") { 358 t.Error("Output should contain task description") 359 } 360 if !strings.Contains(outputStr, "pending only") { 361 t.Error("Output should contain 'pending only' filter status") 362 } 363 }) 364 365 t.Run("Format Task For View", func(t *testing.T) { 366 now := time.Now() 367 due := now.Add(24 * time.Hour) 368 start := now.Add(-time.Hour) 369 end := now.Add(time.Hour) 370 371 task := &models.Task{ 372 ID: 1, 373 UUID: "test-uuid-123", 374 Description: "Test task description", 375 Status: "in-progress", 376 Priority: "high", 377 Project: "work", 378 Tags: []string{"urgent", "review"}, 379 Due: &due, 380 Entry: now, 381 Modified: now.Add(30 * time.Minute), 382 Start: &start, 383 End: &end, 384 Annotations: []string{"First note", "Second note"}, 385 } 386 387 result := formatTaskForView(task) 388 389 if !strings.Contains(result, "Task 1") { 390 t.Error("Formatted view should contain task ID in title") 391 } 392 if !strings.Contains(result, "test-uuid-123") { 393 t.Error("Formatted view should contain UUID") 394 } 395 if !strings.Contains(result, "Test task description") { 396 t.Error("Formatted view should contain description") 397 } 398 if !strings.Contains(result, "in-progress") { 399 t.Error("Formatted view should contain status") 400 } 401 if !strings.Contains(result, "high") { 402 t.Error("Formatted view should contain priority") 403 } 404 if !strings.Contains(result, "work") { 405 t.Error("Formatted view should contain project") 406 } 407 if !strings.Contains(result, "urgent, review") { 408 t.Error("Formatted view should contain tags") 409 } 410 if !strings.Contains(result, "Due:") { 411 t.Error("Formatted view should contain due date") 412 } 413 if !strings.Contains(result, "Started:") { 414 t.Error("Formatted view should contain start time") 415 } 416 if !strings.Contains(result, "Completed:") { 417 t.Error("Formatted view should contain end time") 418 } 419 if !strings.Contains(result, "First note") { 420 t.Error("Formatted view should contain annotations") 421 } 422 }) 423 424 t.Run("Format Priority Field", func(t *testing.T) { 425 tests := []struct { 426 priority string 427 name string 428 contains string // What the result should contain 429 }{ 430 {"", "empty priority", "-"}, 431 {"high", "high priority", "High"}, 432 {"urgent", "urgent priority", "Urgent"}, 433 {"medium", "medium priority", "Medium"}, 434 {"low", "low priority", "Low"}, 435 {"unknown", "unknown priority", "Unknown"}, 436 } 437 438 for _, tt := range tests { 439 t.Run(tt.name, func(t *testing.T) { 440 result := formatPriorityField(tt.priority) 441 if !strings.Contains(result, tt.contains) { 442 t.Errorf("formatPriorityField(%q) = %q, should contain %q", tt.priority, result, tt.contains) 443 } 444 }) 445 } 446 }) 447}