cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package ui
2
3import (
4 "testing"
5 "time"
6
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/stormlightlabs/noteleaf/internal/models"
9)
10
11func TestInteractiveTUIBehavior(t *testing.T) {
12 t.Run("Priority Mode Switching with TUI Framework", func(t *testing.T) {
13 task := &models.Task{ID: 1, Priority: models.PriorityHigh}
14 model := createTestTaskEditModel(task)
15
16 suite := NewTUITestSuite(t, model, WithInitialSize(80, 24))
17 suite.Start()
18
19 if err := suite.SendKeyString("p"); err != nil {
20 t.Fatalf("Failed to send 'p' key: %v", err)
21 }
22
23 if err := suite.WaitFor(func(m tea.Model) bool {
24 if taskModel, ok := m.(taskEditModel); ok {
25 return taskModel.mode == priorityPicker
26 }
27 return false
28 }, 1*time.Second); err != nil {
29 t.Fatalf("Failed to enter priority picker mode: %v", err)
30 }
31
32 if err := suite.SendKeyString("m"); err != nil {
33 t.Fatalf("Failed to send 'm' key: %v", err)
34 }
35
36 if err := suite.WaitFor(func(m tea.Model) bool {
37 if taskModel, ok := m.(taskEditModel); ok {
38 return taskModel.priorityMode == priorityModeNumeric
39 }
40 return false
41 }, 1*time.Second); err != nil {
42 t.Fatalf("Failed to switch to numeric priority mode: %v", err)
43 }
44
45 if err := suite.WaitForView("Numeric", 1*time.Second); err != nil {
46 t.Errorf("Expected view to contain 'Numeric': %v", err)
47 }
48
49 if err := suite.SendKeyString("m"); err != nil {
50 t.Fatalf("Failed to send second 'm' key: %v", err)
51 }
52
53 if err := suite.WaitFor(func(m tea.Model) bool {
54 if taskModel, ok := m.(taskEditModel); ok {
55 return taskModel.priorityMode == priorityModeLegacy
56 }
57 return false
58 }, 1*time.Second); err != nil {
59 t.Fatalf("Failed to switch to legacy priority mode: %v", err)
60 }
61
62 if err := suite.WaitForView("Legacy", 1*time.Second); err != nil {
63 t.Errorf("Expected view to contain 'Legacy': %v", err)
64 }
65 })
66
67 t.Run("Keyboard Navigation with TUI Framework", func(t *testing.T) {
68 task := &models.Task{ID: 1, Status: models.StatusTodo}
69 model := createTestTaskEditModel(task)
70
71 suite := NewTUITestSuite(t, model)
72 suite.Start()
73
74 if err := suite.SendKeyString("s"); err != nil {
75 t.Fatalf("Failed to send 's' key: %v", err)
76 }
77
78 if err := suite.WaitFor(func(m tea.Model) bool {
79 if taskModel, ok := m.(taskEditModel); ok {
80 return taskModel.mode == statusPicker
81 }
82 return false
83 }, 1*time.Second); err != nil {
84 t.Fatalf("Failed to enter status picker mode: %v", err)
85 }
86
87 if err := suite.SendKey(tea.KeyDown); err != nil {
88 t.Fatalf("Failed to send down arrow: %v", err)
89 }
90
91 if err := suite.WaitFor(func(m tea.Model) bool {
92 if taskModel, ok := m.(taskEditModel); ok {
93 return taskModel.statusIndex == 1
94 }
95 return false
96 }, 1*time.Second); err != nil {
97 t.Fatalf("Status index should have changed to 1: %v", err)
98 }
99
100 if err := suite.SendKey(tea.KeyEsc); err != nil {
101 t.Fatalf("Failed to send escape key: %v", err)
102 }
103
104 if err := suite.WaitFor(func(m tea.Model) bool {
105 if taskModel, ok := m.(taskEditModel); ok {
106 return taskModel.mode == fieldNavigation
107 }
108 return false
109 }, 1*time.Second); err != nil {
110 t.Fatalf("Should have returned to field navigation mode: %v", err)
111 }
112 })
113
114 t.Run("Window Resize Handling", func(t *testing.T) {
115 task := &models.Task{ID: 1}
116 model := createTestTaskEditModel(task)
117
118 suite := NewTUITestSuite(t, model)
119 suite.Start()
120
121 resizeMsg := tea.WindowSizeMsg{Width: 120, Height: 40}
122 if err := suite.SendMessage(resizeMsg); err != nil {
123 t.Fatalf("Failed to send window resize message: %v", err)
124 }
125
126 if err := suite.WaitFor(func(m tea.Model) bool {
127 if taskModel, ok := m.(taskEditModel); ok {
128 return taskModel.opts.Width == 120
129 }
130 return false
131 }, 1*time.Second); err != nil {
132 t.Fatalf("Window width should have been updated to 120: %v", err)
133 }
134 })
135
136 t.Run("Complex Key Sequence with TUI Framework", func(t *testing.T) {
137 task := &models.Task{ID: 1, Description: "Test", Project: "TestProject"}
138 model := createTestTaskEditModel(task)
139
140 suite := NewTUITestSuite(t, model)
141 suite.Start()
142
143 keySequence := []KeyWithTiming{
144 {KeyType: tea.KeyDown, Delay: 50 * time.Millisecond},
145 {KeyType: tea.KeyDown, Delay: 50 * time.Millisecond},
146 {KeyType: tea.KeyDown, Delay: 50 * time.Millisecond},
147 {KeyType: tea.KeyEnter, Delay: 100 * time.Millisecond},
148 }
149
150 if err := suite.SimulateKeySequence(keySequence); err != nil {
151 t.Fatalf("Failed to simulate key sequence: %v", err)
152 }
153
154 if err := suite.WaitFor(func(m tea.Model) bool {
155 if taskModel, ok := m.(taskEditModel); ok {
156 return taskModel.mode == textInput && taskModel.currentField == 3 // Project field
157 }
158 return false
159 }, 2*time.Second); err != nil {
160 t.Fatalf("Should have entered text input mode for project field: %v", err)
161 }
162
163 if err := suite.SendKeyString(" Updated"); err != nil {
164 t.Fatalf("Failed to send text: %v", err)
165 }
166
167 if err := suite.SendKey(tea.KeyEnter); err != nil {
168 t.Fatalf("Failed to send enter key: %v", err)
169 }
170
171 if err := suite.WaitFor(func(m tea.Model) bool {
172 if taskModel, ok := m.(taskEditModel); ok {
173 return taskModel.mode == fieldNavigation &&
174 taskModel.task.Project == "TestProject Updated"
175 }
176 return false
177 }, 1*time.Second); err != nil {
178 t.Fatalf("Project should have been updated: %v", err)
179 }
180 })
181}
182
183func TestTUIFrameworkFeatures(t *testing.T) {
184 t.Run("Output Capture", func(t *testing.T) {
185 task := &models.Task{ID: 1, Description: "Test Output"}
186 model := createTestTaskEditModel(task)
187
188 suite := NewTUITestSuite(t, model)
189 suite.Start()
190
191 time.Sleep(100 * time.Millisecond)
192
193 view := suite.GetCurrentView()
194 if len(view) == 0 {
195 t.Error("View should not be empty")
196 }
197
198 if !containsString(view, "Test Output") {
199 t.Error("View should contain task description")
200 }
201 })
202
203 t.Run("Timeout Handling", func(t *testing.T) {
204 task := &models.Task{ID: 1}
205 model := createTestTaskEditModel(task)
206
207 suite := NewTUITestSuite(t, model, WithTimeout(100*time.Millisecond))
208 suite.Start()
209
210 if err := suite.WaitFor(func(m tea.Model) bool {
211 return false
212 }, 50*time.Millisecond); err == nil {
213 t.Error("Expected timeout error")
214 }
215 })
216
217 t.Run("Multiple Assertions", func(t *testing.T) {
218 task := &models.Task{ID: 1, Description: "Test Task", Status: models.StatusTodo}
219 model := createTestTaskEditModel(task)
220
221 suite := NewTUITestSuite(t, model)
222 suite.Start()
223
224 Expect.AssertViewContains(t, suite, "Test Task", "View should contain task description")
225 Expect.AssertViewContains(t, suite, models.StatusTodo, "View should contain status")
226 Expect.AssertModelState(t, suite, func(m tea.Model) bool {
227 if taskModel, ok := m.(taskEditModel); ok {
228 return taskModel.mode == fieldNavigation
229 }
230 return false
231 }, "Model should be in field navigation mode")
232 })
233}