1package view
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "github.com/charmbracelet/lipgloss"
9)
10
11// clearAllTerminalEnv clears all environment variables that could indicate terminal capabilities
12func clearAllTerminalEnv() {
13 // Clear hyperlink support indicators
14 os.Unsetenv("VTE_VERSION")
15 os.Unsetenv("KITTY_WINDOW_ID")
16 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
17 os.Unsetenv("WEZTERM_EXECUTABLE")
18 os.Unsetenv("WEZTERM_CONFIG_FILE")
19 os.Unsetenv("ITERM_SESSION_ID")
20 os.Unsetenv("ITERM_PROFILE")
21 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
22 os.Unsetenv("WARP_COMBINED_PROMPT_COMMAND_FINISHED")
23 os.Unsetenv("KONSOLE_DBUS_SESSION")
24 os.Unsetenv("KONSOLE_VERSION")
25
26 // Set basic terminal that doesn't support anything special
27 os.Setenv("TERM", "xterm")
28 os.Setenv("TERM_PROGRAM", "basic")
29}
30
31func TestDecodeQuotedPrintable(t *testing.T) {
32 testCases := []struct {
33 name string
34 input string
35 expected string
36 }{
37 {
38 name: "Simple case",
39 input: "Hello=2C world=21",
40 expected: "Hello, world!",
41 },
42 {
43 name: "With soft line break",
44 input: "This is a long line that gets wrapped=\r\n and continues here.",
45 expected: "This is a long line that gets wrapped and continues here.",
46 },
47 {
48 name: "No encoding",
49 input: "Just a plain string.",
50 expected: "Just a plain string.",
51 },
52 }
53
54 for _, tc := range testCases {
55 t.Run(tc.name, func(t *testing.T) {
56 decoded, err := decodeQuotedPrintable(tc.input)
57 if err != nil {
58 t.Fatalf("decodeQuotedPrintable() failed: %v", err)
59 }
60 if decoded != tc.expected {
61 t.Errorf("Expected %q, got %q", tc.expected, decoded)
62 }
63 })
64 }
65}
66
67func TestMarkdownToHTML(t *testing.T) {
68 testCases := []struct {
69 name string
70 input string
71 expected string
72 }{
73 {
74 name: "Heading",
75 input: "# Hello",
76 expected: "<h1>Hello</h1>",
77 },
78 {
79 name: "Bold",
80 input: "**bold text**",
81 expected: "<p><strong>bold text</strong></p>",
82 },
83 {
84 name: "Link",
85 input: "[link](http://example.com)",
86 expected: `<p><a href="http://example.com">link</a></p>`,
87 },
88 }
89
90 for _, tc := range testCases {
91 t.Run(tc.name, func(t *testing.T) {
92 html := markdownToHTML([]byte(tc.input))
93 // Trim newlines for consistent comparison
94 if strings.TrimSpace(string(html)) != tc.expected {
95 t.Errorf("Expected %s, got %s", tc.expected, html)
96 }
97 })
98 }
99}
100
101func TestHyperlinkSupported(t *testing.T) {
102 testCases := []struct {
103 name string
104 term string
105 termProgram string
106 vteVersion string
107 kittyWindowID string
108 ghosttyResources string
109 weztermExecutable string
110 expected bool
111 }{
112 {
113 name: "Kitty terminal",
114 term: "xterm-kitty",
115 expected: true,
116 },
117 {
118 name: "Ghostty terminal",
119 term: "ghostty",
120 expected: true,
121 },
122 {
123 name: "WezTerm terminal",
124 term: "wezterm",
125 expected: true,
126 },
127 {
128 name: "Alacritty terminal",
129 term: "alacritty",
130 expected: true,
131 },
132 {
133 name: "iTerm2 via TERM_PROGRAM",
134 term: "xterm-256color",
135 termProgram: "iTerm.app",
136 expected: true,
137 },
138 {
139 name: "VTE-based terminal",
140 term: "xterm-256color",
141 vteVersion: "0.64.2",
142 expected: true,
143 },
144 {
145 name: "Kitty via env var",
146 term: "xterm-256color",
147 kittyWindowID: "1",
148 expected: true,
149 },
150 {
151 name: "Ghostty via env var",
152 term: "xterm-256color",
153 ghosttyResources: "/opt/ghostty",
154 expected: true,
155 },
156 {
157 name: "WezTerm via env var",
158 term: "xterm-256color",
159 weztermExecutable: "/usr/bin/wezterm",
160 expected: true,
161 },
162 {
163 name: "Unsupported terminal",
164 term: "xterm",
165 expected: false,
166 },
167 }
168
169 for _, tc := range testCases {
170 t.Run(tc.name, func(t *testing.T) {
171 // Save original env vars
172 origTerm := os.Getenv("TERM")
173 origTermProgram := os.Getenv("TERM_PROGRAM")
174 origVteVersion := os.Getenv("VTE_VERSION")
175 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
176 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
177 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
178
179 // Set test env vars
180 os.Setenv("TERM", tc.term)
181 os.Setenv("TERM_PROGRAM", tc.termProgram)
182 os.Setenv("VTE_VERSION", tc.vteVersion)
183 os.Setenv("KITTY_WINDOW_ID", tc.kittyWindowID)
184 os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResources)
185 os.Setenv("WEZTERM_EXECUTABLE", tc.weztermExecutable)
186
187 // Test the function
188 result := hyperlinkSupported()
189
190 // Restore original env vars
191 os.Setenv("TERM", origTerm)
192 os.Setenv("TERM_PROGRAM", origTermProgram)
193 os.Setenv("VTE_VERSION", origVteVersion)
194 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
195 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
196 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
197
198 if result != tc.expected {
199 t.Errorf("Expected %v, got %v", tc.expected, result)
200 }
201 })
202 }
203}
204
205func TestImageProtocolSupported(t *testing.T) {
206 // Save original environment variables
207 origTerm := os.Getenv("TERM")
208 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
209 origTermProgram := os.Getenv("TERM_PROGRAM")
210 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
211 origItermlSession := os.Getenv("ITERM_SESSION_ID")
212 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
213 origWarpLocal := os.Getenv("WARP_IS_LOCAL_SHELL_SESSION")
214 origKonsoleDBus := os.Getenv("KONSOLE_DBUS_SESSION")
215
216 // Restore environment variables after test
217 defer func() {
218 os.Setenv("TERM", origTerm)
219 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
220 os.Setenv("TERM_PROGRAM", origTermProgram)
221 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
222 os.Setenv("ITERM_SESSION_ID", origItermlSession)
223 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
224 os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", origWarpLocal)
225 os.Setenv("KONSOLE_DBUS_SESSION", origKonsoleDBus)
226 }()
227
228 testCases := []struct {
229 name string
230 setupEnv func()
231 clearAllEnv func()
232 expected bool
233 }{
234 {
235 name: "No supported terminals",
236 setupEnv: func() {
237 os.Setenv("TERM", "xterm")
238 os.Setenv("TERM_PROGRAM", "basic")
239 },
240 clearAllEnv: func() {
241 os.Unsetenv("KITTY_WINDOW_ID")
242 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
243 os.Unsetenv("ITERM_SESSION_ID")
244 os.Unsetenv("WEZTERM_EXECUTABLE")
245 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
246 os.Unsetenv("KONSOLE_DBUS_SESSION")
247 },
248 expected: false,
249 },
250 {
251 name: "Kitty supported via TERM",
252 setupEnv: func() {
253 os.Setenv("TERM", "xterm-kitty")
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: "Kitty supported via KITTY_WINDOW_ID",
267 setupEnv: func() {
268 os.Setenv("TERM", "xterm")
269 os.Setenv("KITTY_WINDOW_ID", "1")
270 },
271 clearAllEnv: func() {
272 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
273 os.Unsetenv("ITERM_SESSION_ID")
274 os.Unsetenv("WEZTERM_EXECUTABLE")
275 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
276 os.Unsetenv("KONSOLE_DBUS_SESSION")
277 },
278 expected: true,
279 },
280 {
281 name: "Ghostty supported via TERM_PROGRAM",
282 setupEnv: func() {
283 os.Setenv("TERM", "xterm")
284 os.Setenv("TERM_PROGRAM", "ghostty")
285 },
286 clearAllEnv: func() {
287 os.Unsetenv("KITTY_WINDOW_ID")
288 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
289 os.Unsetenv("ITERM_SESSION_ID")
290 os.Unsetenv("WEZTERM_EXECUTABLE")
291 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
292 os.Unsetenv("KONSOLE_DBUS_SESSION")
293 },
294 expected: true,
295 },
296 {
297 name: "iTerm2 supported via TERM_PROGRAM",
298 setupEnv: func() {
299 os.Setenv("TERM", "xterm")
300 os.Setenv("TERM_PROGRAM", "iterm.app")
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("WARP_IS_LOCAL_SHELL_SESSION")
308 os.Unsetenv("KONSOLE_DBUS_SESSION")
309 },
310 expected: true,
311 },
312 {
313 name: "WezTerm supported via WEZTERM_EXECUTABLE",
314 setupEnv: func() {
315 os.Setenv("TERM", "xterm")
316 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
317 },
318 clearAllEnv: func() {
319 os.Unsetenv("KITTY_WINDOW_ID")
320 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
321 os.Unsetenv("ITERM_SESSION_ID")
322 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
323 os.Unsetenv("KONSOLE_DBUS_SESSION")
324 },
325 expected: true,
326 },
327 {
328 name: "Warp supported via WARP_IS_LOCAL_SHELL_SESSION",
329 setupEnv: func() {
330 os.Setenv("TERM", "xterm")
331 os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", "1")
332 },
333 clearAllEnv: func() {
334 os.Unsetenv("KITTY_WINDOW_ID")
335 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
336 os.Unsetenv("ITERM_SESSION_ID")
337 os.Unsetenv("WEZTERM_EXECUTABLE")
338 os.Unsetenv("KONSOLE_DBUS_SESSION")
339 },
340 expected: true,
341 },
342 {
343 name: "Konsole supported via KONSOLE_DBUS_SESSION",
344 setupEnv: func() {
345 os.Setenv("TERM", "xterm")
346 os.Setenv("KONSOLE_DBUS_SESSION", "/Sessions/1")
347 },
348 clearAllEnv: func() {
349 os.Unsetenv("KITTY_WINDOW_ID")
350 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
351 os.Unsetenv("ITERM_SESSION_ID")
352 os.Unsetenv("WEZTERM_EXECUTABLE")
353 os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
354 },
355 expected: true,
356 },
357 }
358
359 for _, tc := range testCases {
360 t.Run(tc.name, func(t *testing.T) {
361 tc.clearAllEnv()
362 tc.setupEnv()
363
364 if result != tc.expected {
365 t.Errorf("Expected %q, got %q", tc.expected, result)
366 }
367 })
368 }
369}
370
371func TestHyperlinkSupported(t *testing.T) {
372 // Save original environment variables
373 origTerm := os.Getenv("TERM")
374 origTermProgram := os.Getenv("TERM_PROGRAM")
375 origVTEVersion := os.Getenv("VTE_VERSION")
376 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
377 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
378 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
379
380 // Restore environment variables after test
381 defer func() {
382 os.Setenv("TERM", origTerm)
383 os.Setenv("TERM_PROGRAM", origTermProgram)
384 os.Setenv("VTE_VERSION", origVTEVersion)
385 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
386 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
387 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
388 }()
389
390 testCases := []struct {
391 name string
392 setupEnv func()
393 clearAllEnv func()
394 expected bool
395 }{
396 {
397 name: "No hyperlink support",
398 setupEnv: func() {
399 os.Setenv("TERM", "xterm")
400 os.Setenv("TERM_PROGRAM", "basic")
401 },
402 clearAllEnv: func() {
403 os.Unsetenv("VTE_VERSION")
404 os.Unsetenv("KITTY_WINDOW_ID")
405 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
406 os.Unsetenv("WEZTERM_EXECUTABLE")
407 },
408 expected: false,
409 },
410 {
411 name: "Kitty hyperlink support via TERM",
412 setupEnv: func() {
413 os.Setenv("TERM", "xterm-kitty")
414 },
415 clearAllEnv: func() {
416 os.Unsetenv("VTE_VERSION")
417 os.Unsetenv("KITTY_WINDOW_ID")
418 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
419 os.Unsetenv("WEZTERM_EXECUTABLE")
420 },
421 expected: true,
422 },
423 {
424 name: "VTE-based terminal hyperlink support",
425 setupEnv: func() {
426 os.Setenv("TERM", "xterm")
427 os.Setenv("VTE_VERSION", "0.60.3")
428 },
429 clearAllEnv: func() {
430 os.Unsetenv("KITTY_WINDOW_ID")
431 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
432 os.Unsetenv("WEZTERM_EXECUTABLE")
433 },
434 expected: true,
435 },
436 {
437 name: "iTerm2 hyperlink support",
438 setupEnv: func() {
439 os.Setenv("TERM", "xterm")
440 os.Setenv("TERM_PROGRAM", "iterm.app")
441 },
442 clearAllEnv: func() {
443 os.Unsetenv("VTE_VERSION")
444 os.Unsetenv("KITTY_WINDOW_ID")
445 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
446 os.Unsetenv("WEZTERM_EXECUTABLE")
447 },
448 expected: true,
449 },
450 {
451 name: "WezTerm hyperlink support",
452 setupEnv: func() {
453 os.Setenv("TERM", "xterm")
454 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
455 },
456 clearAllEnv: func() {
457 os.Unsetenv("VTE_VERSION")
458 os.Unsetenv("KITTY_WINDOW_ID")
459 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
460 },
461 expected: true,
462 },
463 }
464
465 for _, tc := range testCases {
466 t.Run(tc.name, func(t *testing.T) {
467 tc.clearAllEnv()
468 tc.setupEnv()
469
470 result := hyperlinkSupported()
471 if result != tc.expected {
472 t.Errorf("Expected %t, got %t", tc.expected, result)
473 }
474 })
475 }
476}
477
478func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
479 // Save original environment variables
480 origTerm := os.Getenv("TERM")
481 origTermProgram := os.Getenv("TERM_PROGRAM")
482 origVTEVersion := os.Getenv("VTE_VERSION")
483 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
484
485 // Restore environment variables after test
486 defer func() {
487 os.Setenv("TERM", origTerm)
488 os.Setenv("TERM_PROGRAM", origTermProgram)
489 os.Setenv("VTE_VERSION", origVTEVersion)
490 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
491 }()
492
493 h1Style := lipgloss.NewStyle().SetString("H1")
494 h2Style := lipgloss.NewStyle().SetString("H2")
495 bodyStyle := lipgloss.NewStyle().SetString("BODY")
496
497 testCases := []struct {
498 name string
499 setupHyperlinks func()
500 input string
501 expectedContains string
502 expectedNotContains string
503 }{
504 {
505 name: "Link with hyperlink support",
506 setupHyperlinks: func() {
507 os.Setenv("TERM", "xterm-kitty")
508 os.Unsetenv("VTE_VERSION")
509 os.Unsetenv("KITTY_WINDOW_ID")
510 },
511 input: `<a href="http://example.com">Click here</a>`,
512 expectedContains: "Click here",
513 expectedNotContains: "<http://example.com>",
514 },
515 {
516 name: "Link without hyperlink support",
517 setupHyperlinks: func() {
518 clearAllTerminalEnv()
519 },
520 input: `<a href="http://example.com">Click here</a>`,
521 expectedContains: "Click here <http://example.com>",
522 },
523 {
524 name: "Image link with hyperlink support",
525 setupHyperlinks: func() {
526 os.Setenv("TERM", "xterm")
527 os.Setenv("VTE_VERSION", "0.60.3")
528 os.Unsetenv("KITTY_WINDOW_ID")
529 },
530 input: `<img src="http://example.com/img.png" alt="alt text">`,
531 expectedContains: "[Click here to view image: alt text]",
532 expectedNotContains: "<http://example.com/img.png>",
533 },
534 {
535 name: "Image link without hyperlink support",
536 setupHyperlinks: func() {
537 clearAllTerminalEnv()
538 },
539 input: `<img src="http://example.com/img.png" alt="alt text">`,
540 expectedContains: "[Image: alt text, http://example.com/img.png]",
541 },
542 }
543
544 for _, tc := range testCases {
545 t.Run(tc.name, func(t *testing.T) {
546 tc.setupHyperlinks()
547
548 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
549 if err != nil {
550 t.Fatalf("ProcessBody() failed: %v", err)
551 }
552
553 if !strings.Contains(processed, tc.expectedContains) {
554 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains)
555 }
556
557 if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) {
558 t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains)
559 }
560 })
561 }
562}
563
564func TestProcessBodyWithImageProtocol(t *testing.T) {
565 // Save original environment variables
566 origTerm := os.Getenv("TERM")
567 origTermProgram := os.Getenv("TERM_PROGRAM")
568 origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
569 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
570 origItermlSession := os.Getenv("ITERM_SESSION_ID")
571 origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
572
573 // Restore environment variables after test
574 defer func() {
575 os.Setenv("TERM", origTerm)
576 os.Setenv("TERM_PROGRAM", origTermProgram)
577 os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
578 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
579 os.Setenv("ITERM_SESSION_ID", origItermlSession)
580 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
581 }()
582
583 h1Style := lipgloss.NewStyle().SetString("H1")
584 h2Style := lipgloss.NewStyle().SetString("H2")
585 bodyStyle := lipgloss.NewStyle().SetString("BODY")
586
587 // Create a simple base64 PNG image (1x1 pixel white PNG)
588 testBase64PNG := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
589
590 testCases := []struct {
591 name string
592 setupImageProtocol func()
593 clearAllImageEnv func()
594 input string
595 expectedContains string
596 expectedNotContains string
597 }{
598 {
599 name: "Data URI image with Kitty support",
600 setupImageProtocol: func() {
601 os.Setenv("TERM", "xterm-kitty")
602 },
603 clearAllImageEnv: func() {
604 os.Unsetenv("KITTY_WINDOW_ID")
605 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
606 os.Unsetenv("ITERM_SESSION_ID")
607 os.Unsetenv("WEZTERM_EXECUTABLE")
608 },
609 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
610 expectedContains: "\x1b_Gf=100,a=T,q=2,C=1,m=0;",
611 expectedNotContains: "[Image: test image,",
612 },
613 {
614 name: "Data URI image with iTerm2 support",
615 setupImageProtocol: func() {
616 os.Setenv("TERM", "xterm")
617 os.Setenv("TERM_PROGRAM", "iterm.app")
618 },
619 clearAllImageEnv: func() {
620 os.Unsetenv("KITTY_WINDOW_ID")
621 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
622 os.Unsetenv("ITERM_SESSION_ID")
623 os.Unsetenv("WEZTERM_EXECUTABLE")
624 },
625 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
626 expectedContains: "\x1b]1337;File=inline=1:",
627 expectedNotContains: "[Image: test image,",
628 },
629 {
630 name: "Data URI image without protocol support",
631 setupImageProtocol: func() {
632 clearAllTerminalEnv()
633 },
634 clearAllImageEnv: func() {
635 // This is handled by clearAllTerminalEnv now
636 },
637 input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
638 expectedContains: "[Image: test image,",
639 },
640 {
641 name: "Remote image with WezTerm support (has hyperlink support)",
642 setupImageProtocol: func() {
643 clearAllTerminalEnv()
644 os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
645 },
646 clearAllImageEnv: func() {
647 // This is handled by clearAllTerminalEnv now
648 },
649 input: `<img src="http://example.com/img.png" alt="remote image">`,
650 expectedContains: "[Click here to view image: remote image]", // Remote images won't render without actual fetch, but hyperlinks work
651 },
652 {
653 name: "Remote image without protocol support",
654 setupImageProtocol: func() {
655 clearAllTerminalEnv()
656 },
657 clearAllImageEnv: func() {
658 // This is handled by clearAllTerminalEnv now
659 },
660 input: `<img src="http://example.com/img.png" alt="remote image">`,
661 expectedContains: "[Image: remote image,",
662 },
663 }
664
665 for _, tc := range testCases {
666 t.Run(tc.name, func(t *testing.T) {
667 tc.clearAllImageEnv()
668 tc.setupImageProtocol()
669
670 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
671 if err != nil {
672 t.Fatalf("ProcessBody() failed: %v", err)
673 }
674
675 if !strings.Contains(processed, tc.expectedContains) {
676 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains)
677 }
678
679 if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) {
680 t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains)
681 }
682 })
683 }
684}
685
686func TestProcessBody(t *testing.T) {
687 h1Style := lipgloss.NewStyle().SetString("H1")
688 h2Style := lipgloss.NewStyle().SetString("H2")
689 bodyStyle := lipgloss.NewStyle().SetString("BODY")
690
691 testCases := []struct {
692 name string
693 input string
694 expected string
695 }{
696 {
697 name: "Simple HTML",
698 input: "<p>Hello, world!</p>",
699 expected: "Hello, world!",
700 },
701 {
702 name: "With headers HTML",
703 input: "<h1>Header 1</h1>",
704 expected: "Header 1",
705 },
706 {
707 name: "With headers Markdown",
708 input: "# Header 1",
709 expected: "Header 1",
710 },
711 {
712 name: "Plain text",
713 input: "Just plain text without any markup",
714 expected: "Just plain text without any markup",
715 },
716 }
717
718 for _, tc := range testCases {
719 t.Run(tc.name, func(t *testing.T) {
720 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
721 if err != nil {
722 t.Fatalf("ProcessBody() failed: %v", err)
723 }
724 // Use Contains because styles add ANSI codes
725 if !strings.Contains(processed, tc.expected) {
726 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expected)
727 }
728 })
729 }
730}
731
732func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
733 h1Style := lipgloss.NewStyle()
734 h2Style := lipgloss.NewStyle()
735 bodyStyle := lipgloss.NewStyle()
736
737 testCases := []struct {
738 name string
739 input string
740 hyperlinkSupported bool
741 expectedContains []string
742 expectedNotContains []string
743 }{
744 {
745 name: "Link with hyperlink support",
746 input: `<a href="https://example.com">Click here</a>`,
747 hyperlinkSupported: true,
748 expectedContains: []string{"\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07"},
749 expectedNotContains: []string{"<https://example.com>"},
750 },
751 {
752 name: "Link without hyperlink support",
753 input: `<a href="https://example.com">Click here</a>`,
754 hyperlinkSupported: false,
755 expectedContains: []string{"Click here <https://example.com>"},
756 expectedNotContains: []string{"\x1b]8;;"},
757 },
758 {
759 name: "Image with hyperlink support",
760 input: `<img src="https://example.com/image.png" alt="Test image">`,
761 hyperlinkSupported: true,
762 expectedContains: []string{"\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"},
763 expectedNotContains: []string{"<https://example.com/image.png>"},
764 },
765 {
766 name: "Image without hyperlink support",
767 input: `<img src="https://example.com/image.png" alt="Test image">`,
768 hyperlinkSupported: false,
769 expectedContains: []string{"[Image: Test image, https://example.com/image.png]"},
770 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
771 },
772 {
773 name: "Markdown link with hyperlink support",
774 input: `[Visit our site](https://example.com)`,
775 hyperlinkSupported: true,
776 expectedContains: []string{"\x1b]8;;https://example.com\x07Visit our site\x1b]8;;\x07"},
777 expectedNotContains: []string{"<https://example.com>"},
778 },
779 {
780 name: "Markdown link without hyperlink support",
781 input: `[Visit our site](https://example.com)`,
782 hyperlinkSupported: false,
783 expectedContains: []string{"Visit our site <https://example.com>"},
784 expectedNotContains: []string{"\x1b]8;;"},
785 },
786 {
787 name: "Markdown image with hyperlink support",
788 input: ``,
789 hyperlinkSupported: true,
790 expectedContains: []string{"\x1b]8;;https://example.com/sunset.jpg\x07", "[Click here to view image: Beautiful sunset]"},
791 expectedNotContains: []string{"<https://example.com/sunset.jpg>"},
792 },
793 {
794 name: "Markdown image without hyperlink support",
795 input: ``,
796 hyperlinkSupported: false,
797 expectedContains: []string{"[Image: Beautiful sunset, https://example.com/sunset.jpg]"},
798 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
799 },
800 }
801
802 for _, tc := range testCases {
803 t.Run(tc.name, func(t *testing.T) {
804 // Save original env vars
805 origTerm := os.Getenv("TERM")
806 origTermProgram := os.Getenv("TERM_PROGRAM")
807 origVteVersion := os.Getenv("VTE_VERSION")
808 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
809 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
810 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
811
812 // Set env to control hyperlink support
813 if tc.hyperlinkSupported {
814 os.Setenv("TERM", "xterm-kitty")
815 } else {
816 // Clear all environment variables that could indicate hyperlink support
817 os.Setenv("TERM", "xterm")
818 os.Unsetenv("TERM_PROGRAM")
819 os.Unsetenv("VTE_VERSION")
820 os.Unsetenv("KITTY_WINDOW_ID")
821 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
822 os.Unsetenv("WEZTERM_EXECUTABLE")
823 }
824
825 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
826
827 // Restore original env vars
828 os.Setenv("TERM", origTerm)
829 os.Setenv("TERM_PROGRAM", origTermProgram)
830 os.Setenv("VTE_VERSION", origVteVersion)
831 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
832 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
833 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
834
835 if err != nil {
836 t.Fatalf("ProcessBody() failed: %v", err)
837 }
838
839 // Check that expected strings are present
840 for _, expected := range tc.expectedContains {
841 if !strings.Contains(processed, expected) {
842 t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
843 }
844 }
845
846 // Check that unexpected strings are not present
847 for _, notExpected := range tc.expectedNotContains {
848 if strings.Contains(processed, notExpected) {
849 t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
850 }
851 }
852 })
853 }
854}
855
856func TestImageAndLinkFallbackBehavior(t *testing.T) {
857 h1Style := lipgloss.NewStyle()
858 h2Style := lipgloss.NewStyle()
859 bodyStyle := lipgloss.NewStyle()
860
861 testCases := []struct {
862 name string
863 input string
864 hyperlinkSupported bool
865 imageProtocolSupported bool
866 expectedContains []string
867 expectedNotContains []string
868 }{
869 {
870 name: "Full support - both image protocol and hyperlinks",
871 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
872 hyperlinkSupported: true,
873 imageProtocolSupported: true,
874 expectedContains: []string{"\x1b]8;;https://example.com\x07link\x1b]8;;\x07", "\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"},
875 expectedNotContains: []string{"<https://example.com>", "[Image:"},
876 },
877 {
878 name: "Hyperlink support only - no image protocol",
879 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
880 hyperlinkSupported: true,
881 imageProtocolSupported: false,
882 expectedContains: []string{"\x1b]8;;https://example.com\x07link\x1b]8;;\x07", "\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"},
883 expectedNotContains: []string{"<https://example.com>", "[Image:"},
884 },
885 {
886 name: "No support - plain text fallback",
887 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
888 hyperlinkSupported: false,
889 imageProtocolSupported: false,
890 expectedContains: []string{"link <https://example.com>", "[Image: Test image, https://example.com/image.png]"},
891 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
892 },
893 }
894
895 for _, tc := range testCases {
896 t.Run(tc.name, func(t *testing.T) {
897 // Save original env vars
898 origTerm := os.Getenv("TERM")
899 origTermProgram := os.Getenv("TERM_PROGRAM")
900 origVteVersion := os.Getenv("VTE_VERSION")
901 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
902 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
903 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
904
905 // Set environment for hyperlink support
906 if tc.hyperlinkSupported {
907 os.Setenv("TERM", "xterm-kitty")
908 } else {
909 os.Setenv("TERM", "xterm")
910 os.Unsetenv("TERM_PROGRAM")
911 os.Unsetenv("VTE_VERSION")
912 os.Unsetenv("KITTY_WINDOW_ID")
913 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
914 os.Unsetenv("WEZTERM_EXECUTABLE")
915 }
916
917 // Set environment for image protocol support
918 if tc.imageProtocolSupported && !tc.hyperlinkSupported {
919 // This case shouldn't happen in practice, but test it anyway
920 os.Setenv("KITTY_WINDOW_ID", "1")
921 } else if !tc.imageProtocolSupported {
922 os.Unsetenv("KITTY_WINDOW_ID")
923 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
924 }
925
926 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
927
928 // Restore original env vars
929 os.Setenv("TERM", origTerm)
930 os.Setenv("TERM_PROGRAM", origTermProgram)
931 os.Setenv("VTE_VERSION", origVteVersion)
932 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
933 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
934 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
935
936 if err != nil {
937 t.Fatalf("ProcessBody() failed: %v", err)
938 }
939
940 // Check that expected strings are present
941 for _, expected := range tc.expectedContains {
942 if !strings.Contains(processed, expected) {
943 t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
944 }
945 }
946
947 // Check that unexpected strings are not present
948 for _, notExpected := range tc.expectedNotContains {
949 if strings.Contains(processed, notExpected) {
950 t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
951 }
952 }
953 })
954 }
955}