html_test.go

  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: "&lt;http://example.com&gt;",
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: "&lt;http://example.com/img.png&gt;",
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:               `![Beautiful sunset](https://example.com/sunset.jpg)`,
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:               `![Beautiful sunset](https://example.com/sunset.jpg)`,
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}