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