cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 419 lines 11 kB view raw
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}