cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}