html_test.go

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