cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}