Skip to content

Commit 8c515f7

Browse files
adding loading screen with spinner (#204)
1 parent 0b05b61 commit 8c515f7

File tree

2 files changed

+114
-80
lines changed

2 files changed

+114
-80
lines changed

cmd/card/imageviewer.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,53 @@
11
package card
22

33
import (
4+
"github.com/charmbracelet/bubbles/spinner"
45
tea "github.com/charmbracelet/bubbletea"
6+
"github.com/charmbracelet/lipgloss"
7+
"github.com/digitalghost-dev/poke-cli/styling"
58
)
69

710
type ImageModel struct {
8-
CardName string
9-
ImageURL string
10-
Error error
11+
CardName string
12+
ImageURL string
13+
Error error
14+
Loading bool
15+
Spinner spinner.Model
16+
ImageData string
17+
}
18+
19+
type imageReadyMsg struct {
20+
sixelData string
21+
}
22+
23+
// fetchImageCmd downloads and renders the image asynchronously
24+
func fetchImageCmd(imageURL string) tea.Cmd {
25+
return func() tea.Msg {
26+
sixelData, err := CardImage(imageURL)
27+
if err != nil {
28+
return imageReadyMsg{err.Error()}
29+
}
30+
return imageReadyMsg{sixelData: sixelData}
31+
}
1132
}
1233

1334
func (m ImageModel) Init() tea.Cmd {
14-
return nil
35+
return tea.Batch(
36+
m.Spinner.Tick,
37+
fetchImageCmd(m.ImageURL),
38+
)
1539
}
1640

1741
func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1842
switch msg := msg.(type) {
43+
case imageReadyMsg:
44+
m.Loading = false
45+
m.ImageData = msg.sixelData
46+
return m, nil
47+
case spinner.TickMsg:
48+
var cmd tea.Cmd
49+
m.Spinner, cmd = m.Spinner.Update(msg)
50+
return m, cmd
1951
case tea.KeyMsg:
2052
switch msg.String() {
2153
case "ctrl+c", "esc":
@@ -26,15 +58,23 @@ func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
2658
}
2759

2860
func (m ImageModel) View() string {
29-
return m.ImageURL
61+
if m.Loading {
62+
return lipgloss.NewStyle().Padding(2).Render(
63+
m.Spinner.View() + "Loading image for \n" + m.CardName,
64+
)
65+
}
66+
return m.ImageData
3067
}
3168

3269
func ImageRenderer(cardName string, imageURL string) ImageModel {
33-
imageData, err := CardImage(imageURL)
70+
s := spinner.New()
71+
s.Spinner = spinner.Dot
72+
s.Style = styling.Yellow
3473

3574
return ImageModel{
3675
CardName: cardName,
37-
ImageURL: imageData,
38-
Error: err,
76+
ImageURL: imageURL,
77+
Loading: true,
78+
Spinner: s,
3979
}
4080
}

cmd/card/imageviewer_test.go

Lines changed: 66 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
package card
22

33
import (
4-
"image"
5-
"image/color"
6-
"image/png"
7-
"net/http"
8-
"net/http/httptest"
4+
"strings"
95
"testing"
106

117
tea "github.com/charmbracelet/bubbletea"
128
)
139

1410
func TestImageModel_Init(t *testing.T) {
15-
model := ImageModel{
16-
CardName: "001/198 - Pineco",
17-
ImageURL: "test-sixel-data",
18-
}
11+
model := ImageRenderer("001/198 - Pineco", "http://example.com/image.png")
1912

2013
cmd := model.Init()
21-
if cmd != nil {
22-
t.Error("Init() should return nil")
14+
if cmd == nil {
15+
t.Error("Init() should return a command (batch of spinner tick + fetch)")
2316
}
2417
}
2518

@@ -53,7 +46,6 @@ func TestImageModel_Update_CtrlC(t *testing.T) {
5346
msg := tea.KeyMsg{Type: tea.KeyCtrlC}
5447
_, cmd := model.Update(msg)
5548

56-
// Should return quit command
5749
if cmd == nil {
5850
t.Error("Update with Ctrl+C should return tea.Quit command")
5951
}
@@ -73,107 +65,109 @@ func TestImageModel_Update_DifferentKey(t *testing.T) {
7365
}
7466
}
7567

76-
func TestImageModel_View(t *testing.T) {
77-
expectedURL := "test-sixel-data-123"
68+
func TestImageModel_View_Loading(t *testing.T) {
69+
model := ImageRenderer("001/198 - Pineco", "http://example.com/image.png")
70+
71+
result := model.View()
72+
73+
// When loading, should show spinner and card name
74+
if result == "" {
75+
t.Error("View() should not be empty when loading")
76+
}
77+
// Can't check exact spinner output as it's dynamic, but should contain card name
78+
if !strings.Contains(result, "001/198 - Pineco") {
79+
t.Error("View() should contain card name when loading")
80+
}
81+
}
82+
83+
func TestImageModel_View_Loaded(t *testing.T) {
84+
expectedData := "test-sixel-data-123"
7885
model := ImageModel{
79-
CardName: "001/198 - Pineco",
80-
ImageURL: expectedURL,
86+
CardName: "001/198 - Pineco",
87+
ImageURL: "http://example.com/image.png",
88+
Loading: false,
89+
ImageData: expectedData,
8190
}
8291

8392
result := model.View()
8493

85-
if result != expectedURL {
86-
t.Errorf("View() = %v, want %v", result, expectedURL)
94+
if result != expectedData {
95+
t.Errorf("View() = %v, want %v", result, expectedData)
8796
}
8897
}
8998

9099
func TestImageModel_View_Empty(t *testing.T) {
91100
model := ImageModel{
92-
CardName: "001/198 - Pineco",
93-
ImageURL: "",
101+
CardName: "001/198 - Pineco",
102+
ImageURL: "",
103+
Loading: false,
104+
ImageData: "",
94105
}
95106

96107
result := model.View()
97108

98109
if result != "" {
99-
t.Errorf("View() with empty ImageURL should return empty string, got %v", result)
110+
t.Errorf("View() with empty ImageData should return empty string, got %v", result)
100111
}
101112
}
102113

103-
func TestImageRenderer_Success(t *testing.T) {
104-
// Create a test HTTP server that serves a valid PNG image
105-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106-
img := image.NewRGBA(image.Rect(0, 0, 10, 10))
107-
blue := color.RGBA{R: 0, G: 0, B: 255, A: 255}
108-
for y := 0; y < 10; y++ {
109-
for x := 0; x < 10; x++ {
110-
img.Set(x, y, blue)
111-
}
112-
}
113-
114-
w.Header().Set("Content-Type", "image/png")
115-
w.WriteHeader(http.StatusOK)
116-
_ = png.Encode(w, img)
117-
}))
118-
defer server.Close()
119-
120-
model := ImageRenderer("Pikachu", server.URL)
114+
func TestImageRenderer_InitializesCorrectly(t *testing.T) {
115+
testURL := "http://example.com/pikachu.png"
116+
model := ImageRenderer("Pikachu", testURL)
121117

122118
if model.CardName != "Pikachu" {
123119
t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Pikachu")
124120
}
125121

126-
if model.Error != nil {
127-
t.Errorf("ImageRenderer() Error should be nil on success, got %v", model.Error)
122+
if model.ImageURL != testURL {
123+
t.Errorf("ImageRenderer() ImageURL = %v, want %v", model.ImageURL, testURL)
128124
}
129125

130-
if model.ImageURL == "" {
131-
t.Error("ImageRenderer() ImageURL should not be empty on success")
126+
if !model.Loading {
127+
t.Error("ImageRenderer() should initialize with Loading = true")
128+
}
129+
130+
if model.ImageData != "" {
131+
t.Error("ImageRenderer() should initialize with empty ImageData")
132132
}
133133
}
134134

135-
func TestImageRenderer_Error(t *testing.T) {
136-
// Create a test HTTP server that returns an error
137-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138-
w.WriteHeader(http.StatusNotFound)
139-
}))
140-
defer server.Close()
135+
func TestImageModel_Update_ImageReady(t *testing.T) {
136+
model := ImageRenderer("Charizard", "http://example.com/charizard.png")
141137

142-
model := ImageRenderer("Charizard", server.URL)
138+
msg := imageReadyMsg{sixelData: "test-sixel-data-456"}
139+
newModel, cmd := model.Update(msg)
143140

144-
if model.CardName != "Charizard" {
145-
t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Charizard")
141+
if cmd != nil {
142+
t.Error("Update with imageReadyMsg should return nil command")
146143
}
147144

148-
if model.Error == nil {
149-
t.Error("ImageRenderer() Error should not be nil when image fetch fails")
145+
updatedModel := newModel.(ImageModel)
146+
if updatedModel.Loading {
147+
t.Error(`Update with imageReadyMsg should set Loading to false`)
150148
}
151149

152-
if model.ImageURL != "" {
153-
t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL)
150+
if updatedModel.ImageData != "test-sixel-data-456" {
151+
t.Errorf("Update with imageReadyMsg should set ImageData, got %v", updatedModel.ImageData)
154152
}
155153
}
156154

157-
func TestImageRenderer_InvalidImage(t *testing.T) {
158-
// Create a test HTTP server that returns invalid image data
159-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
160-
w.Header().Set("Content-Type", "image/png")
161-
w.WriteHeader(http.StatusOK)
162-
_, _ = w.Write([]byte("not a valid image"))
163-
}))
164-
defer server.Close()
155+
func TestImageModel_Update_SpinnerTick(t *testing.T) {
156+
model := ImageRenderer("Mewtwo", "http://example.com/mewtwo.png")
165157

166-
model := ImageRenderer("Mewtwo", server.URL)
158+
// Create a spinner tick message
159+
msg := model.Spinner.Tick()
167160

168-
if model.CardName != "Mewtwo" {
169-
t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Mewtwo")
170-
}
161+
// Update should handle spinner ticks
162+
newModel, cmd := model.Update(msg)
171163

172-
if model.Error == nil {
173-
t.Error("ImageRenderer() Error should not be nil when image decoding fails")
164+
// Should return a spinner command
165+
if cmd == nil {
166+
t.Error("Update with spinner.TickMsg should return a command")
174167
}
175168

176-
if model.ImageURL != "" {
177-
t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL)
169+
// Model should still be ImageModel
170+
if _, ok := newModel.(ImageModel); !ok {
171+
t.Error("Update should return ImageModel")
178172
}
179173
}

0 commit comments

Comments
 (0)