cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 17ba836c74ebd128fed81eb3a2b59aadc0ec887e 486 lines 12 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/charmbracelet/bubbles/help" 11 "github.com/charmbracelet/bubbles/viewport" 12 tea "github.com/charmbracelet/bubbletea" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14) 15 16func createMockTask() *models.Task { 17 now := time.Now() 18 due := now.Add(24 * time.Hour) 19 start := now.Add(-2 * time.Hour) 20 21 return &models.Task{ 22 ID: 1, 23 UUID: "test-uuid-123", 24 Description: "Test task description", 25 Status: "pending", 26 Priority: "high", 27 Project: "test-project", 28 Tags: []string{"urgent", "test"}, 29 Entry: now.Add(-24 * time.Hour), 30 Modified: now.Add(-1 * time.Hour), 31 Due: &due, 32 Start: &start, 33 Annotations: []string{"First annotation", "Second annotation"}, 34 } 35} 36 37func createCompletedMockTask() *models.Task { 38 now := time.Now() 39 task := createMockTask() 40 task.Status = "completed" 41 task.End = &now 42 return task 43} 44 45func TestTaskView(t *testing.T) { 46 t.Run("View Options", func(t *testing.T) { 47 task := createMockTask() 48 49 t.Run("default options", func(t *testing.T) { 50 opts := TaskViewOptions{} 51 tv := NewTaskView(task, opts) 52 53 if tv.opts.Output == nil { 54 t.Error("Output should default to os.Stdout") 55 } 56 if tv.opts.Input == nil { 57 t.Error("Input should default to os.Stdin") 58 } 59 if tv.opts.Width != 80 { 60 t.Errorf("Width should default to 80, got %d", tv.opts.Width) 61 } 62 if tv.opts.Height != 24 { 63 t.Errorf("Height should default to 24, got %d", tv.opts.Height) 64 } 65 }) 66 67 t.Run("custom options", func(t *testing.T) { 68 var buf bytes.Buffer 69 opts := TaskViewOptions{ 70 Output: &buf, 71 Static: true, 72 Width: 100, 73 Height: 30, 74 } 75 tv := NewTaskView(task, opts) 76 77 if tv.opts.Output != &buf { 78 t.Error("Custom output not set") 79 } 80 if !tv.opts.Static { 81 t.Error("Static mode not set") 82 } 83 if tv.opts.Width != 100 { 84 t.Error("Custom width not set") 85 } 86 if tv.opts.Height != 30 { 87 t.Error("Custom height not set") 88 } 89 }) 90 }) 91 92 t.Run("New", func(t *testing.T) { 93 task := createMockTask() 94 95 t.Run("creates task view correctly", func(t *testing.T) { 96 opts := TaskViewOptions{Width: 60, Height: 20} 97 tv := NewTaskView(task, opts) 98 99 if tv.task != task { 100 t.Error("Task not set correctly") 101 } 102 if tv.opts.Width != 60 { 103 t.Error("Width not set correctly") 104 } 105 if tv.opts.Height != 20 { 106 t.Error("Height not set correctly") 107 } 108 }) 109 }) 110 111 t.Run("Static Mode", func(t *testing.T) { 112 t.Run("basic task display", func(t *testing.T) { 113 task := createMockTask() 114 var buf bytes.Buffer 115 116 tv := NewTaskView(task, TaskViewOptions{ 117 Output: &buf, 118 Static: true, 119 }) 120 121 err := tv.Show(context.Background()) 122 if err != nil { 123 t.Fatalf("Show failed: %v", err) 124 } 125 126 output := buf.String() 127 128 if !strings.Contains(output, "Task 1") { 129 t.Error("Task title not displayed") 130 } 131 132 if !strings.Contains(output, "test-uuid-123") { 133 t.Error("UUID not displayed") 134 } 135 if !strings.Contains(output, "Test task description") { 136 t.Error("Description not displayed") 137 } 138 if !strings.Contains(output, "Pending") { 139 t.Error("Status not displayed with title case") 140 } 141 if !strings.Contains(output, "High") { 142 t.Error("Priority not displayed with title case") 143 } 144 if !strings.Contains(output, "test-project") { 145 t.Error("Project not displayed") 146 } 147 if !strings.Contains(output, "urgent, test") { 148 t.Error("Tags not displayed correctly") 149 } 150 151 if !strings.Contains(output, "Dates:") { 152 t.Error("Dates section not displayed") 153 } 154 if !strings.Contains(output, "Created:") { 155 t.Error("Created date not displayed") 156 } 157 if !strings.Contains(output, "Modified:") { 158 t.Error("Modified date not displayed") 159 } 160 if !strings.Contains(output, "Due:") { 161 t.Error("Due date not displayed") 162 } 163 if !strings.Contains(output, "Started:") { 164 t.Error("Start date not displayed") 165 } 166 167 if !strings.Contains(output, "Annotations:") { 168 t.Error("Annotations section not displayed") 169 } 170 if !strings.Contains(output, "First annotation") { 171 t.Error("First annotation not displayed") 172 } 173 if !strings.Contains(output, "Second annotation") { 174 t.Error("Second annotation not displayed") 175 } 176 }) 177 178 t.Run("completed task display", func(t *testing.T) { 179 task := createCompletedMockTask() 180 var buf bytes.Buffer 181 182 tv := NewTaskView(task, TaskViewOptions{ 183 Output: &buf, 184 Static: true, 185 }) 186 187 err := tv.Show(context.Background()) 188 if err != nil { 189 t.Fatalf("Show failed: %v", err) 190 } 191 192 output := buf.String() 193 194 if !strings.Contains(output, "Completed") { 195 t.Error("Completed status not displayed with title case") 196 } 197 if !strings.Contains(output, "Completed:") { 198 t.Error("Completion date not displayed") 199 } 200 }) 201 202 t.Run("minimal task display", func(t *testing.T) { 203 now := time.Now() 204 task := &models.Task{ 205 ID: 2, 206 UUID: "minimal-uuid", 207 Description: "Minimal task", 208 Status: "pending", 209 Entry: now, 210 Modified: now, 211 } 212 213 var buf bytes.Buffer 214 tv := NewTaskView(task, TaskViewOptions{ 215 Output: &buf, 216 Static: true, 217 }) 218 219 err := tv.Show(context.Background()) 220 if err != nil { 221 t.Fatalf("Show failed: %v", err) 222 } 223 224 output := buf.String() 225 226 if !strings.Contains(output, "Task 2") { 227 t.Error("Task title not displayed") 228 } 229 if !strings.Contains(output, "minimal-uuid") { 230 t.Error("UUID not displayed") 231 } 232 if !strings.Contains(output, "Minimal task") { 233 t.Error("Description not displayed") 234 } 235 236 // Should not contain optional fields 237 if strings.Contains(output, "Priority:") { 238 t.Error("Priority should not be displayed for minimal task") 239 } 240 if strings.Contains(output, "Project:") { 241 t.Error("Project should not be displayed for minimal task") 242 } 243 if strings.Contains(output, "Tags:") { 244 t.Error("Tags should not be displayed for minimal task") 245 } 246 if strings.Contains(output, "Annotations:") { 247 t.Error("Annotations should not be displayed for minimal task") 248 } 249 }) 250 }) 251 252 t.Run("Format Content", func(t *testing.T) { 253 t.Run("formats task content correctly", func(t *testing.T) { 254 task := createMockTask() 255 content := formatTaskContent(task) 256 257 expectedStrings := []string{ 258 "UUID: test-uuid-123", 259 "Description: Test task description", 260 "Status: Pending", 261 "Priority: High", 262 "Project: test-project", 263 "Tags: urgent, test", 264 "Dates:", 265 "Created:", 266 "Modified:", 267 "Due:", 268 "Started:", 269 "Annotations:", 270 "1. First annotation", 271 "2. Second annotation", 272 } 273 274 for _, expected := range expectedStrings { 275 if !strings.Contains(content, expected) { 276 t.Errorf("Expected content '%s' not found in formatted output", expected) 277 } 278 } 279 }) 280 281 t.Run("handles empty optional fields", func(t *testing.T) { 282 now := time.Now() 283 task := &models.Task{ 284 ID: 1, 285 UUID: "test-uuid", 286 Description: "Test description", 287 Status: "pending", 288 Entry: now, 289 Modified: now, 290 } 291 292 content := formatTaskContent(task) 293 294 if strings.Contains(content, "Priority:") { 295 t.Error("Priority should not appear when empty") 296 } 297 if strings.Contains(content, "Project:") { 298 t.Error("Project should not appear when empty") 299 } 300 if strings.Contains(content, "Tags:") { 301 t.Error("Tags should not appear when empty") 302 } 303 if strings.Contains(content, "Annotations:") { 304 t.Error("Annotations should not appear when empty") 305 } 306 if strings.Contains(content, "Due:") { 307 t.Error("Due date should not appear when nil") 308 } 309 if strings.Contains(content, "Started:") { 310 t.Error("Start date should not appear when nil") 311 } 312 if strings.Contains(content, "Completed:") { 313 t.Error("End date should not appear when nil") 314 } 315 }) 316 }) 317 318 t.Run("Model", func(t *testing.T) { 319 task := createMockTask() 320 321 t.Run("initial model state", func(t *testing.T) { 322 vp := viewport.New(80, 20) 323 vp.SetContent(formatTaskContent(task)) 324 325 model := taskViewModel{task: task, opts: TaskViewOptions{Width: 80, Height: 24}} 326 327 if model.showingHelp { 328 t.Error("Initial showingHelp should be false") 329 } 330 if model.task != task { 331 t.Error("Task not set correctly") 332 } 333 }) 334 335 t.Run("key handling - help toggle", func(t *testing.T) { 336 vp := viewport.New(80, 20) 337 model := taskViewModel{ 338 task: task, 339 viewport: vp, 340 keys: taskViewKeys, 341 help: help.New(), 342 } 343 344 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}) 345 if m, ok := newModel.(taskViewModel); ok { 346 if !m.showingHelp { 347 t.Error("Help key should show help") 348 } 349 } 350 351 model.showingHelp = true 352 newModel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}) 353 if m, ok := newModel.(taskViewModel); ok { 354 if m.showingHelp { 355 t.Error("Help key should exit help when already showing") 356 } 357 } 358 }) 359 360 t.Run("key handling - quit and back", func(t *testing.T) { 361 vp := viewport.New(80, 20) 362 model := taskViewModel{ 363 task: task, 364 viewport: vp, 365 keys: taskViewKeys, 366 help: help.New(), 367 } 368 369 quitKeys := []string{"q", "esc"} 370 for _, key := range quitKeys { 371 _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}) 372 if cmd == nil { 373 t.Errorf("Key %s should return quit command", key) 374 } 375 } 376 }) 377 378 t.Run("viewport navigation", func(t *testing.T) { 379 vp := viewport.New(80, 20) 380 longContent := strings.Repeat("Line of content\n", 50) // Create content longer than viewport 381 vp.SetContent(longContent) 382 383 model := taskViewModel{ 384 task: task, 385 viewport: vp, 386 keys: taskViewKeys, 387 help: help.New(), 388 } 389 390 initialOffset := model.viewport.YOffset 391 392 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 393 if m, ok := newModel.(taskViewModel); ok { 394 if m.viewport.YOffset <= initialOffset { 395 t.Error("Down key should scroll viewport down") 396 } 397 } 398 399 model.viewport.ScrollDown(5) 400 initialOffset = model.viewport.YOffset 401 newModel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 402 if m, ok := newModel.(taskViewModel); ok { 403 if m.viewport.YOffset >= initialOffset { 404 t.Error("Up key should scroll viewport up") 405 } 406 } 407 }) 408 }) 409 410 t.Run("View Model", func(t *testing.T) { 411 task := createMockTask() 412 413 t.Run("normal view", func(t *testing.T) { 414 vp := viewport.New(80, 20) 415 vp.SetContent(formatTaskContent(task)) 416 417 model := taskViewModel{ 418 task: task, 419 viewport: vp, 420 keys: taskViewKeys, 421 help: help.New(), 422 } 423 424 view := model.View() 425 426 if !strings.Contains(view, "Task 1") { 427 t.Error("Task title not displayed in view") 428 } 429 if !strings.Contains(view, "help") { 430 t.Error("Help information not displayed") 431 } 432 }) 433 434 t.Run("help view", func(t *testing.T) { 435 vp := viewport.New(80, 20) 436 model := taskViewModel{ 437 task: task, 438 viewport: vp, 439 keys: taskViewKeys, 440 help: help.New(), 441 showingHelp: true, 442 } 443 444 view := model.View() 445 446 if !strings.Contains(view, "scroll") { 447 t.Error("Help view should contain scroll instructions") 448 } 449 }) 450 }) 451 452 t.Run("Integration", func(t *testing.T) { 453 t.Run("creates and displays task view", func(t *testing.T) { 454 task := createMockTask() 455 var buf bytes.Buffer 456 457 tv := NewTaskView(task, TaskViewOptions{ 458 Output: &buf, 459 Static: true, 460 Width: 80, 461 Height: 24, 462 }) 463 464 if tv == nil { 465 t.Fatal("NewTaskView returned nil") 466 } 467 468 err := tv.Show(context.Background()) 469 if err != nil { 470 t.Fatalf("Show failed: %v", err) 471 } 472 473 output := buf.String() 474 if len(output) == 0 { 475 t.Error("No output generated") 476 } 477 478 if !strings.Contains(output, task.Description) { 479 t.Error("Task description not displayed") 480 } 481 if !strings.Contains(output, task.UUID) { 482 t.Error("Task UUID not displayed") 483 } 484 }) 485 }) 486}