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 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}