cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 745 lines 20 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} 487 488// TestUncoveredPriorityModes tests all priority mode switch cases 489func TestUncoveredPriorityModes(t *testing.T) { 490 t.Run("Priority Mode Display Strings", func(t *testing.T) { 491 task := &models.Task{ID: 1, Priority: models.PriorityHigh} 492 model := createTestTaskEditModel(task) 493 model.mode = priorityPicker 494 495 // Test numeric mode 496 model.priorityMode = priorityModeNumeric 497 result := model.renderPriorityPicker() 498 if !strings.Contains(result, "Numeric") { 499 t.Error("Priority picker should show Numeric mode") 500 } 501 502 // Test legacy mode 503 model.priorityMode = priorityModeLegacy 504 result = model.renderPriorityPicker() 505 if !strings.Contains(result, "Legacy") { 506 t.Error("Priority picker should show Legacy mode") 507 } 508 }) 509 510 t.Run("Priority Display Type Switches", func(t *testing.T) { 511 tests := []struct { 512 name string 513 priority string 514 expectedType string 515 }{ 516 {"Numeric Priority", "3", "numeric"}, 517 {"Legacy Priority A", "A", "legacy"}, 518 {"Legacy Priority E", "E", "legacy"}, 519 } 520 521 for _, tt := range tests { 522 t.Run(tt.name, func(t *testing.T) { 523 task := &models.Task{ID: 1, Priority: tt.priority} 524 525 displayType := GetPriorityDisplayType(task.Priority) 526 if displayType != tt.expectedType { 527 t.Errorf("Expected display type %s for priority %s, got %s (task:%v)", tt.expectedType, tt.priority, displayType, task) 528 } 529 }) 530 } 531 }) 532} 533 534func TestUncoveredKeyboardNavigation(t *testing.T) { 535 t.Run("Status Edit Key Binding", func(t *testing.T) { 536 task := &models.Task{ID: 1, Status: models.StatusTodo} 537 model := createTestTaskEditModel(task) 538 539 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}} 540 updatedModel, _ := model.Update(msg) 541 model = updatedModel.(taskEditModel) 542 543 if model.mode != statusPicker { 544 t.Error("Expected status picker mode when 's' key is pressed") 545 } 546 }) 547 548 t.Run("Priority Edit Key Binding", func(t *testing.T) { 549 task := &models.Task{ID: 1} 550 model := createTestTaskEditModel(task) 551 552 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'p'}} 553 updatedModel, _ := model.Update(msg) 554 model = updatedModel.(taskEditModel) 555 556 if model.mode != priorityPicker { 557 t.Error("Expected priority picker mode when 'p' key is pressed") 558 } 559 }) 560 561 t.Run("Window Resize Handling", func(t *testing.T) { 562 task := &models.Task{ID: 1} 563 model := createTestTaskEditModel(task) 564 565 msg := tea.WindowSizeMsg{Width: 120, Height: 40} 566 updatedModel, _ := model.Update(msg) 567 model = updatedModel.(taskEditModel) 568 569 if model.opts.Width != 120 { 570 t.Errorf("Expected width to be updated to 120, got %d", model.opts.Width) 571 } 572 }) 573 574 t.Run("Escape Key in Status Picker", func(t *testing.T) { 575 task := &models.Task{ID: 1} 576 model := createTestTaskEditModel(task) 577 model.mode = statusPicker 578 579 msg := tea.KeyMsg{Type: tea.KeyEsc} 580 updatedModel, _ := model.Update(msg) 581 model = updatedModel.(taskEditModel) 582 583 if model.mode != fieldNavigation { 584 t.Error("Expected to return to field navigation when escape is pressed in status picker") 585 } 586 }) 587 588 t.Run("Escape Key in Priority Picker", func(t *testing.T) { 589 task := &models.Task{ID: 1} 590 model := createTestTaskEditModel(task) 591 model.mode = priorityPicker 592 593 msg := tea.KeyMsg{Type: tea.KeyEsc} 594 updatedModel, _ := model.Update(msg) 595 model = updatedModel.(taskEditModel) 596 597 if model.mode != fieldNavigation { 598 t.Error("Expected to return to field navigation when escape is pressed in priority picker") 599 } 600 }) 601 602 t.Run("Navigation Keys in Status Picker", func(t *testing.T) { 603 task := &models.Task{ID: 1} 604 model := createTestTaskEditModel(task) 605 model.mode = statusPicker 606 model.statusIndex = 0 607 608 // Test down/right navigation 609 msg := tea.KeyMsg{Type: tea.KeyDown} 610 updatedModel, _ := model.Update(msg) 611 model = updatedModel.(taskEditModel) 612 613 if model.statusIndex != 1 { 614 t.Errorf("Expected status index to be 1, got %d", model.statusIndex) 615 } 616 617 // Test up/left navigation 618 msg = tea.KeyMsg{Type: tea.KeyUp} 619 updatedModel, _ = model.Update(msg) 620 model = updatedModel.(taskEditModel) 621 622 if model.statusIndex != 0 { 623 t.Errorf("Expected status index to be 0, got %d", model.statusIndex) 624 } 625 }) 626 627 t.Run("Navigation Keys in Priority Picker", func(t *testing.T) { 628 task := &models.Task{ID: 1} 629 model := createTestTaskEditModel(task) 630 model.mode = priorityPicker 631 model.priorityIndex = 0 632 633 // Test down/right navigation 634 msg := tea.KeyMsg{Type: tea.KeyDown} 635 updatedModel, _ := model.Update(msg) 636 model = updatedModel.(taskEditModel) 637 638 if model.priorityIndex != 1 { 639 t.Errorf("Expected priority index to be 1, got %d", model.priorityIndex) 640 } 641 642 // Test up/left navigation 643 msg = tea.KeyMsg{Type: tea.KeyUp} 644 updatedModel, _ = model.Update(msg) 645 model = updatedModel.(taskEditModel) 646 647 if model.priorityIndex != 0 { 648 t.Errorf("Expected priority index to be 0, got %d", model.priorityIndex) 649 } 650 }) 651} 652 653func TestUncoveredFieldSwitches(t *testing.T) { 654 t.Run("Field Entry Switch Cases", func(t *testing.T) { 655 task := &models.Task{ID: 1, Description: "Test", Project: "TestProject"} 656 model := createTestTaskEditModel(task) 657 658 model.currentField = 3 659 model.mode = textInput 660 661 msg := tea.KeyMsg{Type: tea.KeyEnter} 662 updatedModel, _ := model.Update(msg) 663 model = updatedModel.(taskEditModel) 664 665 if model.mode != fieldNavigation { 666 t.Error("Expected to return to field navigation after entering project field") 667 } 668 }) 669 670 t.Run("Field Update Switch Cases", func(t *testing.T) { 671 task := &models.Task{ID: 1, Description: "Test", Project: "TestProject"} 672 model := createTestTaskEditModel(task) 673 model.currentField = 3 674 model.mode = textInput 675 676 model.projectInput.SetValue("Updated Project") 677 678 msg := tea.KeyMsg{Type: tea.KeyEnter} 679 updatedModel, _ := model.Update(msg) 680 model = updatedModel.(taskEditModel) 681 682 if model.task.Project != "Updated Project" { 683 t.Errorf("Expected project to be updated to 'Updated Project', got %s", model.task.Project) 684 } 685 }) 686} 687 688func TestTaskFieldAccessors(t *testing.T) { 689 t.Run("Task Field Value Extraction", func(t *testing.T) { 690 now := time.Now() 691 task := &models.Task{ 692 ID: 1, 693 Description: "Test", 694 Tags: []string{"tag1", "tag2"}, 695 Due: &now, 696 Entry: now, 697 Start: &now, 698 End: &now, 699 } 700 701 tests := []struct { 702 field string 703 expected any 704 }{ 705 {"due", task.Due}, 706 {"entry", task.Entry}, 707 {"start", task.Start}, 708 {"end", task.End}, 709 } 710 711 for _, tt := range tests { 712 t.Run(tt.field, func(t *testing.T) { 713 result := getTaskFieldValue(task, tt.field) 714 if result != tt.expected { 715 t.Errorf("Expected %v for field %s, got %v", tt.expected, tt.field, result) 716 } 717 }) 718 } 719 }) 720} 721 722func getTaskFieldValue(task *models.Task, field string) any { 723 switch field { 724 case "description": 725 return task.Description 726 case "status": 727 return task.Status 728 case "priority": 729 return task.Priority 730 case "project": 731 return task.Project 732 case "tags": 733 return task.Tags 734 case "due": 735 return task.Due 736 case "entry": 737 return task.Entry 738 case "start": 739 return task.Start 740 case "end": 741 return task.End 742 default: 743 return nil 744 } 745}