1package view
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "github.com/charmbracelet/lipgloss"
9)
10
11func TestDecodeQuotedPrintable(t *testing.T) {
12 testCases := []struct {
13 name string
14 input string
15 expected string
16 }{
17 {
18 name: "Simple case",
19 input: "Hello=2C world=21",
20 expected: "Hello, world!",
21 },
22 {
23 name: "With soft line break",
24 input: "This is a long line that gets wrapped=\r\n and continues here.",
25 expected: "This is a long line that gets wrapped and continues here.",
26 },
27 {
28 name: "No encoding",
29 input: "Just a plain string.",
30 expected: "Just a plain string.",
31 },
32 }
33
34 for _, tc := range testCases {
35 t.Run(tc.name, func(t *testing.T) {
36 decoded, err := decodeQuotedPrintable(tc.input)
37 if err != nil {
38 t.Fatalf("decodeQuotedPrintable() failed: %v", err)
39 }
40 if decoded != tc.expected {
41 t.Errorf("Expected %q, got %q", tc.expected, decoded)
42 }
43 })
44 }
45}
46
47func TestMarkdownToHTML(t *testing.T) {
48 testCases := []struct {
49 name string
50 input string
51 expected string
52 }{
53 {
54 name: "Heading",
55 input: "# Hello",
56 expected: "<h1>Hello</h1>",
57 },
58 {
59 name: "Bold",
60 input: "**bold text**",
61 expected: "<p><strong>bold text</strong></p>",
62 },
63 {
64 name: "Link",
65 input: "[link](http://example.com)",
66 expected: `<p><a href="http://example.com">link</a></p>`,
67 },
68 }
69
70 for _, tc := range testCases {
71 t.Run(tc.name, func(t *testing.T) {
72 html := markdownToHTML([]byte(tc.input))
73 // Trim newlines for consistent comparison
74 if strings.TrimSpace(string(html)) != tc.expected {
75 t.Errorf("Expected %s, got %s", tc.expected, html)
76 }
77 })
78 }
79}
80
81func TestHyperlinkSupported(t *testing.T) {
82 testCases := []struct {
83 name string
84 term string
85 termProgram string
86 vteVersion string
87 kittyWindowID string
88 ghosttyResources string
89 weztermExecutable string
90 expected bool
91 }{
92 {
93 name: "Kitty terminal",
94 term: "xterm-kitty",
95 expected: true,
96 },
97 {
98 name: "Ghostty terminal",
99 term: "ghostty",
100 expected: true,
101 },
102 {
103 name: "WezTerm terminal",
104 term: "wezterm",
105 expected: true,
106 },
107 {
108 name: "Alacritty terminal",
109 term: "alacritty",
110 expected: true,
111 },
112 {
113 name: "iTerm2 via TERM_PROGRAM",
114 term: "xterm-256color",
115 termProgram: "iTerm.app",
116 expected: true,
117 },
118 {
119 name: "VTE-based terminal",
120 term: "xterm-256color",
121 vteVersion: "0.64.2",
122 expected: true,
123 },
124 {
125 name: "Kitty via env var",
126 term: "xterm-256color",
127 kittyWindowID: "1",
128 expected: true,
129 },
130 {
131 name: "Ghostty via env var",
132 term: "xterm-256color",
133 ghosttyResources: "/opt/ghostty",
134 expected: true,
135 },
136 {
137 name: "WezTerm via env var",
138 term: "xterm-256color",
139 weztermExecutable: "/usr/bin/wezterm",
140 expected: true,
141 },
142 {
143 name: "Unsupported terminal",
144 term: "xterm",
145 expected: false,
146 },
147 }
148
149 for _, tc := range testCases {
150 t.Run(tc.name, func(t *testing.T) {
151 // Save original env vars
152 origTerm := os.Getenv("TERM")
153 origTermProgram := os.Getenv("TERM_PROGRAM")
154 origVteVersion := os.Getenv("VTE_VERSION")
155 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
156 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
157 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
158
159 // Set test env vars
160 os.Setenv("TERM", tc.term)
161 os.Setenv("TERM_PROGRAM", tc.termProgram)
162 os.Setenv("VTE_VERSION", tc.vteVersion)
163 os.Setenv("KITTY_WINDOW_ID", tc.kittyWindowID)
164 os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResources)
165 os.Setenv("WEZTERM_EXECUTABLE", tc.weztermExecutable)
166
167 // Test the function
168 result := hyperlinkSupported()
169
170 // Restore original env vars
171 os.Setenv("TERM", origTerm)
172 os.Setenv("TERM_PROGRAM", origTermProgram)
173 os.Setenv("VTE_VERSION", origVteVersion)
174 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
175 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
176 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
177
178 if result != tc.expected {
179 t.Errorf("Expected %v, got %v", tc.expected, result)
180 }
181 })
182 }
183}
184
185func TestHyperlink(t *testing.T) {
186 testCases := []struct {
187 name string
188 url string
189 text string
190 hyperlinkSupported bool
191 expected string
192 }{
193 {
194 name: "Supported terminal with different text",
195 url: "https://example.com",
196 text: "Click here",
197 hyperlinkSupported: true,
198 expected: "\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07",
199 },
200 {
201 name: "Supported terminal with same text as URL",
202 url: "https://example.com",
203 text: "https://example.com",
204 hyperlinkSupported: true,
205 expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
206 },
207 {
208 name: "Supported terminal with empty text",
209 url: "https://example.com",
210 text: "",
211 hyperlinkSupported: true,
212 expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
213 },
214 {
215 name: "Unsupported terminal with different text",
216 url: "https://example.com",
217 text: "Click here",
218 hyperlinkSupported: false,
219 expected: "Click here <https://example.com>",
220 },
221 {
222 name: "Unsupported terminal with same text as URL",
223 url: "https://example.com",
224 text: "https://example.com",
225 hyperlinkSupported: false,
226 expected: "<https://example.com>",
227 },
228 {
229 name: "Unsupported terminal with empty text",
230 url: "https://example.com",
231 text: "",
232 hyperlinkSupported: false,
233 expected: "<https://example.com>",
234 },
235 }
236
237 for _, tc := range testCases {
238 t.Run(tc.name, func(t *testing.T) {
239 // Save original env vars
240 origTerm := os.Getenv("TERM")
241 origTermProgram := os.Getenv("TERM_PROGRAM")
242 origVteVersion := os.Getenv("VTE_VERSION")
243 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
244 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
245 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
246
247 // Set env to control hyperlink support
248 if tc.hyperlinkSupported {
249 os.Setenv("TERM", "xterm-kitty")
250 } else {
251 // Clear all environment variables that could indicate hyperlink support
252 os.Setenv("TERM", "xterm")
253 os.Unsetenv("TERM_PROGRAM")
254 os.Unsetenv("VTE_VERSION")
255 os.Unsetenv("KITTY_WINDOW_ID")
256 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
257 os.Unsetenv("WEZTERM_EXECUTABLE")
258 }
259
260 result := hyperlink(tc.url, tc.text)
261
262 // Restore original env vars
263 os.Setenv("TERM", origTerm)
264 os.Setenv("TERM_PROGRAM", origTermProgram)
265 os.Setenv("VTE_VERSION", origVteVersion)
266 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
267 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
268 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
269
270 if result != tc.expected {
271 t.Errorf("Expected %q, got %q", tc.expected, result)
272 }
273 })
274 }
275}
276
277func TestProcessBody(t *testing.T) {
278 h1Style := lipgloss.NewStyle().SetString("H1")
279 h2Style := lipgloss.NewStyle().SetString("H2")
280 bodyStyle := lipgloss.NewStyle().SetString("BODY")
281
282 testCases := []struct {
283 name string
284 input string
285 expected string
286 }{
287 {
288 name: "Simple HTML",
289 input: "<p>Hello, world!</p>",
290 expected: "Hello, world!",
291 },
292 {
293 name: "With link HTML",
294 input: `<a href="http://example.com">Click here</a>`,
295 expected: "Click here",
296 },
297 {
298 name: "With image HTML",
299 input: `<img src="http://example.com/img.png" alt="alt text">`,
300 expected: "[Image: alt text, http://example.com/img.png]",
301 },
302 {
303 name: "With headers HTML",
304 input: "<h1>Header 1</h1>",
305 expected: "Header 1",
306 },
307 {
308 name: "With link Markdown",
309 input: `[Click here](http://example.com)`,
310 expected: "Click here",
311 },
312 {
313 name: "With image Markdown",
314 input: ``,
315 expected: "[Image: alt text, http://example.com/img.png]",
316 },
317 {
318 name: "With headers Markdown",
319 input: "# Header 1",
320 expected: "Header 1",
321 },
322 }
323
324 for _, tc := range testCases {
325 t.Run(tc.name, func(t *testing.T) {
326 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
327 if err != nil {
328 t.Fatalf("ProcessBody() failed: %v", err)
329 }
330 // Use Contains because styles add ANSI codes
331 if !strings.Contains(processed, tc.expected) {
332 t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expected)
333 }
334 })
335 }
336}
337
338func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
339 h1Style := lipgloss.NewStyle()
340 h2Style := lipgloss.NewStyle()
341 bodyStyle := lipgloss.NewStyle()
342
343 testCases := []struct {
344 name string
345 input string
346 hyperlinkSupported bool
347 expectedContains []string
348 expectedNotContains []string
349 }{
350 {
351 name: "Link with hyperlink support",
352 input: `<a href="https://example.com">Click here</a>`,
353 hyperlinkSupported: true,
354 expectedContains: []string{"\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07"},
355 expectedNotContains: []string{"<https://example.com>"},
356 },
357 {
358 name: "Link without hyperlink support",
359 input: `<a href="https://example.com">Click here</a>`,
360 hyperlinkSupported: false,
361 expectedContains: []string{"Click here <https://example.com>"},
362 expectedNotContains: []string{"\x1b]8;;"},
363 },
364 {
365 name: "Image with hyperlink support",
366 input: `<img src="https://example.com/image.png" alt="Test image">`,
367 hyperlinkSupported: true,
368 expectedContains: []string{"\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"},
369 expectedNotContains: []string{"<https://example.com/image.png>"},
370 },
371 {
372 name: "Image without hyperlink support",
373 input: `<img src="https://example.com/image.png" alt="Test image">`,
374 hyperlinkSupported: false,
375 expectedContains: []string{"[Image: Test image, https://example.com/image.png]"},
376 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
377 },
378 {
379 name: "Markdown link with hyperlink support",
380 input: `[Visit our site](https://example.com)`,
381 hyperlinkSupported: true,
382 expectedContains: []string{"\x1b]8;;https://example.com\x07Visit our site\x1b]8;;\x07"},
383 expectedNotContains: []string{"<https://example.com>"},
384 },
385 {
386 name: "Markdown link without hyperlink support",
387 input: `[Visit our site](https://example.com)`,
388 hyperlinkSupported: false,
389 expectedContains: []string{"Visit our site <https://example.com>"},
390 expectedNotContains: []string{"\x1b]8;;"},
391 },
392 {
393 name: "Markdown image with hyperlink support",
394 input: ``,
395 hyperlinkSupported: true,
396 expectedContains: []string{"\x1b]8;;https://example.com/sunset.jpg\x07", "[Click here to view image: Beautiful sunset]"},
397 expectedNotContains: []string{"<https://example.com/sunset.jpg>"},
398 },
399 {
400 name: "Markdown image without hyperlink support",
401 input: ``,
402 hyperlinkSupported: false,
403 expectedContains: []string{"[Image: Beautiful sunset, https://example.com/sunset.jpg]"},
404 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
405 },
406 }
407
408 for _, tc := range testCases {
409 t.Run(tc.name, func(t *testing.T) {
410 // Save original env vars
411 origTerm := os.Getenv("TERM")
412 origTermProgram := os.Getenv("TERM_PROGRAM")
413 origVteVersion := os.Getenv("VTE_VERSION")
414 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
415 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
416 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
417
418 // Set env to control hyperlink support
419 if tc.hyperlinkSupported {
420 os.Setenv("TERM", "xterm-kitty")
421 } else {
422 // Clear all environment variables that could indicate hyperlink support
423 os.Setenv("TERM", "xterm")
424 os.Unsetenv("TERM_PROGRAM")
425 os.Unsetenv("VTE_VERSION")
426 os.Unsetenv("KITTY_WINDOW_ID")
427 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
428 os.Unsetenv("WEZTERM_EXECUTABLE")
429 }
430
431 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
432
433 // Restore original env vars
434 os.Setenv("TERM", origTerm)
435 os.Setenv("TERM_PROGRAM", origTermProgram)
436 os.Setenv("VTE_VERSION", origVteVersion)
437 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
438 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
439 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
440
441 if err != nil {
442 t.Fatalf("ProcessBody() failed: %v", err)
443 }
444
445 // Check that expected strings are present
446 for _, expected := range tc.expectedContains {
447 if !strings.Contains(processed, expected) {
448 t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
449 }
450 }
451
452 // Check that unexpected strings are not present
453 for _, notExpected := range tc.expectedNotContains {
454 if strings.Contains(processed, notExpected) {
455 t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
456 }
457 }
458 })
459 }
460}
461
462func TestImageAndLinkFallbackBehavior(t *testing.T) {
463 h1Style := lipgloss.NewStyle()
464 h2Style := lipgloss.NewStyle()
465 bodyStyle := lipgloss.NewStyle()
466
467 testCases := []struct {
468 name string
469 input string
470 hyperlinkSupported bool
471 imageProtocolSupported bool
472 expectedContains []string
473 expectedNotContains []string
474 }{
475 {
476 name: "Full support - both image protocol and hyperlinks",
477 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
478 hyperlinkSupported: true,
479 imageProtocolSupported: true,
480 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]"},
481 expectedNotContains: []string{"<https://example.com>", "[Image:"},
482 },
483 {
484 name: "Hyperlink support only - no image protocol",
485 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
486 hyperlinkSupported: true,
487 imageProtocolSupported: false,
488 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]"},
489 expectedNotContains: []string{"<https://example.com>", "[Image:"},
490 },
491 {
492 name: "No support - plain text fallback",
493 input: `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
494 hyperlinkSupported: false,
495 imageProtocolSupported: false,
496 expectedContains: []string{"link <https://example.com>", "[Image: Test image, https://example.com/image.png]"},
497 expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
498 },
499 }
500
501 for _, tc := range testCases {
502 t.Run(tc.name, func(t *testing.T) {
503 // Save original env vars
504 origTerm := os.Getenv("TERM")
505 origTermProgram := os.Getenv("TERM_PROGRAM")
506 origVteVersion := os.Getenv("VTE_VERSION")
507 origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
508 origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
509 origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
510
511 // Set environment for hyperlink support
512 if tc.hyperlinkSupported {
513 os.Setenv("TERM", "xterm-kitty")
514 } else {
515 os.Setenv("TERM", "xterm")
516 os.Unsetenv("TERM_PROGRAM")
517 os.Unsetenv("VTE_VERSION")
518 os.Unsetenv("KITTY_WINDOW_ID")
519 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
520 os.Unsetenv("WEZTERM_EXECUTABLE")
521 }
522
523 // Set environment for image protocol support
524 if tc.imageProtocolSupported && !tc.hyperlinkSupported {
525 // This case shouldn't happen in practice, but test it anyway
526 os.Setenv("KITTY_WINDOW_ID", "1")
527 } else if !tc.imageProtocolSupported {
528 os.Unsetenv("KITTY_WINDOW_ID")
529 os.Unsetenv("GHOSTTY_RESOURCES_DIR")
530 }
531
532 processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
533
534 // Restore original env vars
535 os.Setenv("TERM", origTerm)
536 os.Setenv("TERM_PROGRAM", origTermProgram)
537 os.Setenv("VTE_VERSION", origVteVersion)
538 os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
539 os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
540 os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
541
542 if err != nil {
543 t.Fatalf("ProcessBody() failed: %v", err)
544 }
545
546 // Check that expected strings are present
547 for _, expected := range tc.expectedContains {
548 if !strings.Contains(processed, expected) {
549 t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
550 }
551 }
552
553 // Check that unexpected strings are not present
554 for _, notExpected := range tc.expectedNotContains {
555 if strings.Contains(processed, notExpected) {
556 t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
557 }
558 }
559 })
560 }
561}