cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm
leaflet
readability
golang
1package ui
2
3import (
4 "strings"
5 "testing"
6 "time"
7
8 "github.com/charmbracelet/bubbles/textinput"
9 tea "github.com/charmbracelet/bubbletea"
10)
11
12func createTestAuthFormModel(handle string) authFormModel {
13 handleInput := textinput.New()
14 handleInput.Placeholder = "username.bsky.social"
15 handleInput.Width = 40
16
17 passwordInput := textinput.New()
18 passwordInput.Placeholder = "App password"
19 passwordInput.Width = 40
20 passwordInput.EchoMode = textinput.EchoPassword
21 passwordInput.EchoCharacter = '•'
22
23 handleLocked := false
24 focusIndex := 0
25
26 if handle != "" {
27 handleInput.SetValue(handle)
28 handleLocked = true
29 focusIndex = 1
30 }
31
32 return authFormModel{
33 handleInput: handleInput,
34 passwordInput: passwordInput,
35 focusIndex: focusIndex,
36 keys: authFormKeys,
37 handleLocked: handleLocked,
38 }
39}
40
41func TestAuthFormModel(t *testing.T) {
42 t.Run("Init", func(t *testing.T) {
43 t.Run("focuses handle input when no initial handle", func(t *testing.T) {
44 model := createTestAuthFormModel("")
45
46 cmd := model.Init()
47 if cmd == nil {
48 t.Error("Expected Init to return a focus command")
49 }
50 })
51
52 t.Run("focuses password input when handle is locked", func(t *testing.T) {
53 model := createTestAuthFormModel("test.bsky.social")
54
55 cmd := model.Init()
56 if cmd == nil {
57 t.Error("Expected Init to return a focus command")
58 }
59
60 if !model.handleLocked {
61 t.Error("Expected handleLocked to be true")
62 }
63 if model.focusIndex != 1 {
64 t.Errorf("Expected focusIndex to be 1, got %d", model.focusIndex)
65 }
66 })
67 })
68
69 t.Run("Navigation", func(t *testing.T) {
70 t.Run("tab moves to next field", func(t *testing.T) {
71 model := createTestAuthFormModel("")
72 if cmd := model.Init(); cmd != nil {
73 model.handleInput.Focus()
74 }
75
76 suite := NewTUITestSuite(t, model)
77 suite.Start()
78
79 if err := suite.SendKey(tea.KeyTab); err != nil {
80 t.Fatalf("Failed to send tab key: %v", err)
81 }
82
83 if err := suite.WaitFor(func(m tea.Model) bool {
84 if authModel, ok := m.(authFormModel); ok {
85 return authModel.focusIndex == 1
86 }
87 return false
88 }, 1*time.Second); err != nil {
89 t.Errorf("Expected focusIndex to change to 1: %v", err)
90 }
91 })
92
93 t.Run("shift+tab moves to previous field", func(t *testing.T) {
94 model := createTestAuthFormModel("")
95 if cmd := model.Init(); cmd != nil {
96 model.handleInput.Focus()
97 }
98 model.focusIndex = 1
99
100 suite := NewTUITestSuite(t, model)
101 suite.Start()
102
103 if err := suite.SendKey(tea.KeyShiftTab); err != nil {
104 t.Fatalf("Failed to send shift+tab key: %v", err)
105 }
106
107 if err := suite.WaitFor(func(m tea.Model) bool {
108 if authModel, ok := m.(authFormModel); ok {
109 return authModel.focusIndex == 0
110 }
111 return false
112 }, 1*time.Second); err != nil {
113 t.Errorf("Expected focusIndex to change to 0: %v", err)
114 }
115 })
116
117 t.Run("locked handle prevents navigation to handle field", func(t *testing.T) {
118 model := createTestAuthFormModel("test.bsky.social")
119
120 suite := NewTUITestSuite(t, model)
121 suite.Start()
122
123 if err := suite.SendKey(tea.KeyShiftTab); err != nil {
124 t.Fatalf("Failed to send shift+tab key: %v", err)
125 }
126
127 if err := suite.WaitFor(func(m tea.Model) bool {
128 if authModel, ok := m.(authFormModel); ok {
129 return authModel.focusIndex == 1
130 }
131 return false
132 }, 500*time.Millisecond); err != nil {
133 t.Errorf("Expected focusIndex to stay at 1 when handle is locked: %v", err)
134 }
135 })
136 })
137
138 t.Run("Submission", func(t *testing.T) {
139 t.Run("enter submits when both fields are filled", func(t *testing.T) {
140 model := createTestAuthFormModel("")
141 model.handleInput.SetValue("test.bsky.social")
142 model.passwordInput.SetValue("test-password")
143
144 suite := NewTUITestSuite(t, model)
145 suite.Start()
146
147 if err := suite.SendKey(tea.KeyEnter); err != nil {
148 t.Fatalf("Failed to send enter key: %v", err)
149 }
150
151 if err := suite.WaitFor(func(m tea.Model) bool {
152 if authModel, ok := m.(authFormModel); ok {
153 return authModel.submitted
154 }
155 return false
156 }, 1*time.Second); err != nil {
157 t.Errorf("Expected model to be submitted: %v", err)
158 }
159 })
160
161 t.Run("enter does not submit when handle is empty", func(t *testing.T) {
162 model := createTestAuthFormModel("")
163 model.passwordInput.SetValue("test-password")
164
165 suite := NewTUITestSuite(t, model)
166 suite.Start()
167
168 if err := suite.SendKey(tea.KeyEnter); err != nil {
169 t.Fatalf("Failed to send enter key: %v", err)
170 }
171
172 time.Sleep(100 * time.Millisecond)
173
174 currentModel := suite.GetCurrentModel()
175 if authModel, ok := currentModel.(authFormModel); ok {
176 if authModel.submitted {
177 t.Error("Expected model to not be submitted when handle is empty")
178 }
179 }
180 })
181
182 t.Run("enter does not submit when password is empty", func(t *testing.T) {
183 model := createTestAuthFormModel("")
184 model.handleInput.SetValue("test.bsky.social")
185
186 suite := NewTUITestSuite(t, model)
187 suite.Start()
188
189 if err := suite.SendKey(tea.KeyEnter); err != nil {
190 t.Fatalf("Failed to send enter key: %v", err)
191 }
192
193 time.Sleep(100 * time.Millisecond)
194
195 currentModel := suite.GetCurrentModel()
196 if authModel, ok := currentModel.(authFormModel); ok {
197 if authModel.submitted {
198 t.Error("Expected model to not be submitted when password is empty")
199 }
200 }
201 })
202
203 t.Run("ctrl+s submits when both fields are filled", func(t *testing.T) {
204 model := createTestAuthFormModel("")
205 model.handleInput.SetValue("test.bsky.social")
206 model.passwordInput.SetValue("test-password")
207
208 suite := NewTUITestSuite(t, model)
209 suite.Start()
210
211 if err := suite.SendKeyString("ctrl+s"); err != nil {
212 t.Fatalf("Failed to send ctrl+s: %v", err)
213 }
214
215 if err := suite.WaitFor(func(m tea.Model) bool {
216 if authModel, ok := m.(authFormModel); ok {
217 return authModel.submitted
218 }
219 return false
220 }, 1*time.Second); err != nil {
221 t.Errorf("Expected model to be submitted: %v", err)
222 }
223 })
224 })
225
226 t.Run("Cancellation", func(t *testing.T) {
227 t.Run("esc cancels the form", func(t *testing.T) {
228 model := createTestAuthFormModel("")
229
230 suite := NewTUITestSuite(t, model)
231 suite.Start()
232
233 if err := suite.SendKey(tea.KeyEsc); err != nil {
234 t.Fatalf("Failed to send esc key: %v", err)
235 }
236
237 if err := suite.WaitFor(func(m tea.Model) bool {
238 if authModel, ok := m.(authFormModel); ok {
239 return authModel.canceled
240 }
241 return false
242 }, 1*time.Second); err != nil {
243 t.Errorf("Expected model to be canceled: %v", err)
244 }
245 })
246
247 t.Run("ctrl+c cancels the form", func(t *testing.T) {
248 model := createTestAuthFormModel("")
249
250 suite := NewTUITestSuite(t, model)
251 suite.Start()
252
253 if err := suite.SendKeyString("ctrl+c"); err != nil {
254 t.Fatalf("Failed to send ctrl+c: %v", err)
255 }
256
257 if err := suite.WaitFor(func(m tea.Model) bool {
258 if authModel, ok := m.(authFormModel); ok {
259 return authModel.canceled
260 }
261 return false
262 }, 1*time.Second); err != nil {
263 t.Errorf("Expected model to be canceled: %v", err)
264 }
265 })
266 })
267
268 t.Run("View", func(t *testing.T) {
269 t.Run("displays handle and password fields", func(t *testing.T) {
270 model := createTestAuthFormModel("")
271
272 view := model.View()
273
274 if !strings.Contains(view, "AT Protocol Authentication") {
275 t.Error("Expected view to contain title")
276 }
277 if !strings.Contains(view, "BlueSky Handle:") {
278 t.Error("Expected view to contain handle label")
279 }
280 if !strings.Contains(view, "App Password:") {
281 t.Error("Expected view to contain password label")
282 }
283 })
284
285 t.Run("displays locked status for handle", func(t *testing.T) {
286 model := createTestAuthFormModel("test.bsky.social")
287
288 view := model.View()
289
290 if !strings.Contains(view, "test.bsky.social") {
291 t.Error("Expected view to contain handle value")
292 }
293 if !strings.Contains(view, "(locked)") {
294 t.Error("Expected view to indicate handle is locked")
295 }
296 })
297
298 t.Run("displays validation messages when fields are empty", func(t *testing.T) {
299 model := createTestAuthFormModel("")
300
301 view := model.View()
302
303 if !strings.Contains(view, "Handle is required") {
304 t.Error("Expected view to show handle validation message")
305 }
306 if !strings.Contains(view, "Password is required") {
307 t.Error("Expected view to show password validation message")
308 }
309 })
310
311 t.Run("displays help text", func(t *testing.T) {
312 model := createTestAuthFormModel("")
313
314 view := model.View()
315
316 if !strings.Contains(view, "tab/shift+tab: navigate") {
317 t.Error("Expected view to contain navigation help")
318 }
319 if !strings.Contains(view, "enter/ctrl+s: submit") {
320 t.Error("Expected view to contain submit help")
321 }
322 if !strings.Contains(view, "esc/ctrl+c: cancel") {
323 t.Error("Expected view to contain cancel help")
324 }
325 })
326 })
327
328 t.Run("Input handling", func(t *testing.T) {
329 t.Run("accepts text input in handle field", func(t *testing.T) {
330 model := createTestAuthFormModel("")
331 if cmd := model.Init(); cmd != nil {
332 model.handleInput.Focus()
333 }
334
335 suite := NewTUITestSuite(t, model)
336 suite.Start()
337
338 if err := suite.SendKeyString("t"); err != nil {
339 t.Fatalf("Failed to send 't' key: %v", err)
340 }
341
342 if err := suite.WaitFor(func(m tea.Model) bool {
343 if authModel, ok := m.(authFormModel); ok {
344 return authModel.handleInput.Value() == "t"
345 }
346 return false
347 }, 1*time.Second); err != nil {
348 t.Errorf("Expected handle input to contain 't': %v", err)
349 }
350 })
351
352 t.Run("does not accept text input in locked handle field", func(t *testing.T) {
353 model := createTestAuthFormModel("test.bsky.social")
354 originalValue := model.handleInput.Value()
355
356 suite := NewTUITestSuite(t, model)
357 suite.Start()
358
359 if err := suite.SendKeyString("x"); err != nil {
360 t.Fatalf("Failed to send 'x' key: %v", err)
361 }
362
363 time.Sleep(100 * time.Millisecond)
364
365 currentModel := suite.GetCurrentModel()
366 if authModel, ok := currentModel.(authFormModel); ok {
367 if authModel.handleInput.Value() != originalValue {
368 t.Errorf("Expected handle input to remain unchanged, got '%s'", authModel.handleInput.Value())
369 }
370 }
371 })
372 })
373}
374
375func TestNewAuthForm(t *testing.T) {
376 t.Run("creates form with default options", func(t *testing.T) {
377 form := NewAuthForm("", AuthFormOptions{})
378
379 if form == nil {
380 t.Fatal("Expected form to be created")
381 }
382 if form.opts.Width != 80 {
383 t.Errorf("Expected default width 80, got %d", form.opts.Width)
384 }
385 if form.opts.Height != 24 {
386 t.Errorf("Expected default height 24, got %d", form.opts.Height)
387 }
388 if form.opts.Output == nil {
389 t.Error("Expected output to be set to default")
390 }
391 if form.opts.Input == nil {
392 t.Error("Expected input to be set to default")
393 }
394 })
395
396 t.Run("creates form with initial handle", func(t *testing.T) {
397 handle := "test.bsky.social"
398 form := NewAuthForm(handle, AuthFormOptions{})
399
400 if form.initialHandle != handle {
401 t.Errorf("Expected initialHandle '%s', got '%s'", handle, form.initialHandle)
402 }
403 })
404
405 t.Run("creates form with custom options", func(t *testing.T) {
406 opts := AuthFormOptions{
407 Width: 100,
408 Height: 30,
409 }
410 form := NewAuthForm("", opts)
411
412 if form.opts.Width != 100 {
413 t.Errorf("Expected width 100, got %d", form.opts.Width)
414 }
415 if form.opts.Height != 30 {
416 t.Errorf("Expected height 30, got %d", form.opts.Height)
417 }
418 })
419}