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