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 0dbb5b3bba74dcb62db8b636cddd0f41ac28d44a 486 lines 13 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/textinput" 12 tea "github.com/charmbracelet/bubbletea" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15) 16 17type mockTaskRepo struct { 18 tasks map[int64]*models.Task 19 updated []*models.Task 20} 21 22func (m *mockTaskRepo) List(ctx context.Context, opts repo.TaskListOptions) ([]*models.Task, error) { 23 var result []*models.Task 24 for _, task := range m.tasks { 25 result = append(result, task) 26 } 27 return result, nil 28} 29 30func (m *mockTaskRepo) Update(ctx context.Context, task *models.Task) error { 31 m.updated = append(m.updated, task) 32 if existing, exists := m.tasks[task.ID]; exists { 33 *existing = *task 34 } 35 return nil 36} 37 38func createTestTaskEditModel(task *models.Task) taskEditModel { 39 now := time.Now() 40 if task.Entry.IsZero() { 41 task.Entry = now 42 } 43 if task.Modified.IsZero() { 44 task.Modified = now 45 } 46 47 repo := &mockTaskRepo{tasks: map[int64]*models.Task{task.ID: task}} 48 49 model := taskEditModel{ 50 task: task, 51 originalTask: task, 52 repo: repo, 53 opts: TaskEditOptions{Output: &bytes.Buffer{}, Width: 80, Height: 24}, 54 keys: taskEditKeys, 55 help: help.New(), 56 57 mode: fieldNavigation, 58 currentField: 0, 59 priorityMode: priorityModeText, 60 61 fields: []string{"Description", "Status", "Priority", "Project"}, 62 } 63 64 model.descInput = textinput.New() 65 model.descInput.SetValue(task.Description) 66 model.projectInput = textinput.New() 67 model.projectInput.SetValue(task.Project) 68 69 for i, status := range statusOptions { 70 if status == task.Status { 71 model.statusIndex = i 72 break 73 } 74 } 75 76 model.updatePriorityIndex() 77 78 return model 79} 80 81func TestTaskEditor(t *testing.T) { 82 t.Run("Creation", func(t *testing.T) { 83 task := &models.Task{ 84 ID: 1, 85 Description: "Test task", 86 Status: models.StatusTodo, 87 Priority: models.PriorityHigh, 88 Project: "test-project", 89 } 90 91 repo := &mockTaskRepo{tasks: map[int64]*models.Task{1: task}} 92 editor := NewTaskEditor(task, repo, TaskEditOptions{Width: 80, Height: 24}) 93 94 if editor.task != task { 95 t.Error("Task should be set correctly") 96 } 97 98 if editor.repo != repo { 99 t.Error("Repository should be set correctly") 100 } 101 102 if editor.opts.Width != 80 { 103 t.Errorf("Expected width 80, got %d", editor.opts.Width) 104 } 105 }) 106 107 t.Run("Default Options", func(t *testing.T) { 108 task := &models.Task{ID: 1} 109 repo := &mockTaskRepo{} 110 editor := NewTaskEditor(task, repo, TaskEditOptions{}) 111 112 if editor.opts.Width != 80 { 113 t.Errorf("Expected default width 80, got %d", editor.opts.Width) 114 } 115 116 if editor.opts.Height != 24 { 117 t.Errorf("Expected default height 24, got %d", editor.opts.Height) 118 } 119 }) 120} 121 122func TestTaskEditModel(t *testing.T) { 123 t.Run("Init", func(t *testing.T) { 124 task := &models.Task{ 125 ID: 1, 126 Description: "Test task", 127 Status: models.StatusInProgress, 128 Priority: models.PriorityMedium, 129 } 130 131 model := createTestTaskEditModel(task) 132 cmd := model.Init() 133 if cmd == nil { 134 t.Error("Init should return a command") 135 } 136 }) 137 138 t.Run("Field Navigation", func(t *testing.T) { 139 task := &models.Task{ID: 1, Description: "Test task", Status: models.StatusTodo} 140 model := createTestTaskEditModel(task) 141 142 if model.currentField != 0 { 143 t.Errorf("Expected initial field 0, got %d", model.currentField) 144 } 145 146 msg := tea.KeyMsg{Type: tea.KeyDown} 147 updatedModel, _ := model.Update(msg) 148 model = updatedModel.(taskEditModel) 149 150 if model.currentField != 1 { 151 t.Errorf("Expected field 1 after down, got %d", model.currentField) 152 } 153 154 msg = tea.KeyMsg{Type: tea.KeyUp} 155 updatedModel, _ = model.Update(msg) 156 model = updatedModel.(taskEditModel) 157 158 if model.currentField != 0 { 159 t.Errorf("Expected field 0 after up, got %d", model.currentField) 160 } 161 }) 162 163 t.Run("Status Picker", func(t *testing.T) { 164 task := &models.Task{ID: 1, Description: "Test task", Status: models.StatusTodo} 165 model := createTestTaskEditModel(task) 166 model.currentField = 1 167 168 msg := tea.KeyMsg{Type: tea.KeyEnter} 169 updatedModel, _ := model.Update(msg) 170 model = updatedModel.(taskEditModel) 171 172 if model.mode != statusPicker { 173 t.Errorf("Expected statusPicker mode, got %d", model.mode) 174 } 175 176 msg = tea.KeyMsg{Type: tea.KeyDown} 177 updatedModel, _ = model.Update(msg) 178 model = updatedModel.(taskEditModel) 179 180 if model.statusIndex != 1 { 181 t.Errorf("Expected status index 1, got %d", model.statusIndex) 182 } 183 184 msg = tea.KeyMsg{Type: tea.KeyEnter} 185 updatedModel, _ = model.Update(msg) 186 model = updatedModel.(taskEditModel) 187 188 if model.task.Status != statusOptions[1] { 189 t.Errorf("Expected status %s, got %s", statusOptions[1], model.task.Status) 190 } 191 192 if model.mode != fieldNavigation { 193 t.Errorf("Expected fieldNavigation mode after selection, got %d", model.mode) 194 } 195 }) 196 197 t.Run("Priority Picker", func(t *testing.T) { 198 task := &models.Task{ID: 1, Description: "Test task", Priority: ""} 199 model := createTestTaskEditModel(task) 200 model.currentField = 2 201 202 msg := tea.KeyMsg{Type: tea.KeyEnter} 203 updatedModel, _ := model.Update(msg) 204 model = updatedModel.(taskEditModel) 205 206 if model.mode != priorityPicker { 207 t.Errorf("Expected priorityPicker mode, got %d", model.mode) 208 } 209 210 msg = tea.KeyMsg{Type: tea.KeyDown} 211 updatedModel, _ = model.Update(msg) 212 model = updatedModel.(taskEditModel) 213 214 if model.priorityIndex != 1 { 215 t.Errorf("Expected priority index 1, got %d", model.priorityIndex) 216 } 217 218 msg = tea.KeyMsg{Type: tea.KeyEnter} 219 updatedModel, _ = model.Update(msg) 220 model = updatedModel.(taskEditModel) 221 222 expectedPriority := textPriorityOptions[1] 223 if model.task.Priority != expectedPriority { 224 t.Errorf("Expected priority %s, got %s", expectedPriority, model.task.Priority) 225 } 226 }) 227 228 t.Run("Priority Mode Switch", func(t *testing.T) { 229 task := &models.Task{ID: 1, Priority: models.PriorityHigh} 230 model := createTestTaskEditModel(task) 231 model.mode = priorityPicker 232 233 if model.priorityMode != priorityModeText { 234 t.Errorf("Expected text priority mode initially, got %d", model.priorityMode) 235 } 236 237 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 238 updatedModel, _ := model.Update(msg) 239 model = updatedModel.(taskEditModel) 240 241 if model.priorityMode != priorityModeNumeric { 242 t.Errorf("Expected numeric priority mode, got %d", model.priorityMode) 243 } 244 245 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 246 updatedModel, _ = model.Update(msg) 247 model = updatedModel.(taskEditModel) 248 249 if model.priorityMode != priorityModeLegacy { 250 t.Errorf("Expected legacy priority mode, got %d", model.priorityMode) 251 } 252 253 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 254 updatedModel, _ = model.Update(msg) 255 model = updatedModel.(taskEditModel) 256 257 if model.priorityMode != priorityModeText { 258 t.Errorf("Expected text priority mode after full cycle, got %d", model.priorityMode) 259 } 260 }) 261 262 t.Run("TextInput", func(t *testing.T) { 263 task := &models.Task{ID: 1, Description: "Original description", Project: "original-project"} 264 265 model := createTestTaskEditModel(task) 266 model.currentField = 0 267 268 msg := tea.KeyMsg{Type: tea.KeyEnter} 269 updatedModel, _ := model.Update(msg) 270 model = updatedModel.(taskEditModel) 271 272 if model.mode != textInput { 273 t.Errorf("Expected textInput mode, got %d", model.mode) 274 } 275 276 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'X'}} 277 updatedModel, _ = model.Update(msg) 278 model = updatedModel.(taskEditModel) 279 280 msg = tea.KeyMsg{Type: tea.KeyEnter} 281 updatedModel, _ = model.Update(msg) 282 model = updatedModel.(taskEditModel) 283 284 if model.mode != fieldNavigation { 285 t.Errorf("Expected fieldNavigation mode after text input, got %d", model.mode) 286 } 287 288 expected := "Original descriptionX" 289 if model.task.Description != expected { 290 t.Errorf("Expected description %s, got %s", expected, model.task.Description) 291 } 292 }) 293 294 t.Run("Help", func(t *testing.T) { 295 task := &models.Task{ID: 1} 296 model := createTestTaskEditModel(task) 297 298 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} 299 updatedModel, _ := model.Update(msg) 300 model = updatedModel.(taskEditModel) 301 302 if !model.showingHelp { 303 t.Error("Expected help to be shown") 304 } 305 306 msg = tea.KeyMsg{Type: tea.KeyEsc} 307 updatedModel, _ = model.Update(msg) 308 model = updatedModel.(taskEditModel) 309 310 if model.showingHelp { 311 t.Error("Expected help to be hidden") 312 } 313 }) 314 315 t.Run("Save", func(t *testing.T) { 316 task := &models.Task{ID: 1} 317 model := createTestTaskEditModel(task) 318 msg := tea.KeyMsg{Type: tea.KeyCtrlS} 319 updatedModel, cmd := model.Update(msg) 320 model = updatedModel.(taskEditModel) 321 322 if !model.saved { 323 t.Error("Expected saved flag to be set") 324 } 325 326 if cmd == nil { 327 t.Error("Expected quit command after save") 328 } 329 }) 330 331 t.Run("Cancel", func(t *testing.T) { 332 task := &models.Task{ID: 1} 333 model := createTestTaskEditModel(task) 334 msg := tea.KeyMsg{Type: tea.KeyCtrlC} 335 updatedModel, cmd := model.Update(msg) 336 model = updatedModel.(taskEditModel) 337 338 if !model.cancelled { 339 t.Error("Expected cancelled flag to be set") 340 } 341 342 if cmd == nil { 343 t.Error("Expected quit command after cancel") 344 } 345 }) 346 347 t.Run("View", func(t *testing.T) { 348 task := &models.Task{ 349 ID: 1, 350 Description: "Test task", 351 Status: models.StatusTodo, 352 Priority: models.PriorityHigh, 353 Project: "test-project", 354 } 355 356 model := createTestTaskEditModel(task) 357 view := model.View() 358 359 if !strings.Contains(view, "Edit Task") { 360 t.Error("View should contain title") 361 } 362 363 if !strings.Contains(view, "Test task") { 364 t.Error("View should contain task description") 365 } 366 367 if !strings.Contains(view, "test-project") { 368 t.Error("View should contain project") 369 } 370 }) 371 372 t.Run("Status Picker View", func(t *testing.T) { 373 task := &models.Task{ID: 1, Status: models.StatusTodo} 374 model := createTestTaskEditModel(task) 375 model.mode = statusPicker 376 377 view := model.View() 378 379 if !strings.Contains(view, "Select Status:") { 380 t.Error("Status picker should show selection prompt") 381 } 382 383 for _, status := range statusOptions { 384 if !strings.Contains(view, status) { 385 t.Errorf("Status picker should contain %s", status) 386 } 387 } 388 }) 389 390 t.Run("Priority Picker View", func(t *testing.T) { 391 task := &models.Task{ID: 1, Priority: ""} 392 model := createTestTaskEditModel(task) 393 model.mode = priorityPicker 394 model.priorityMode = priorityModeText 395 396 view := model.View() 397 398 if !strings.Contains(view, "Select Priority") { 399 t.Error("Priority picker should show selection prompt") 400 } 401 402 if !strings.Contains(view, "Text") { 403 t.Error("Priority picker should show current mode") 404 } 405 }) 406 407 t.Run("KeyBindings", func(t *testing.T) { 408 keyMap := taskEditKeys 409 410 if keyMap.Up.Keys()[0] != "up" { 411 t.Error("Up key binding should be defined") 412 } 413 414 if keyMap.StatusEdit.Keys()[0] != "s" { 415 t.Error("Status edit key binding should be 's'") 416 } 417 418 if keyMap.Priority.Keys()[0] != "p" { 419 t.Error("Priority key binding should be 'p'") 420 } 421 422 if keyMap.PriorityMode.Keys()[0] != "m" { 423 t.Error("Priority mode key binding should be 'm'") 424 } 425 }) 426} 427 428func TestUpdatePriorityIndex(t *testing.T) { 429 testCases := []struct { 430 priority string 431 mode priorityMode 432 expectedIdx int 433 }{ 434 {models.PriorityHigh, priorityModeText, 3}, 435 {models.PriorityMedium, priorityModeText, 2}, 436 {models.PriorityLow, priorityModeText, 1}, 437 {"", priorityModeText, 0}, 438 {"3", priorityModeNumeric, 3}, 439 {"A", priorityModeLegacy, 1}, 440 {"unknown", priorityModeText, 0}, 441 } 442 443 for _, tc := range testCases { 444 task := &models.Task{ID: 1, Priority: tc.priority} 445 model := createTestTaskEditModel(task) 446 model.priorityMode = tc.mode 447 model.updatePriorityIndex() 448 449 if model.priorityIndex != tc.expectedIdx { 450 t.Errorf("Priority %s in mode %d should have index %d, got %d", 451 tc.priority, tc.mode, tc.expectedIdx, model.priorityIndex) 452 } 453 } 454} 455 456func TestRenderStatusField(t *testing.T) { 457 task := &models.Task{ID: 1, Status: models.StatusInProgress} 458 model := createTestTaskEditModel(task) 459 460 result := model.renderStatusField() 461 if !strings.Contains(result, models.StatusInProgress) { 462 t.Error("Status field should contain the status") 463 } 464 465 model.mode = statusPicker 466 result = model.renderStatusField() 467 if !strings.Contains(result, models.StatusTodo) || !strings.Contains(result, models.StatusDone) { 468 t.Error("Status picker should show status legend") 469 } 470} 471 472func TestRenderPriorityField(t *testing.T) { 473 task := &models.Task{ID: 1, Priority: models.PriorityMedium} 474 model := createTestTaskEditModel(task) 475 result := model.renderPriorityField() 476 if !strings.Contains(result, models.PriorityMedium) { 477 t.Error("Priority field should contain the priority") 478 } 479 480 model.mode = priorityPicker 481 model.priorityMode = priorityModeNumeric 482 result = model.renderPriorityField() 483 if !strings.Contains(result, "Numeric") { 484 t.Error("Priority picker should show current mode") 485 } 486}