Skip to content

Commit a21fb7b

Browse files
authored
test: add tests for cloudpicker and modal UI components (#40)
Add 22 tests covering the Bubble Tea state machines for cloudpicker, confirm modal, and error modal — cursor navigation, key handling, focus toggling, and window resize. Closes #34
1 parent c00d459 commit a21fb7b

3 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cloudpicker
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/larkly/lazystack/internal/shared"
8+
tea "charm.land/bubbletea/v2"
9+
)
10+
11+
func TestNew(t *testing.T) {
12+
clouds := []string{"alpha", "beta", "gamma"}
13+
m := New(clouds, nil)
14+
15+
if len(m.clouds) != 3 {
16+
t.Errorf("clouds len = %d, want 3", len(m.clouds))
17+
}
18+
if m.cursor != 0 {
19+
t.Errorf("cursor = %d, want 0", m.cursor)
20+
}
21+
if m.err != nil {
22+
t.Errorf("err = %v, want nil", m.err)
23+
}
24+
}
25+
26+
func TestNew_WithError(t *testing.T) {
27+
e := errors.New("no clouds.yaml")
28+
m := New(nil, e)
29+
30+
if m.err == nil {
31+
t.Error("expected error to be stored")
32+
}
33+
if m.err.Error() != "no clouds.yaml" {
34+
t.Errorf("err = %q, want %q", m.err.Error(), "no clouds.yaml")
35+
}
36+
}
37+
38+
func TestCursorDown(t *testing.T) {
39+
m := New([]string{"a", "b", "c"}, nil)
40+
41+
// Move down twice
42+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
43+
if m.cursor != 1 {
44+
t.Errorf("cursor = %d, want 1", m.cursor)
45+
}
46+
47+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
48+
if m.cursor != 2 {
49+
t.Errorf("cursor = %d, want 2", m.cursor)
50+
}
51+
52+
// Should not go past end
53+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
54+
if m.cursor != 2 {
55+
t.Errorf("cursor = %d, want 2 (clamped)", m.cursor)
56+
}
57+
}
58+
59+
func TestCursorUp(t *testing.T) {
60+
m := New([]string{"a", "b", "c"}, nil)
61+
62+
// Should not go below 0
63+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyUp}))
64+
if m.cursor != 0 {
65+
t.Errorf("cursor = %d, want 0 (clamped)", m.cursor)
66+
}
67+
68+
// Move down then up
69+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
70+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyUp}))
71+
if m.cursor != 0 {
72+
t.Errorf("cursor = %d, want 0", m.cursor)
73+
}
74+
}
75+
76+
func TestEnter_SelectsCloud(t *testing.T) {
77+
m := New([]string{"prod", "staging", "dev"}, nil)
78+
79+
// Move to "staging" (index 1)
80+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
81+
82+
var cmd tea.Cmd
83+
m, cmd = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
84+
85+
if cmd == nil {
86+
t.Fatal("expected cmd from Enter")
87+
}
88+
msg := cmd()
89+
sel, ok := msg.(shared.CloudSelectedMsg)
90+
if !ok {
91+
t.Fatalf("expected CloudSelectedMsg, got %T", msg)
92+
}
93+
if sel.CloudName != "staging" {
94+
t.Errorf("CloudName = %q, want %q", sel.CloudName, "staging")
95+
}
96+
}
97+
98+
func TestEnter_EmptyClouds(t *testing.T) {
99+
m := New([]string{}, nil)
100+
101+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
102+
if cmd != nil {
103+
t.Error("expected nil cmd for empty clouds list")
104+
}
105+
}
106+
107+
func TestQuit(t *testing.T) {
108+
m := New([]string{"a"}, nil)
109+
110+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'q', Text: "q"}))
111+
if cmd == nil {
112+
t.Fatal("expected quit cmd")
113+
}
114+
}
115+
116+
func TestWindowSize(t *testing.T) {
117+
m := New([]string{"a"}, nil)
118+
119+
m, _ = m.Update(tea.WindowSizeMsg{Width: 120, Height: 50})
120+
121+
if m.width != 120 {
122+
t.Errorf("width = %d, want 120", m.width)
123+
}
124+
if m.height != 50 {
125+
t.Errorf("height = %d, want 50", m.height)
126+
}
127+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package modal
2+
3+
import (
4+
"testing"
5+
6+
tea "charm.land/bubbletea/v2"
7+
)
8+
9+
func TestNewConfirm(t *testing.T) {
10+
m := NewConfirm("delete", "srv-123", "my-server")
11+
12+
if m.Action != "delete" {
13+
t.Errorf("Action = %q, want %q", m.Action, "delete")
14+
}
15+
if m.ServerID != "srv-123" {
16+
t.Errorf("ServerID = %q, want %q", m.ServerID, "srv-123")
17+
}
18+
if m.Name != "my-server" {
19+
t.Errorf("Name = %q, want %q", m.Name, "my-server")
20+
}
21+
if m.focused != 1 {
22+
t.Errorf("focused = %d, want 1 (cancel by default)", m.focused)
23+
}
24+
}
25+
26+
func TestNewBulkConfirm(t *testing.T) {
27+
servers := []ServerRef{
28+
{ID: "s1", Name: "web-1"},
29+
{ID: "s2", Name: "web-2"},
30+
{ID: "s3", Name: "web-3"},
31+
}
32+
m := NewBulkConfirm("reboot", servers)
33+
34+
if m.Action != "reboot" {
35+
t.Errorf("Action = %q, want %q", m.Action, "reboot")
36+
}
37+
if len(m.Servers) != 3 {
38+
t.Errorf("Servers len = %d, want 3", len(m.Servers))
39+
}
40+
if m.Name != "3 servers" {
41+
t.Errorf("Name = %q, want %q", m.Name, "3 servers")
42+
}
43+
if m.focused != 1 {
44+
t.Errorf("focused = %d, want 1 (cancel by default)", m.focused)
45+
}
46+
}
47+
48+
func TestConfirmKey(t *testing.T) {
49+
m := NewConfirm("delete", "srv-1", "web-1")
50+
51+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'y', Text: "y"}))
52+
if cmd == nil {
53+
t.Fatal("expected cmd from y key")
54+
}
55+
msg := cmd()
56+
action, ok := msg.(ConfirmAction)
57+
if !ok {
58+
t.Fatalf("expected ConfirmAction, got %T", msg)
59+
}
60+
if !action.Confirm {
61+
t.Error("expected Confirm = true")
62+
}
63+
if action.Action != "delete" {
64+
t.Errorf("Action = %q, want %q", action.Action, "delete")
65+
}
66+
if action.ServerID != "srv-1" {
67+
t.Errorf("ServerID = %q, want %q", action.ServerID, "srv-1")
68+
}
69+
}
70+
71+
func TestDenyKey(t *testing.T) {
72+
m := NewConfirm("delete", "srv-1", "web-1")
73+
74+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'n', Text: "n"}))
75+
if cmd == nil {
76+
t.Fatal("expected cmd from n key")
77+
}
78+
msg := cmd()
79+
action, ok := msg.(ConfirmAction)
80+
if !ok {
81+
t.Fatalf("expected ConfirmAction, got %T", msg)
82+
}
83+
if action.Confirm {
84+
t.Error("expected Confirm = false")
85+
}
86+
}
87+
88+
func TestBackKey(t *testing.T) {
89+
m := NewConfirm("delete", "srv-1", "web-1")
90+
91+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEscape}))
92+
if cmd == nil {
93+
t.Fatal("expected cmd from esc key")
94+
}
95+
msg := cmd()
96+
action, ok := msg.(ConfirmAction)
97+
if !ok {
98+
t.Fatalf("expected ConfirmAction, got %T", msg)
99+
}
100+
if action.Confirm {
101+
t.Error("expected Confirm = false on esc")
102+
}
103+
}
104+
105+
func TestToggleFocus(t *testing.T) {
106+
m := NewConfirm("delete", "srv-1", "web-1")
107+
// starts at focused=1 (cancel)
108+
109+
// Tab should toggle to 0
110+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyTab}))
111+
if m.focused != 0 {
112+
t.Errorf("focused = %d, want 0 after tab", m.focused)
113+
}
114+
115+
// Tab again should toggle back to 1
116+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyTab}))
117+
if m.focused != 1 {
118+
t.Errorf("focused = %d, want 1 after second tab", m.focused)
119+
}
120+
121+
// Arrow keys should also toggle
122+
m, _ = m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyLeft}))
123+
if m.focused != 0 {
124+
t.Errorf("focused = %d, want 0 after left arrow", m.focused)
125+
}
126+
}
127+
128+
func TestEnter_FocusedConfirm(t *testing.T) {
129+
m := NewConfirm("delete", "srv-1", "web-1")
130+
m.focused = 0
131+
132+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
133+
if cmd == nil {
134+
t.Fatal("expected cmd from enter on confirm")
135+
}
136+
msg := cmd()
137+
action, ok := msg.(ConfirmAction)
138+
if !ok {
139+
t.Fatalf("expected ConfirmAction, got %T", msg)
140+
}
141+
if !action.Confirm {
142+
t.Error("expected Confirm = true when focused on confirm button")
143+
}
144+
}
145+
146+
func TestEnter_FocusedCancel(t *testing.T) {
147+
m := NewConfirm("delete", "srv-1", "web-1")
148+
// focused defaults to 1 (cancel)
149+
150+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
151+
if cmd == nil {
152+
t.Fatal("expected cmd from enter on cancel")
153+
}
154+
msg := cmd()
155+
action, ok := msg.(ConfirmAction)
156+
if !ok {
157+
t.Fatalf("expected ConfirmAction, got %T", msg)
158+
}
159+
if action.Confirm {
160+
t.Error("expected Confirm = false when focused on cancel button")
161+
}
162+
}
163+
164+
func TestConfirm_WindowSize(t *testing.T) {
165+
m := NewConfirm("delete", "srv-1", "web-1")
166+
167+
m, _ = m.Update(tea.WindowSizeMsg{Width: 80, Height: 30})
168+
169+
if m.Width != 80 {
170+
t.Errorf("Width = %d, want 80", m.Width)
171+
}
172+
if m.Height != 30 {
173+
t.Errorf("Height = %d, want 30", m.Height)
174+
}
175+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package modal
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
tea "charm.land/bubbletea/v2"
8+
)
9+
10+
func TestNewError(t *testing.T) {
11+
m := NewError("deleting server", errors.New("connection refused"))
12+
13+
if m.Context != "deleting server" {
14+
t.Errorf("Context = %q, want %q", m.Context, "deleting server")
15+
}
16+
if m.Err != "connection refused" {
17+
t.Errorf("Err = %q, want %q", m.Err, "connection refused")
18+
}
19+
}
20+
21+
func TestEnterDismisses(t *testing.T) {
22+
m := NewError("test", errors.New("fail"))
23+
24+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
25+
if cmd == nil {
26+
t.Fatal("expected cmd from enter key")
27+
}
28+
msg := cmd()
29+
if _, ok := msg.(ErrorDismissedMsg); !ok {
30+
t.Fatalf("expected ErrorDismissedMsg, got %T", msg)
31+
}
32+
}
33+
34+
func TestEscDismisses(t *testing.T) {
35+
m := NewError("test", errors.New("fail"))
36+
37+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEscape}))
38+
if cmd == nil {
39+
t.Fatal("expected cmd from esc key")
40+
}
41+
msg := cmd()
42+
if _, ok := msg.(ErrorDismissedMsg); !ok {
43+
t.Fatalf("expected ErrorDismissedMsg, got %T", msg)
44+
}
45+
}
46+
47+
func TestOtherKeyIgnored(t *testing.T) {
48+
m := NewError("test", errors.New("fail"))
49+
50+
_, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'x', Text: "x"}))
51+
if cmd != nil {
52+
t.Error("expected nil cmd for unhandled key")
53+
}
54+
}
55+
56+
func TestError_WindowSize(t *testing.T) {
57+
m := NewError("test", errors.New("fail"))
58+
59+
m, _ = m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
60+
61+
if m.Width != 100 {
62+
t.Errorf("Width = %d, want 100", m.Width)
63+
}
64+
if m.Height != 40 {
65+
t.Errorf("Height = %d, want 40", m.Height)
66+
}
67+
}

0 commit comments

Comments
 (0)