1package view
2
3import (
4 "fmt"
5 "os"
6 "regexp"
7 "strings"
8 "testing"
9
10 "charm.land/lipgloss/v2"
11)
12
13// clearAllTerminalEnv clears all environment variables that could indicate terminal capabilities
14func clearAllTerminalEnv() {
15 // Clear hyperlink support indicators
16 os.Unsetenv("VTE_VERSION")
17 os.Unsetenv("KITTY_WINDOW_ID")
18 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
19 os.Unsetenv("WEZTERM_EXECUTABLE")
20 os.Unsetenv("WEZTERM_CONFIG_FILE")
21 os.Unsetenv("ITERM_SESSION_ID")
22 os.Unsetenv("ITERM_PROFILE")
23 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
24 os.Unsetenv("WARP_COMBINED_PROMPT_COMMAND_FINISHED")
25 os.Unsetenv("KONSOLE_DBUS_SESSION")
26 os.Unsetenv("KONSOLE_VERSION")
27
28 // Set basic terminal that doesn't support anything special
29 os.Setenv("TERM", "xterm")
30 os.Setenv("TERM_PROGRAM", "basic")
31}
32
33func TestDecodeQuotedPrintable(t *testing.T) {
34 testCases := []struct {
35 name string
36 input string
37 expected string
38 }{
39 {
40 name: "Simple case",
41 input: "Hello=2C world=21",
42 expected: "Hello, world!",
43 },
44 {
45 name: "With soft line break",
46 input: "This is a long line that gets wrapped=\r\n and continues here.",
47 expected: "This is a long line that gets wrapped and continues here.",
48 },
49 {
50 name: "No encoding",
51 input: "Just a plain string.",
52 expected: "Just a plain string.",
53 },
54 }
55
56 for _, tc := range testCases {
57 t.Run(tc.name, func(t *testing.T) {
58 decoded, err := decodeQuotedPrintable(tc.input)
59 if err != nil {
60 t.Fatalf("decodeQuotedPrintable() failed: %v", err)
61 }
62 if decoded != tc.expected {
63 t.Errorf("Expected %q, got %q", tc.expected, decoded)
64 }
65 })
66 }
67}
68
69func TestMarkdownToHTML(t *testing.T) {
70 testCases := []struct {
71 name string
72 input string
73 expected string
74 }{
75 {
76 name: "Heading",
77 input: "# Hello",
78 expected: "<h1>Hello</h1>",
79 },
80 {
81 name: "Bold",
82 input: "**bold text**",
83 expected: "<p><strong>bold text</strong></p>",
84 },
85 {
86 name: "Link",
87 input: "[link](http://example.com)",
88 expected: `<p><a href="http://example.com">link</a></p>`,
89 },
90 }
91
92 for _, tc := range testCases {
93 t.Run(tc.name, func(t *testing.T) {
94 html := markdownToHTML([]byte(tc.input))
95 // Trim newlines for consistent comparison
96 if strings.TrimSpace(string(html)) != tc.expected {
97 t.Errorf("Expected %s, got %s", tc.expected, html)
98 }
99 })
100 }
101}
102
103func TestGhosttySupported(t *testing.T) {
104 // Save original environment variables
105 origTerm := os.Getenv("TERM")
106 origTermProgram := os.Getenv("TERM_PROGRAM")
107 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
108
109 // Restore environment variables after test
110 defer func() {
111 os.Setenv("TERM", origTerm)
112 os.Setenv("TERM_PROGRAM", origTermProgram)
113 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
114 }()
115
116 testCases := []struct {
117 name string
118 term string
119 termProgram string
120 ghosttyResourcesDir string
121 expected bool
122 }{
123 {
124 name: "No Ghostty environment variables",
125 term: "xterm",
126 termProgram: "",
127 ghosttyResourcesDir: "",
128 expected: false,
129 },
130 {
131 name: "TERM contains ghostty",
132 term: "xterm-ghostty",
133 termProgram: "",
134 ghosttyResourcesDir: "",
135 expected: true,
136 },
137 {
138 name: "TERM_PROGRAM is ghostty",
139 term: "xterm",
140 termProgram: "ghostty",
141 ghosttyResourcesDir: "",
142 expected: true,
143 },
144 {
145 name: "GHOSTTY_RESOURCES_DIR is set",
146 term: "xterm",
147 termProgram: "",
148 ghosttyResourcesDir: "/usr/share/ghostty",
149 expected: true,
150 },
151 {
152 name: "Multiple Ghostty indicators",
153 term: "ghostty",
154 termProgram: "ghostty",
155 ghosttyResourcesDir: "/usr/share/ghostty",
156 expected: true,
157 },
158 }
159
160 for _, tc := range testCases {
161 t.Run(tc.name, func(t *testing.T) {
162 os.Setenv("TERM", tc.term)
163 os.Setenv("TERM_PROGRAM", tc.termProgram)
164 os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResourcesDir)
165
166 result := ghosttySupported()
167 if result != tc.expected {
168 t.Errorf("Expected %t, got %t", tc.expected, result)
169 }
170 })
171 }
172}
173
174func TestImageProtocolSupported(t *testing.T) {
175 // Save original environment variables
176 origTerm := os.Getenv("TERM")
177 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
178 origTermProgram := os.Getenv("TERM_PROGRAM")
179 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
180 origItermlSession := os.Getenv("ITERM_SESSION_ID")
181 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
182 origWarpLocal := os.Getenv("WARP_IS_LOCAL_SHELL_SESSION")
183 origKonsoleDBus := os.Getenv("KONSOLE_DBUS_SESSION")
184
185 // Restore environment variables after test
186 defer func() {
187 os.Setenv("TERM", origTerm)
188 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
189 os.Setenv("TERM_PROGRAM", origTermProgram)
190 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
191 os.Setenv("ITERM_SESSION_ID", origItermlSession)
192 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
193 os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", origWarpLocal)
194 os.Setenv("KONSOLE_DBUS_SESSION", origKonsoleDBus)
195 }()
196
197 testCases := []struct {
198 name string
199 setupEnv func()
200 clearAllEnv func()
201 expected bool
202 }{
203 {
204 name: "No supported terminals",
205 setupEnv: func() {
206 os.Setenv("TERM", "xterm")
207 os.Setenv("TERM_PROGRAM", "basic")
208 },
209 clearAllEnv: func() {
210 os.Unsetenv("KITTY_WINDOW_ID")
211 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
212 os.Unsetenv("ITERM_SESSION_ID")
213 os.Unsetenv("WEZTERM_EXECUTABLE")
214 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
215 os.Unsetenv("KONSOLE_DBUS_SESSION")
216 },
217 expected: false,
218 },
219 {
220 name: "Kitty supported via TERM",
221 setupEnv: func() {
222 os.Setenv("TERM", "xterm-kitty")
223 },
224 clearAllEnv: func() {
225 os.Unsetenv("KITTY_WINDOW_ID")
226 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
227 os.Unsetenv("ITERM_SESSION_ID")
228 os.Unsetenv("WEZTERM_EXECUTABLE")
229 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
230 os.Unsetenv("KONSOLE_DBUS_SESSION")
231 },
232 expected: true,
233 },
234 {
235 name: "Kitty supported via KITTY_WINDOW_ID",
236 setupEnv: func() {
237 os.Setenv("TERM", "xterm")
238 os.Setenv("KITTY_WINDOW_ID", "1")
239 },
240 clearAllEnv: func() {
241 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
242 os.Unsetenv("ITERM_SESSION_ID")
243 os.Unsetenv("WEZTERM_EXECUTABLE")
244 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
245 os.Unsetenv("KONSOLE_DBUS_SESSION")
246 },
247 expected: true,
248 },
249 {
250 name: "Ghostty supported via TERM_PROGRAM",
251 setupEnv: func() {
252 os.Setenv("TERM", "xterm")
253 os.Setenv("TERM_PROGRAM", "ghostty")
254 },
255 clearAllEnv: func() {
256 os.Unsetenv("KITTY_WINDOW_ID")
257 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
258 os.Unsetenv("ITERM_SESSION_ID")
259 os.Unsetenv("WEZTERM_EXECUTABLE")
260 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
261 os.Unsetenv("KONSOLE_DBUS_SESSION")
262 },
263 expected: true,
264 },
265 {
266 name: "iTerm2 supported via TERM_PROGRAM",
267 setupEnv: func() {
268 os.Setenv("TERM", "xterm")
269 os.Setenv("TERM_PROGRAM", "iterm.app")
270 },
271 clearAllEnv: func() {
272 os.Unsetenv("KITTY_WINDOW_ID")
273 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
274 os.Unsetenv("ITERM_SESSION_ID")
275 os.Unsetenv("WEZTERM_EXECUTABLE")
276 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
277 os.Unsetenv("KONSOLE_DBUS_SESSION")
278 },
279 expected: true,
280 },
281 {
282 name: "WezTerm supported via WEZTERM_EXECUTABLE",
283 setupEnv: func() {
284 os.Setenv("TERM", "xterm")
285 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
286 },
287 clearAllEnv: func() {
288 os.Unsetenv("KITTY_WINDOW_ID")
289 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
290 os.Unsetenv("ITERM_SESSION_ID")
291 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
292 os.Unsetenv("KONSOLE_DBUS_SESSION")
293 },
294 expected: true,
295 },
296 {
297 name: "Warp supported via WARP_IS_LOCAL_SHELL_SESSION",
298 setupEnv: func() {
299 os.Setenv("TERM", "xterm")
300 os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", "1")
301 },
302 clearAllEnv: func() {
303 os.Unsetenv("KITTY_WINDOW_ID")
304 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
305 os.Unsetenv("ITERM_SESSION_ID")
306 os.Unsetenv("WEZTERM_EXECUTABLE")
307 os.Unsetenv("KONSOLE_DBUS_SESSION")
308 },
309 expected: true,
310 },
311 {
312 name: "Konsole supported via KONSOLE_DBUS_SESSION",
313 setupEnv: func() {
314 os.Setenv("TERM", "xterm")
315 os.Setenv("KONSOLE_DBUS_SESSION", "/Sessions/1")
316 },
317 clearAllEnv: func() {
318 os.Unsetenv("KITTY_WINDOW_ID")
319 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
320 os.Unsetenv("ITERM_SESSION_ID")
321 os.Unsetenv("WEZTERM_EXECUTABLE")
322 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
323 },
324 expected: true,
325 },
326 }
327
328 for _, tc := range testCases {
329 t.Run(tc.name, func(t *testing.T) {
330 tc.clearAllEnv()
331 tc.setupEnv()
332
333 result := imageProtocolSupported()
334 if result != tc.expected {
335 t.Errorf("Expected %t, got %t", tc.expected, result)
336 }
337 })
338 }
339}
340
341func TestHyperlinkSupported(t *testing.T) {
342 // Save original environment variables
343 origTerm := os.Getenv("TERM")
344 origTermProgram := os.Getenv("TERM_PROGRAM")
345 origVTEVersion := os.Getenv("VTE_VERSION")
346 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
347 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
348 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
349
350 // Restore environment variables after test
351 defer func() {
352 os.Setenv("TERM", origTerm)
353 os.Setenv("TERM_PROGRAM", origTermProgram)
354 os.Setenv("VTE_VERSION", origVTEVersion)
355 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
356 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
357 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
358 }()
359
360 testCases := []struct {
361 name string
362 setupEnv func()
363 clearAllEnv func()
364 expected bool
365 }{
366 {
367 name: "No hyperlink support",
368 setupEnv: func() {
369 os.Setenv("TERM", "xterm")
370 os.Setenv("TERM_PROGRAM", "basic")
371 },
372 clearAllEnv: func() {
373 os.Unsetenv("VTE_VERSION")
374 os.Unsetenv("KITTY_WINDOW_ID")
375 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
376 os.Unsetenv("WEZTERM_EXECUTABLE")
377 },
378 expected: false,
379 },
380 {
381 name: "Kitty hyperlink support via TERM",
382 setupEnv: func() {
383 os.Setenv("TERM", "xterm-kitty")
384 },
385 clearAllEnv: func() {
386 os.Unsetenv("VTE_VERSION")
387 os.Unsetenv("KITTY_WINDOW_ID")
388 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
389 os.Unsetenv("WEZTERM_EXECUTABLE")
390 },
391 expected: true,
392 },
393 {
394 name: "VTE-based terminal hyperlink support",
395 setupEnv: func() {
396 os.Setenv("TERM", "xterm")
397 os.Setenv("VTE_VERSION", "0.60.3")
398 },
399 clearAllEnv: func() {
400 os.Unsetenv("KITTY_WINDOW_ID")
401 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
402 os.Unsetenv("WEZTERM_EXECUTABLE")
403 },
404 expected: true,
405 },
406 {
407 name: "iTerm2 hyperlink support",
408 setupEnv: func() {
409 os.Setenv("TERM", "xterm")
410 os.Setenv("TERM_PROGRAM", "iterm.app")
411 },
412 clearAllEnv: func() {
413 os.Unsetenv("VTE_VERSION")
414 os.Unsetenv("KITTY_WINDOW_ID")
415 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
416 os.Unsetenv("WEZTERM_EXECUTABLE")
417 },
418 expected: true,
419 },
420 {
421 name: "WezTerm hyperlink support",
422 setupEnv: func() {
423 os.Setenv("TERM", "xterm")
424 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
425 },
426 clearAllEnv: func() {
427 os.Unsetenv("VTE_VERSION")
428 os.Unsetenv("KITTY_WINDOW_ID")
429 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
430 },
431 expected: true,
432 },
433 }
434
435 for _, tc := range testCases {
436 t.Run(tc.name, func(t *testing.T) {
437 tc.clearAllEnv()
438 tc.setupEnv()
439
440 result := hyperlinkSupported()
441 if result != tc.expected {
442 t.Errorf("Expected %t, got %t", tc.expected, result)
443 }
444 })
445 }
446}
447
448func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
449 // Save original environment variables
450 origTerm := os.Getenv("TERM")
451 origTermProgram := os.Getenv("TERM_PROGRAM")
452 origVTEVersion := os.Getenv("VTE_VERSION")
453 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
454
455 // Restore environment variables after test
456 defer func() {
457 os.Setenv("TERM", origTerm)
458 os.Setenv("TERM_PROGRAM", origTermProgram)
459 os.Setenv("VTE_VERSION", origVTEVersion)
460 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
461 }()
462
463 h1Style := lipgloss.NewStyle().SetString("H1")
464 h2Style := lipgloss.NewStyle().SetString("H2")
465 bodyStyle := lipgloss.NewStyle().SetString("BODY")
466
467 testCases := []struct {
468 name string
469 setupHyperlinks func()
470 input string
471 expectedContains string
472 expectedNotContains string
473 }{
474 {
475 name: "Link with hyperlink support",
476 setupHyperlinks: func() {
477 os.Setenv("TERM", "xterm-kitty")
478 os.Unsetenv("VTE_VERSION")
479 os.Unsetenv("KITTY_WINDOW_ID")
480 },
481 input: `<a href="http://example.com">Click here</a>`,
482 expectedContains: "Click here",
483 expectedNotContains: "<http://example.com>",
484 },
485 {
486 name: "Link without hyperlink support",
487 setupHyperlinks: func() {
488 clearAllTerminalEnv()
489 },
490 input: `<a href="http://example.com">Click here</a>`,
491 expectedContains: "Click here <http://example.com>",
492 },
493 {
494 name: "Image link with hyperlink support",
495 setupHyperlinks: func() {
496 os.Setenv("TERM", "xterm")
497 os.Setenv("VTE_VERSION", "0.60.3")
498 os.Unsetenv("KITTY_WINDOW_ID")
499 },
500 input: `<img src="http://example.com/img.png" alt="alt text">`,
501 expectedContains: "[Click here to view image: alt text]",
502 expectedNotContains: "<http://example.com/img.png>",
503 },
504 {
505 name: "Image link without hyperlink support",
506 setupHyperlinks: func() {
507 clearAllTerminalEnv()
508 },
509 input: `<img src="http://example.com/img.png" alt="alt text">`,
510 expectedContains: "[Image: alt text, http://example.com/img.png]",
511 },
512 }
513
514 // Regex to strip out ANSI SGR escape codes (e.g. \x1b[38;2;...m)
515 ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
516
517 for _, tc := range testCases {
518 t.Run(tc.name, func(t *testing.T) {
519 tc.setupHyperlinks()
520
521 processed, _, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle, false)
522 if err != nil {
523 t.Fatalf("ProcessBody() failed: %v", err)
524 }
525
526 cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "")
527
528 if !strings.Contains(cleanProcessed, tc.expectedContains) {
529 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expectedContains)
530 }
531
532 if tc.expectedNotContains != "" && strings.Contains(cleanProcessed, tc.expectedNotContains) {
533 t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", cleanProcessed, tc.expectedNotContains)
534 }
535 })
536 }
537}
538
539func TestProcessBodyWithImageProtocol(t *testing.T) {
540 // Save original environment variables
541 origTerm := os.Getenv("TERM")
542 origTermProgram := os.Getenv("TERM_PROGRAM")
543 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
544 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
545 origItermlSession := os.Getenv("ITERM_SESSION_ID")
546 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
547
548 // Restore environment variables after test
549 defer func() {
550 os.Setenv("TERM", origTerm)
551 os.Setenv("TERM_PROGRAM", origTermProgram)
552 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
553 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
554 os.Setenv("ITERM_SESSION_ID", origItermlSession)
555 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
556 }()
557
558 h1Style := lipgloss.NewStyle().SetString("H1")
559 h2Style := lipgloss.NewStyle().SetString("H2")
560 bodyStyle := lipgloss.NewStyle().SetString("BODY")
561
562 // Create a simple base64 PNG image (1x1 pixel white PNG)
563 testBase64PNG := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
564
565 testCases := []struct {
566 name string
567 setupImageProtocol func()
568 clearAllImageEnv func()
569 input string
570 expectedContains string
571 expectedNotContains string
572 expectPlacements bool
573 }{
574 {
575 name: "Data URI image with Kitty support returns placement",
576 setupImageProtocol: func() {
577 os.Setenv("TERM", "xterm-kitty")
578 },
579 clearAllImageEnv: func() {
580 os.Unsetenv("KITTY_WINDOW_ID")
581 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
582 os.Unsetenv("ITERM_SESSION_ID")
583 os.Unsetenv("WEZTERM_EXECUTABLE")
584 },
585 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
586 expectedNotContains: "[Image: test image,",
587 expectPlacements: true,
588 },
589 {
590 name: "Data URI image with iTerm2 support returns placement",
591 setupImageProtocol: func() {
592 os.Setenv("TERM", "xterm")
593 os.Setenv("TERM_PROGRAM", "iterm.app")
594 },
595 clearAllImageEnv: func() {
596 os.Unsetenv("KITTY_WINDOW_ID")
597 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
598 os.Unsetenv("ITERM_SESSION_ID")
599 os.Unsetenv("WEZTERM_EXECUTABLE")
600 },
601 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
602 expectedNotContains: "[Image: test image,",
603 expectPlacements: true,
604 },
605 {
606 name: "Data URI image without protocol support",
607 setupImageProtocol: func() {
608 clearAllTerminalEnv()
609 },
610 clearAllImageEnv: func() {
611 // This is handled by clearAllTerminalEnv now
612 },
613 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
614 expectedContains: "[Image: test image,",
615 },
616 {
617 name: "Remote image with WezTerm support (has hyperlink support)",
618 setupImageProtocol: func() {
619 clearAllTerminalEnv()
620 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
621 },
622 clearAllImageEnv: func() {
623 // This is handled by clearAllTerminalEnv now
624 },
625 input: `<img src="http://example.com/img.png" alt="remote image">`,
626 expectedContains: "[Click here to view image: remote image]", // Remote images won't render without actual fetch, but hyperlinks work
627 },
628 {
629 name: "Remote image without protocol support",
630 setupImageProtocol: func() {
631 clearAllTerminalEnv()
632 },
633 clearAllImageEnv: func() {
634 // This is handled by clearAllTerminalEnv now
635 },
636 input: `<img src="http://example.com/img.png" alt="remote image">`,
637 expectedContains: "[Image: remote image,",
638 },
639 }
640
641 ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
642
643 for _, tc := range testCases {
644 t.Run(tc.name, func(t *testing.T) {
645 tc.clearAllImageEnv()
646 tc.setupImageProtocol()
647
648 processed, placements, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle, false)
649 if err != nil {
650 t.Fatalf("ProcessBody() failed: %v", err)
651 }
652
653 if tc.expectPlacements {
654 if len(placements) == 0 {
655 t.Errorf("Expected image placements but got none")
656 } else {
657 if placements[0].Base64 == "" {
658 t.Errorf("Expected non-empty Base64 in placement")
659 }
660 if placements[0].Rows < 1 {
661 t.Errorf("Expected Rows >= 1, got %d", placements[0].Rows)
662 }
663 }
664 }
665
666 cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "")
667
668 if tc.expectedContains != "" && !strings.Contains(cleanProcessed, tc.expectedContains) {
669 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expectedContains)
670 }
671
672 if tc.expectedNotContains != "" && strings.Contains(cleanProcessed, tc.expectedNotContains) {
673 t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", cleanProcessed, tc.expectedNotContains)
674 }
675 })
676 }
677}
678
679func TestProcessBody(t *testing.T) {
680 h1Style := lipgloss.NewStyle().SetString("H1")
681 h2Style := lipgloss.NewStyle().SetString("H2")
682 bodyStyle := lipgloss.NewStyle().SetString("BODY")
683
684 testCases := []struct {
685 name string
686 input string
687 expected string
688 }{
689 {
690 name: "Simple HTML",
691 input: "<p>Hello, world!</p>",
692 expected: "Hello, world!",
693 },
694 {
695 name: "With headers HTML",
696 input: "<h1>Header 1</h1>",
697 expected: "Header 1",
698 },
699 {
700 name: "With headers Markdown",
701 input: "# Header 1",
702 expected: "Header 1",
703 },
704 {
705 name: "Plain text",
706 input: "Just plain text without any markup",
707 expected: "Just plain text without any markup",
708 },
709 }
710
711 ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
712
713 for _, tc := range testCases {
714 t.Run(tc.name, func(t *testing.T) {
715 processed, _, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle, false)
716 if err != nil {
717 t.Fatalf("ProcessBody() failed: %v", err)
718 }
719
720 cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "")
721
722 if !strings.Contains(cleanProcessed, tc.expected) {
723 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expected)
724 }
725 })
726 }
727}
728
729func TestRemoteImageCache_EvictsOldestWhenFull(t *testing.T) {
730 // Start with a clean cache so prior tests don't interfere.
731 remoteImageCache.Purge()
732 // cleaning up the current test's cache
733 defer remoteImageCache.Purge()
734
735 // overfilling the cache beyond its configured capacity.
736 overfillBy := 5
737 totalInserts := remoteImageCacheSize + overfillBy
738 for i := range totalInserts {
739 url := fmt.Sprintf("https://example.com/img%d.png", i)
740 remoteImageCache.Add(url, "fake-base64-data")
741 }
742
743 // cache should not be overfilled beyond it's capped size
744 if got := remoteImageCache.Len(); got != remoteImageCacheSize {
745 t.Errorf("expected cache size %d, got %d", remoteImageCacheSize, got)
746 }
747
748 // old entries should be evicted
749 for i := range overfillBy {
750 evictedURL := fmt.Sprintf("https://example.com/img%d.png", i)
751 if _, ok := remoteImageCache.Get(evictedURL); ok {
752 t.Errorf("expected %q to be evicted, but it's still in cache", evictedURL)
753 }
754 }
755
756 // The most recent entries should still be present.
757 for i := overfillBy; i < totalInserts; i++ {
758 keptURL := fmt.Sprintf("https://example.com/img%d.png", i)
759 if _, ok := remoteImageCache.Get(keptURL); !ok {
760 t.Errorf("expected %q to still be in cache", keptURL)
761 }
762 }
763}