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