cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm
leaflet
readability
golang
1package ui
2
3import (
4 "bytes"
5 "os"
6 "regexp"
7 "strings"
8 "testing"
9
10 "github.com/charmbracelet/lipgloss"
11 "github.com/muesli/termenv"
12)
13
14func stripAnsi(str string) string {
15 return regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`).ReplaceAllString(str, "")
16}
17
18func withColor(t *testing.T, fn func(r *lipgloss.Renderer)) {
19 t.Helper()
20
21 var buf bytes.Buffer
22 r := lipgloss.NewRenderer(&buf)
23 r.SetColorProfile(termenv.TrueColor)
24 lipgloss.SetDefaultRenderer(r)
25
26 fn(r)
27
28 lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(os.Stdout))
29}
30
31func TestUI(t *testing.T) {
32 t.Run("Hex", func(t *testing.T) {
33 tests := []struct {
34 key Key
35 expected string
36 }{
37 {Cumin, "#BF976F"},
38 {Tang, "#FF985A"},
39 {Yam, "#FFB587"},
40 {Paprika, "#D36C64"},
41 {Bengal, "#FF6E63"},
42 {Uni, "#FF937D"},
43 {Sriracha, "#EB4268"},
44 {Coral, "#FF577D"},
45 {Salmon, "#FF7F90"},
46 {Chili, "#E23080"},
47 {Cherry, "#FF388B"},
48 {Tuna, "#FF6DAA"},
49 {Macaron, "#E940B0"},
50 {Pony, "#FF4FBF"},
51 {Cheeky, "#FF79D0"},
52 {Flamingo, "#F947E3"},
53 {Dolly, "#FF60FF"},
54 {Blush, "#FF84FF"},
55 {Urchin, "#C337E0"},
56 {Mochi, "#EB5DFF"},
57 {Lilac, "#F379FF"},
58 {Prince, "#9C35E1"},
59 {Violet, "#C259FF"},
60 {Mauve, "#D46EFF"},
61 {Grape, "#7134DD"},
62 {Plum, "#9953FF"},
63 {Orchid, "#AD6EFF"},
64 {Jelly, "#4A30D9"},
65 {Charple, "#6B50FF"},
66 {Hazy, "#8B75FF"},
67 {Ox, "#3331B2"},
68 {Sapphire, "#4949FF"},
69 {Guppy, "#7272FF"},
70 {Oceania, "#2B55B3"},
71 {Thunder, "#4776FF"},
72 {Anchovy, "#719AFC"},
73 {Damson, "#007AB8"},
74 {Malibu, "#00A4FF"},
75 {Sardine, "#4FBEFE"},
76 {Zinc, "#10B1AE"},
77 {Turtle, "#0ADCD9"},
78 {Lichen, "#5CDFEA"},
79 {Guac, "#12C78F"},
80 {Julep, "#00FFB2"},
81 {Bok, "#68FFD6"},
82 {Mustard, "#F5EF34"},
83 {Citron, "#E8FF27"},
84 {Zest, "#E8FE96"},
85 {Pepper, "#201F26"},
86 {BBQ, "#2d2c35"},
87 {Charcoal, "#3A3943"},
88 {Iron, "#4D4C57"},
89 {Oyster, "#605F6B"},
90 {Squid, "#858392"},
91 {Smoke, "#BFBCC8"},
92 {Ash, "#DFDBDD"},
93 {Salt, "#F1EFEF"},
94 {Butter, "#FFFAF1"},
95 // Diffs: additions
96 {Pickle, "#00A475"},
97 {Gator, "#18463D"},
98 {Spinach, "#1C3634"},
99 {Pom, "#AB2454"},
100 {Steak, "#582238"},
101 {Toast, "#412130"},
102 {NeueGuac, "#00b875"},
103 {NeueZinc, "#0e9996"},
104 {Key(-1), ""},
105 }
106
107 for _, tt := range tests {
108 t.Run(tt.key.String(), func(t *testing.T) {
109 if hex := tt.key.Hex(); hex != tt.expected {
110 t.Errorf("expected hex %q, got %q", tt.expected, hex)
111 }
112 })
113 }
114 })
115
116 t.Run("String", func(t *testing.T) {
117 tests := []struct {
118 key Key
119 expected string
120 }{
121 {Cumin, "Cumin"},
122 {Tang, "Tang"},
123 {Yam, "Yam"},
124 {Paprika, "Paprika"},
125 {Bengal, "Bengal"},
126 {Uni, "Uni"},
127 {Sriracha, "Sriracha"},
128 {Coral, "Coral"},
129 {Salmon, "Salmon"},
130 {Chili, "Chili"},
131 {Cherry, "Cherry"},
132 {Tuna, "Tuna"},
133 {Macaron, "Macaron"},
134 {Pony, "Pony"},
135 {Cheeky, "Cheeky"},
136 {Flamingo, "Flamingo"},
137 {Dolly, "Dolly"},
138 {Blush, "Blush"},
139 {Urchin, "Urchin"},
140 {Mochi, "Mochi"},
141 {Lilac, "Lilac"},
142 {Prince, "Prince"},
143 {Violet, "Violet"},
144 {Mauve, "Mauve"},
145 {Grape, "Grape"},
146 {Plum, "Plum"},
147 {Orchid, "Orchid"},
148 {Jelly, "Jelly"},
149 {Charple, "Charple"},
150 {Hazy, "Hazy"},
151 {Ox, "Ox"},
152 {Sapphire, "Sapphire"},
153 {Guppy, "Guppy"},
154 {Oceania, "Oceania"},
155 {Thunder, "Thunder"},
156 {Anchovy, "Anchovy"},
157 {Damson, "Damson"},
158 {Malibu, "Malibu"},
159 {Sardine, "Sardine"},
160 {Zinc, "Zinc"},
161 {Turtle, "Turtle"},
162 {Lichen, "Lichen"},
163 {Guac, "Guac"},
164 {Julep, "Julep"},
165 {Bok, "Bok"},
166 {Mustard, "Mustard"},
167 {Citron, "Citron"},
168 {Zest, "Zest"},
169 {Pepper, "Pepper"},
170 {BBQ, "BBQ"},
171 {Charcoal, "Charcoal"},
172 {Iron, "Iron"},
173 {Oyster, "Oyster"},
174 {Squid, "Squid"},
175 {Smoke, "Smoke"},
176 {Ash, "Ash"},
177 {Salt, "Salt"},
178 {Butter, "Butter"},
179 {Pickle, "Pickle"},
180 {Gator, "Gator"},
181 {Spinach, "Spinach"},
182 {Pom, "Pom"},
183 {Steak, "Steak"},
184 {Toast, "Toast"},
185 {NeueGuac, "NeueGuac"},
186 {NeueZinc, "NeueZinc"},
187 {Key(-1), "Key(-1)"},
188 }
189
190 for _, tt := range tests {
191 t.Run(tt.expected, func(t *testing.T) {
192 if str := tt.key.String(); str != tt.expected {
193 t.Errorf("expected string %q, got %q", tt.expected, str)
194 }
195 })
196 }
197 })
198
199 t.Run("RGBA", func(t *testing.T) {
200 t.Run("valid key", func(t *testing.T) {
201 r, g, b, a := Cumin.RGBA()
202 if a == 0 {
203 t.Error("Alpha should not be zero for a valid color")
204 }
205 if !(r > 0 && g > 0 && b > 0) {
206 t.Error("RGB components should be greater than zero for Cumin")
207 }
208 })
209
210 t.Run("invalid key", func(t *testing.T) {
211 defer func() {
212 if r := recover(); r == nil {
213 t.Errorf("The code did not panic")
214 }
215 }()
216 Key(-1).RGBA()
217 })
218 })
219
220 t.Run("Color Categories", func(t *testing.T) {
221 if !Guac.IsPrimary() {
222 t.Error("Guac should be a primary color")
223 }
224 if Malibu.IsPrimary() {
225 t.Error("Malibu should not be a primary color")
226 }
227 if !Malibu.IsSecondary() {
228 t.Error("Malibu should be a secondary color")
229 }
230 if Guac.IsSecondary() {
231 t.Error("Guac should not be a secondary color")
232 }
233 if !Violet.IsTertiary() {
234 t.Error("Violet should be a tertiary color")
235 }
236 if Guac.IsTertiary() {
237 t.Error("Guac should not be a tertiary color")
238 }
239 })
240
241 t.Run("New Palette", func(t *testing.T) {
242 t.Run("dark mode", func(t *testing.T) {
243 p := NewPalette(true)
244 if p == nil {
245 t.Fatal("NewPalette(true) returned nil")
246 }
247 r, g, b, a := p.scheme.Base.RGBA()
248 pr, pg, pb, pa := Pepper.RGBA()
249 if r != pr || g != pg || b != pb || a != pa {
250 t.Errorf("dark mode base color mismatch: expected Pepper, got %v", p.scheme.Base)
251 }
252 })
253
254 t.Run("light mode", func(t *testing.T) {
255 p := NewPalette(false)
256 if p == nil {
257 t.Fatal("NewPalette(false) returned nil")
258 }
259 r, g, b, a := p.scheme.Base.RGBA()
260 sr, sg, sb, sa := Salt.RGBA()
261 if r != sr || g != sg || b != sb || a != sa {
262 t.Errorf("light mode base color mismatch: expected Salt, got %v", p.scheme.Base)
263 }
264 })
265 })
266
267 t.Run("Logo", func(t *testing.T) {
268 logos := []struct {
269 logo Logo
270 name string
271 contains string
272 }{
273 {Colossal, "Collosal", "888b 888"},
274 {Georgia, "Georgia", "`7MN. `7MF'"},
275 {Alligator, "Alligator", ":::: ::: ::::::::"},
276 {ANSI, "ANSI", "███ ██"},
277 {ANSIShadow, "ANSIShadow", "███╗ ██╗"},
278 }
279 for _, l := range logos {
280 t.Run(l.name, func(t *testing.T) {
281 s := l.logo.String()
282 if s == "" {
283 t.Error("logo string should not be empty")
284 }
285 if !strings.Contains(s, l.contains) {
286 t.Errorf("logo string does not contain expected content: %q", l.contains)
287 }
288 })
289 }
290
291 t.Run("Colored", func(t *testing.T) {
292 withColor(t, func(r *lipgloss.Renderer) {
293 logo := Georgia
294 plain := logo.String()
295 colored := logo.Colored()
296
297 if colored == "" {
298 t.Fatal("colored logo is empty")
299 }
300 if plain == colored {
301 t.Error("Colored logo should be different from plain")
302 }
303 if !strings.Contains(colored, "\u001b[") {
304 t.Error("Colored logo should contain ANSI escape codes")
305 }
306 })
307 })
308
309 t.Run("Colored in Viewport", func(t *testing.T) {
310 withColor(t, func(r *lipgloss.Renderer) {
311 logo := Colossal
312 viewport := logo.ColoredInViewport(r)
313
314 if viewport == "" {
315 t.Fatal("viewport is empty")
316 }
317
318 cleanedViewport := stripAnsi(viewport)
319 if !strings.Contains(cleanedViewport, "888") {
320 t.Error("Viewport should contain parts of the logo")
321 }
322
323 borderChars := []string{"╭", "╮", "╯", "╰"}
324 for _, char := range borderChars {
325 if !strings.Contains(viewport, char) {
326 t.Errorf("Viewport should contain rounded border character %s", char)
327 }
328 }
329 })
330 })
331 })
332
333}