cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 911 lines 23 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/charmbracelet/bubbles/help" 13 "github.com/charmbracelet/bubbles/key" 14 tea "github.com/charmbracelet/bubbletea" 15) 16 17type MockDataRecord struct { 18 fields map[string]any 19} 20 21func (m MockDataRecord) GetID() int64 { return 1 } 22func (m MockDataRecord) SetID(id int64) {} 23func (m MockDataRecord) GetTableName() string { return "mock_records" } 24func (m MockDataRecord) GetCreatedAt() time.Time { return time.Time{} } 25func (m MockDataRecord) SetCreatedAt(t time.Time) {} 26func (m MockDataRecord) GetUpdatedAt() time.Time { return time.Time{} } 27func (m MockDataRecord) SetUpdatedAt(t time.Time) {} 28func (m MockDataRecord) GetField(name string) any { return m.fields[name] } 29 30func NewMockRecord(id int64, fields map[string]any) MockDataRecord { 31 return MockDataRecord{fields: fields} 32} 33 34type MockDataSource struct { 35 records []DataRecord 36 loadError error 37 countError error 38} 39 40func (m *MockDataSource) Load(ctx context.Context, opts DataOptions) ([]DataRecord, error) { 41 if m.loadError != nil { 42 return nil, m.loadError 43 } 44 45 filtered := make([]DataRecord, 0) 46 for _, record := range m.records { 47 include := true 48 for filterField, filterValue := range opts.Filters { 49 if record.GetField(filterField) != filterValue { 50 include = false 51 break 52 } 53 } 54 if include { 55 filtered = append(filtered, record) 56 } 57 } 58 59 if opts.Limit > 0 && len(filtered) > opts.Limit { 60 filtered = filtered[:opts.Limit] 61 } 62 63 return filtered, nil 64} 65 66func (m *MockDataSource) Count(ctx context.Context, opts DataOptions) (int, error) { 67 if m.countError != nil { 68 return 0, m.countError 69 } 70 71 count := 0 72 for _, record := range m.records { 73 include := true 74 for filterField, filterValue := range opts.Filters { 75 if record.GetField(filterField) != filterValue { 76 include = false 77 break 78 } 79 } 80 if include { 81 count++ 82 } 83 } 84 85 return count, nil 86} 87 88func createMockRecords() []DataRecord { 89 return []DataRecord{ 90 NewMockRecord(1, map[string]any{ 91 "name": "John Doe", 92 "status": "active", 93 "priority": "high", 94 "project": "alpha", 95 }), 96 NewMockRecord(2, map[string]any{ 97 "name": "Jane Smith", 98 "status": "pending", 99 "priority": "medium", 100 "project": "beta", 101 }), 102 NewMockRecord(3, map[string]any{ 103 "name": "Bob Johnson", 104 "status": "completed", 105 "priority": "low", 106 "project": "alpha", 107 }), 108 } 109} 110 111func createTestFields() []Field { 112 return []Field{ 113 {Name: "name", Title: "Name", Width: 20}, 114 {Name: "status", Title: "Status", Width: 12}, 115 {Name: "priority", Title: "Priority", Width: 10, Formatter: func(v any) string { 116 return strings.ToUpper(fmt.Sprintf("%v", v)) 117 }}, 118 {Name: "project", Title: "Project", Width: 15}, 119 } 120} 121 122func testViewHandler(record DataRecord) string { 123 return fmt.Sprintf("Viewing: %v", record.GetField("name")) 124} 125 126func TestDataTable(t *testing.T) { 127 t.Run("TestDataTableOptions", func(t *testing.T) { 128 t.Run("default options", func(t *testing.T) { 129 source := &MockDataSource{records: createMockRecords()} 130 opts := DataTableOptions{ 131 Fields: createTestFields(), 132 } 133 134 table := NewDataTable(source, opts) 135 if table.opts.Output == nil { 136 t.Error("Output should default to os.Stdout") 137 } 138 if table.opts.Input == nil { 139 t.Error("Input should default to os.Stdin") 140 } 141 if table.opts.Title != "Data" { 142 t.Error("Title should default to 'Data'") 143 } 144 }) 145 146 t.Run("custom options", func(t *testing.T) { 147 var buf bytes.Buffer 148 source := &MockDataSource{records: createMockRecords()} 149 opts := DataTableOptions{ 150 Output: &buf, 151 Static: true, 152 Title: "Test Table", 153 Fields: createTestFields(), 154 ViewHandler: testViewHandler, 155 } 156 157 table := NewDataTable(source, opts) 158 if table.opts.Output != &buf { 159 t.Error("Custom output not set") 160 } 161 if !table.opts.Static { 162 t.Error("Static mode not set") 163 } 164 if table.opts.Title != "Test Table" { 165 t.Error("Custom title not set") 166 } 167 }) 168 }) 169 170 t.Run("Static Mode", func(t *testing.T) { 171 t.Run("successful static display", func(t *testing.T) { 172 var buf bytes.Buffer 173 source := &MockDataSource{records: createMockRecords()} 174 175 table := NewDataTable(source, DataTableOptions{ 176 Output: &buf, 177 Static: true, 178 Title: "Test Table", 179 Fields: createTestFields(), 180 }) 181 182 err := table.Browse(context.Background()) 183 if err != nil { 184 t.Fatalf("Browse failed: %v", err) 185 } 186 187 output := buf.String() 188 if !strings.Contains(output, "Test Table") { 189 t.Error("Title not displayed") 190 } 191 if !strings.Contains(output, "John Doe") { 192 t.Error("First record not displayed") 193 } 194 if !strings.Contains(output, "Jane Smith") { 195 t.Error("Second record not displayed") 196 } 197 if !strings.Contains(output, "Name") { 198 t.Error("Header not displayed") 199 } 200 }) 201 202 t.Run("static display with no records", func(t *testing.T) { 203 var buf bytes.Buffer 204 source := &MockDataSource{records: []DataRecord{}} 205 206 table := NewDataTable(source, DataTableOptions{ 207 Output: &buf, 208 Static: true, 209 Fields: createTestFields(), 210 }) 211 212 err := table.Browse(context.Background()) 213 if err != nil { 214 t.Fatalf("Browse failed: %v", err) 215 } 216 217 output := buf.String() 218 if !strings.Contains(output, "No records found") { 219 t.Error("No records message not displayed") 220 } 221 }) 222 223 t.Run("static display with load error", func(t *testing.T) { 224 var buf bytes.Buffer 225 source := &MockDataSource{ 226 loadError: errors.New("database error"), 227 } 228 229 table := NewDataTable(source, DataTableOptions{ 230 Output: &buf, 231 Static: true, 232 Fields: createTestFields(), 233 }) 234 235 err := table.Browse(context.Background()) 236 if err == nil { 237 t.Fatal("Expected error, got nil") 238 } 239 240 output := buf.String() 241 if !strings.Contains(output, "Error: database error") { 242 t.Error("Error message not displayed") 243 } 244 }) 245 246 t.Run("static display with filters", func(t *testing.T) { 247 var buf bytes.Buffer 248 source := &MockDataSource{records: createMockRecords()} 249 250 table := NewDataTable(source, DataTableOptions{ 251 Output: &buf, 252 Static: true, 253 Fields: createTestFields(), 254 }) 255 256 opts := DataOptions{ 257 Filters: map[string]any{ 258 "status": "active", 259 }, 260 } 261 262 err := table.BrowseWithOptions(context.Background(), opts) 263 if err != nil { 264 t.Fatalf("Browse failed: %v", err) 265 } 266 267 output := buf.String() 268 if !strings.Contains(output, "John Doe") { 269 t.Error("Active record not displayed") 270 } 271 if strings.Contains(output, "Jane Smith") { 272 t.Error("Pending record should be filtered out") 273 } 274 }) 275 }) 276 277 t.Run("Model", func(t *testing.T) { 278 t.Run("initial model state", func(t *testing.T) { 279 model := dataTableModel{ 280 opts: DataTableOptions{ 281 Fields: createTestFields(), 282 }, 283 loading: true, 284 } 285 286 if model.selected != 0 { 287 t.Error("Initial selected should be 0") 288 } 289 if model.viewing { 290 t.Error("Initial viewing should be false") 291 } 292 if !model.loading { 293 t.Error("Initial loading should be true") 294 } 295 }) 296 297 t.Run("load data command", func(t *testing.T) { 298 source := &MockDataSource{records: createMockRecords()} 299 300 model := dataTableModel{ 301 source: source, 302 keys: DefaultDataTableKeys(), 303 dataOpts: DataOptions{}, 304 } 305 306 cmd := model.loadData() 307 if cmd == nil { 308 t.Fatal("loadData should return a command") 309 } 310 311 msg := cmd() 312 switch msg := msg.(type) { 313 case dataLoadedMsg: 314 records := []DataRecord(msg) 315 if len(records) != 3 { 316 t.Errorf("Expected 3 records, got %d", len(records)) 317 } 318 case dataErrorMsg: 319 t.Fatalf("Unexpected error: %v", error(msg)) 320 default: 321 t.Fatalf("Unexpected message type: %T", msg) 322 } 323 }) 324 325 t.Run("load data with error", func(t *testing.T) { 326 source := &MockDataSource{ 327 loadError: errors.New("connection failed"), 328 } 329 330 model := dataTableModel{ 331 source: source, 332 dataOpts: DataOptions{}, 333 } 334 335 cmd := model.loadData() 336 msg := cmd() 337 338 switch msg := msg.(type) { 339 case dataErrorMsg: 340 err := error(msg) 341 if !strings.Contains(err.Error(), "connection failed") { 342 t.Errorf("Expected connection error, got: %v", err) 343 } 344 default: 345 t.Fatalf("Expected dataErrorMsg, got: %T", msg) 346 } 347 }) 348 349 t.Run("load count command", func(t *testing.T) { 350 source := &MockDataSource{records: createMockRecords()} 351 352 model := dataTableModel{ 353 source: source, 354 dataOpts: DataOptions{}, 355 } 356 357 cmd := model.loadCount() 358 msg := cmd() 359 360 switch msg := msg.(type) { 361 case dataCountMsg: 362 count := int(msg) 363 if count != 3 { 364 t.Errorf("Expected count 3, got %d", count) 365 } 366 default: 367 t.Fatalf("Expected dataCountMsg, got: %T", msg) 368 } 369 }) 370 371 t.Run("load count with error", func(t *testing.T) { 372 source := &MockDataSource{ 373 records: createMockRecords(), 374 countError: errors.New("count failed"), 375 } 376 377 model := dataTableModel{ 378 source: source, 379 dataOpts: DataOptions{}, 380 } 381 382 cmd := model.loadCount() 383 msg := cmd() 384 385 switch msg := msg.(type) { 386 case dataCountMsg: 387 count := int(msg) 388 if count != 0 { 389 t.Errorf("Expected count 0 on error, got %d", count) 390 } 391 default: 392 t.Fatalf("Expected dataCountMsg even on error, got: %T", msg) 393 } 394 }) 395 396 t.Run("view record command", func(t *testing.T) { 397 model := dataTableModel{ 398 opts: DataTableOptions{ 399 ViewHandler: testViewHandler, 400 Fields: createTestFields(), 401 }, 402 } 403 404 record := createMockRecords()[0] 405 cmd := model.viewRecord(record) 406 msg := cmd() 407 408 switch msg := msg.(type) { 409 case dataViewMsg: 410 content := string(msg) 411 if !strings.Contains(content, "Viewing: John Doe") { 412 t.Error("View content not formatted correctly") 413 } 414 default: 415 t.Fatalf("Expected dataViewMsg, got: %T", msg) 416 } 417 }) 418 }) 419 420 t.Run("Key Handling", func(t *testing.T) { 421 source := &MockDataSource{records: createMockRecords()} 422 423 t.Run("navigation keys", func(t *testing.T) { 424 model := dataTableModel{ 425 source: source, 426 records: createMockRecords(), 427 selected: 1, 428 keys: DefaultDataTableKeys(), 429 opts: DataTableOptions{Fields: createTestFields()}, 430 } 431 432 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 433 if m, ok := newModel.(dataTableModel); ok { 434 if m.selected != 0 { 435 t.Errorf("Up key should move selection to 0, got %d", m.selected) 436 } 437 } 438 439 model.selected = 1 440 newModel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 441 if m, ok := newModel.(dataTableModel); ok { 442 if m.selected != 2 { 443 t.Errorf("Down key should move selection to 2, got %d", m.selected) 444 } 445 } 446 }) 447 448 t.Run("boundary conditions", func(t *testing.T) { 449 model := dataTableModel{ 450 source: source, 451 records: createMockRecords(), 452 selected: 0, 453 keys: DefaultDataTableKeys(), 454 opts: DataTableOptions{Fields: createTestFields()}, 455 } 456 457 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 458 if m, ok := newModel.(dataTableModel); ok { 459 if m.selected != 0 { 460 t.Error("Up key at top should not change selection") 461 } 462 } 463 464 model.selected = 2 465 newModel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 466 if m, ok := newModel.(dataTableModel); ok { 467 if m.selected != 2 { 468 t.Error("Down key at bottom should not change selection") 469 } 470 } 471 }) 472 473 t.Run("number shortcuts", func(t *testing.T) { 474 model := dataTableModel{ 475 source: source, 476 records: createMockRecords(), 477 keys: DefaultDataTableKeys(), 478 opts: DataTableOptions{Fields: createTestFields()}, 479 } 480 481 for i := 1; i <= 3; i++ { 482 key := fmt.Sprintf("%d", i) 483 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}) 484 if m, ok := newModel.(dataTableModel); ok { 485 expectedIndex := i - 1 486 if m.selected != expectedIndex { 487 t.Errorf("Number key %s should select index %d, got %d", key, expectedIndex, m.selected) 488 } 489 } 490 } 491 }) 492 493 t.Run("view key with handler", func(t *testing.T) { 494 viewHandler := func(record DataRecord) string { 495 return "test view" 496 } 497 498 model := dataTableModel{ 499 source: source, 500 records: createMockRecords(), 501 keys: DefaultDataTableKeys(), 502 opts: DataTableOptions{ 503 Fields: createTestFields(), 504 ViewHandler: viewHandler, 505 }, 506 } 507 508 _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("v")}) 509 if cmd == nil { 510 t.Error("View key should return command when handler is set") 511 } 512 }) 513 514 t.Run("view key without handler", func(t *testing.T) { 515 model := dataTableModel{ 516 source: source, 517 records: createMockRecords(), 518 keys: DefaultDataTableKeys(), 519 opts: DataTableOptions{Fields: createTestFields()}, 520 } 521 522 _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("v")}) 523 if cmd != nil { 524 t.Error("View key should not return command when no handler is set") 525 } 526 }) 527 528 t.Run("quit key", func(t *testing.T) { 529 model := dataTableModel{ 530 keys: DefaultDataTableKeys(), 531 opts: DataTableOptions{Fields: createTestFields()}, 532 } 533 534 _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}) 535 if cmd == nil { 536 t.Error("Quit key should return quit command") 537 } 538 }) 539 540 t.Run("refresh key", func(t *testing.T) { 541 model := dataTableModel{ 542 source: source, 543 keys: DefaultDataTableKeys(), 544 opts: DataTableOptions{Fields: createTestFields()}, 545 } 546 547 newModel, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")}) 548 if cmd == nil { 549 t.Error("Refresh key should return command") 550 } 551 if m, ok := newModel.(dataTableModel); ok { 552 if !m.loading { 553 t.Error("Refresh should set loading to true") 554 } 555 } 556 }) 557 558 t.Run("help mode", func(t *testing.T) { 559 model := dataTableModel{ 560 keys: DefaultDataTableKeys(), 561 showingHelp: true, 562 opts: DataTableOptions{Fields: createTestFields()}, 563 } 564 565 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 566 if m, ok := newModel.(dataTableModel); ok { 567 if m.selected != 0 { 568 t.Error("Navigation should be ignored in help mode") 569 } 570 } 571 572 newModel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}) 573 if m, ok := newModel.(dataTableModel); ok { 574 if m.showingHelp { 575 t.Error("Help key should exit help mode") 576 } 577 } 578 }) 579 580 t.Run("viewing mode", func(t *testing.T) { 581 model := dataTableModel{ 582 keys: DefaultDataTableKeys(), 583 viewing: true, 584 viewContent: "test content", 585 opts: DataTableOptions{Fields: createTestFields()}, 586 } 587 588 newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}) 589 if m, ok := newModel.(dataTableModel); ok { 590 if m.viewing { 591 t.Error("Quit should exit viewing mode") 592 } 593 if m.viewContent != "" { 594 t.Error("Quit should clear view content") 595 } 596 } 597 }) 598 }) 599 600 t.Run("View", func(t *testing.T) { 601 source := &MockDataSource{records: createMockRecords()} 602 603 t.Run("normal view", func(t *testing.T) { 604 model := dataTableModel{ 605 source: source, 606 records: createMockRecords(), 607 keys: DefaultDataTableKeys(), 608 help: help.New(), 609 opts: DataTableOptions{Title: "Test", Fields: createTestFields()}, 610 } 611 612 view := model.View() 613 if !strings.Contains(view, "Test") { 614 t.Error("Title not displayed") 615 } 616 if !strings.Contains(view, "John Doe") { 617 t.Error("Record data not displayed") 618 } 619 if !strings.Contains(view, "Name") { 620 t.Error("Headers not displayed") 621 } 622 if !strings.Contains(view, " > ") { 623 t.Error("Selection indicator not displayed") 624 } 625 }) 626 627 t.Run("loading view", func(t *testing.T) { 628 model := dataTableModel{ 629 loading: true, 630 opts: DataTableOptions{Title: "Test", Fields: createTestFields()}, 631 } 632 633 view := model.View() 634 if !strings.Contains(view, "Loading...") { 635 t.Error("Loading message not displayed") 636 } 637 }) 638 639 t.Run("error view", func(t *testing.T) { 640 model := dataTableModel{ 641 err: errors.New("test error"), 642 opts: DataTableOptions{Title: "Test", Fields: createTestFields()}, 643 } 644 645 view := model.View() 646 if !strings.Contains(view, "Error: test error") { 647 t.Error("Error message not displayed") 648 } 649 }) 650 651 t.Run("empty records view", func(t *testing.T) { 652 model := dataTableModel{ 653 records: []DataRecord{}, 654 opts: DataTableOptions{Title: "Test", Fields: createTestFields()}, 655 } 656 657 view := model.View() 658 if !strings.Contains(view, "No records found") { 659 t.Error("Empty message not displayed") 660 } 661 }) 662 663 t.Run("viewing mode", func(t *testing.T) { 664 model := dataTableModel{ 665 viewing: true, 666 viewContent: "# Test Content\nDetails here", 667 opts: DataTableOptions{Fields: createTestFields()}, 668 } 669 670 view := model.View() 671 if !strings.Contains(view, "# Test Content") { 672 t.Error("View content not displayed") 673 } 674 if !strings.Contains(view, "Press q/esc/backspace to return") { 675 t.Error("Return instructions not displayed") 676 } 677 }) 678 679 t.Run("help mode", func(t *testing.T) { 680 model := dataTableModel{ 681 showingHelp: true, 682 keys: DefaultDataTableKeys(), 683 help: help.New(), 684 opts: DataTableOptions{Fields: createTestFields()}, 685 } 686 687 view := model.View() 688 if view == "" { 689 t.Error("Help view should not be empty") 690 } 691 }) 692 693 t.Run("field formatters", func(t *testing.T) { 694 fields := []Field{ 695 {Name: "priority", Title: "Priority", Width: 10, Formatter: func(v any) string { 696 return strings.ToUpper(fmt.Sprintf("%v", v)) 697 }}, 698 } 699 700 model := dataTableModel{ 701 records: createMockRecords(), 702 opts: DataTableOptions{Fields: fields}, 703 } 704 705 view := model.View() 706 if !strings.Contains(view, "HIGH") { 707 t.Error("Field formatter not applied") 708 } 709 }) 710 711 t.Run("long field truncation", func(t *testing.T) { 712 longRecord := NewMockRecord(1, map[string]any{ 713 "name": "This is a very long name that should be truncated", 714 }) 715 716 fields := []Field{ 717 {Name: "name", Title: "Name", Width: 10}, 718 } 719 720 model := dataTableModel{ 721 records: []DataRecord{longRecord}, 722 opts: DataTableOptions{Fields: fields}, 723 } 724 725 view := model.View() 726 if !strings.Contains(view, "...") { 727 t.Error("Long field should be truncated with ellipsis") 728 } 729 }) 730 }) 731 732 t.Run("Update", func(t *testing.T) { 733 source := &MockDataSource{records: createMockRecords()} 734 735 t.Run("data loaded message", func(t *testing.T) { 736 model := dataTableModel{ 737 source: source, 738 loading: true, 739 opts: DataTableOptions{Fields: createTestFields()}, 740 } 741 742 records := createMockRecords()[:2] 743 newModel, _ := model.Update(dataLoadedMsg(records)) 744 745 if m, ok := newModel.(dataTableModel); ok { 746 if len(m.records) != 2 { 747 t.Errorf("Expected 2 records, got %d", len(m.records)) 748 } 749 if m.loading { 750 t.Error("Loading should be set to false") 751 } 752 } 753 }) 754 755 t.Run("selected index adjustment", func(t *testing.T) { 756 model := dataTableModel{ 757 selected: 5, 758 opts: DataTableOptions{Fields: createTestFields()}, 759 } 760 761 records := createMockRecords()[:2] 762 newModel, _ := model.Update(dataLoadedMsg(records)) 763 764 if m, ok := newModel.(dataTableModel); ok { 765 if m.selected != 1 { 766 t.Errorf("Selected should be adjusted to 1, got %d", m.selected) 767 } 768 } 769 }) 770 771 t.Run("data view message", func(t *testing.T) { 772 model := dataTableModel{ 773 opts: DataTableOptions{Fields: createTestFields()}, 774 } 775 776 content := "Test view content" 777 newModel, _ := model.Update(dataViewMsg(content)) 778 779 if m, ok := newModel.(dataTableModel); ok { 780 if !m.viewing { 781 t.Error("Viewing mode should be activated") 782 } 783 if m.viewContent != content { 784 t.Error("View content not set correctly") 785 } 786 } 787 }) 788 789 t.Run("data error message", func(t *testing.T) { 790 model := dataTableModel{ 791 loading: true, 792 opts: DataTableOptions{Fields: createTestFields()}, 793 } 794 795 testErr := errors.New("test error") 796 newModel, _ := model.Update(dataErrorMsg(testErr)) 797 798 if m, ok := newModel.(dataTableModel); ok { 799 if m.err == nil { 800 t.Error("Error should be set") 801 } 802 if m.err.Error() != "test error" { 803 t.Errorf("Expected 'test error', got %v", m.err) 804 } 805 if m.loading { 806 t.Error("Loading should be set to false on error") 807 } 808 } 809 }) 810 811 t.Run("data count message", func(t *testing.T) { 812 model := dataTableModel{ 813 opts: DataTableOptions{Fields: createTestFields()}, 814 } 815 816 count := 42 817 newModel, _ := model.Update(dataCountMsg(count)) 818 819 if m, ok := newModel.(dataTableModel); ok { 820 if m.totalCount != count { 821 t.Errorf("Expected count %d, got %d", count, m.totalCount) 822 } 823 } 824 }) 825 }) 826 827 t.Run("Default Keys", func(t *testing.T) { 828 keys := DefaultDataTableKeys() 829 830 if len(keys.Numbers) != 9 { 831 t.Errorf("Expected 9 number bindings, got %d", len(keys.Numbers)) 832 } 833 834 if keys.Actions == nil { 835 t.Error("Actions map should be initialized") 836 } 837 }) 838 839 t.Run("Actions", func(t *testing.T) { 840 t.Run("action key handling", func(t *testing.T) { 841 actionCalled := false 842 action := Action{ 843 Key: "d", 844 Description: "delete", 845 Handler: func(record DataRecord) tea.Cmd { 846 actionCalled = true 847 return nil 848 }, 849 } 850 851 keys := DefaultDataTableKeys() 852 keys.Actions["d"] = key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")) 853 854 model := dataTableModel{ 855 source: &MockDataSource{records: createMockRecords()}, 856 records: createMockRecords(), 857 keys: keys, 858 opts: DataTableOptions{ 859 Fields: createTestFields(), 860 Actions: []Action{action}, 861 }, 862 } 863 864 _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("d")}) 865 if cmd != nil { 866 cmd() 867 } 868 869 if !actionCalled { 870 t.Error("Action handler should be called") 871 } 872 }) 873 }) 874 875 t.Run("Field", func(t *testing.T) { 876 t.Run("field without formatter", func(t *testing.T) { 877 field := Field{Name: "test"} 878 879 record := NewMockRecord(1, map[string]any{ 880 "test": "value", 881 }) 882 883 value := record.GetField(field.Name) 884 displayValue := fmt.Sprintf("%v", value) 885 886 if displayValue != "value" { 887 t.Errorf("Expected 'value', got '%s'", displayValue) 888 } 889 }) 890 891 t.Run("field with formatter", func(t *testing.T) { 892 field := Field{ 893 Name: "test", 894 Formatter: func(v any) string { 895 return strings.ToUpper(fmt.Sprintf("%v", v)) 896 }, 897 } 898 899 record := NewMockRecord(1, map[string]any{ 900 "test": "value", 901 }) 902 903 value := record.GetField(field.Name) 904 displayValue := field.Formatter(value) 905 906 if displayValue != "VALUE" { 907 t.Errorf("Expected 'VALUE', got '%s'", displayValue) 908 } 909 }) 910 }) 911}