fix(view): test fixed for the new hyperlink and image support (#92) (#94)

Drew Smirnoff created

Change summary

view/html.go      |   5 
view/html_test.go | 478 ++++++++++++++++++++++++++++++++++++++----------
2 files changed, 381 insertions(+), 102 deletions(-)

Detailed changes

view/html.go 🔗

@@ -143,10 +143,11 @@ func hyperlink(url, text string) string {
 		return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
 	} else {
 		// Fallback to plain text format for unsupported terminals
+		// Use HTML-encoded angle brackets to prevent HTML parser from treating them as tags
 		if text == url {
-			return fmt.Sprintf("<%s>", url)
+			return fmt.Sprintf("&lt;%s&gt;", url)
 		}
-		return fmt.Sprintf("%s <%s>", text, url)
+		return fmt.Sprintf("%s &lt;%s&gt;", text, url)
 	}
 }
 

view/html_test.go 🔗

@@ -78,144 +78,197 @@ func TestMarkdownToHTML(t *testing.T) {
 	}
 }
 
-func TestGhosttySupported(t *testing.T) {
-	// Save original environment variables
-	origTerm := os.Getenv("TERM")
-	origTermProgram := os.Getenv("TERM_PROGRAM")
-	origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
-
-	// Restore environment variables after test
-	defer func() {
-		os.Setenv("TERM", origTerm)
-		os.Setenv("TERM_PROGRAM", origTermProgram)
-		os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
-	}()
-
+func TestHyperlinkSupported(t *testing.T) {
 	testCases := []struct {
-		name                string
-		term                string
-		termProgram         string
-		ghosttyResourcesDir string
-		expected            bool
+		name              string
+		term              string
+		termProgram       string
+		vteVersion        string
+		kittyWindowID     string
+		ghosttyResources  string
+		weztermExecutable string
+		expected          bool
 	}{
 		{
-			name:                "No Ghostty environment variables",
-			term:                "xterm",
-			termProgram:         "",
-			ghosttyResourcesDir: "",
-			expected:            false,
+			name:     "Kitty terminal",
+			term:     "xterm-kitty",
+			expected: true,
+		},
+		{
+			name:     "Ghostty terminal",
+			term:     "ghostty",
+			expected: true,
 		},
 		{
-			name:                "TERM contains ghostty",
-			term:                "xterm-ghostty",
-			termProgram:         "",
-			ghosttyResourcesDir: "",
-			expected:            true,
+			name:     "WezTerm terminal",
+			term:     "wezterm",
+			expected: true,
 		},
 		{
-			name:                "TERM_PROGRAM is ghostty",
-			term:                "xterm",
-			termProgram:         "ghostty",
-			ghosttyResourcesDir: "",
-			expected:            true,
+			name:     "Alacritty terminal",
+			term:     "alacritty",
+			expected: true,
 		},
 		{
-			name:                "GHOSTTY_RESOURCES_DIR is set",
-			term:                "xterm",
-			termProgram:         "",
-			ghosttyResourcesDir: "/usr/share/ghostty",
-			expected:            true,
+			name:        "iTerm2 via TERM_PROGRAM",
+			term:        "xterm-256color",
+			termProgram: "iTerm.app",
+			expected:    true,
 		},
 		{
-			name:                "Multiple Ghostty indicators",
-			term:                "ghostty",
-			termProgram:         "ghostty",
-			ghosttyResourcesDir: "/usr/share/ghostty",
-			expected:            true,
+			name:       "VTE-based terminal",
+			term:       "xterm-256color",
+			vteVersion: "0.64.2",
+			expected:   true,
+		},
+		{
+			name:          "Kitty via env var",
+			term:          "xterm-256color",
+			kittyWindowID: "1",
+			expected:      true,
+		},
+		{
+			name:             "Ghostty via env var",
+			term:             "xterm-256color",
+			ghosttyResources: "/opt/ghostty",
+			expected:         true,
+		},
+		{
+			name:              "WezTerm via env var",
+			term:              "xterm-256color",
+			weztermExecutable: "/usr/bin/wezterm",
+			expected:          true,
+		},
+		{
+			name:     "Unsupported terminal",
+			term:     "xterm",
+			expected: false,
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
+			// Save original env vars
+			origTerm := os.Getenv("TERM")
+			origTermProgram := os.Getenv("TERM_PROGRAM")
+			origVteVersion := os.Getenv("VTE_VERSION")
+			origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
+			origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+			origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
+
+			// Set test env vars
 			os.Setenv("TERM", tc.term)
 			os.Setenv("TERM_PROGRAM", tc.termProgram)
-			os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResourcesDir)
+			os.Setenv("VTE_VERSION", tc.vteVersion)
+			os.Setenv("KITTY_WINDOW_ID", tc.kittyWindowID)
+			os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResources)
+			os.Setenv("WEZTERM_EXECUTABLE", tc.weztermExecutable)
+
+			// Test the function
+			result := hyperlinkSupported()
+
+			// Restore original env vars
+			os.Setenv("TERM", origTerm)
+			os.Setenv("TERM_PROGRAM", origTermProgram)
+			os.Setenv("VTE_VERSION", origVteVersion)
+			os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
+			os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+			os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
 
-			result := ghosttySupported()
 			if result != tc.expected {
-				t.Errorf("Expected %t, got %t", tc.expected, result)
+				t.Errorf("Expected %v, got %v", tc.expected, result)
 			}
 		})
 	}
 }
 
-func TestImageProtocolSupported(t *testing.T) {
-	// Save original environment variables
-	origTerm := os.Getenv("TERM")
-	origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
-	origTermProgram := os.Getenv("TERM_PROGRAM")
-	origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
-
-	// Restore environment variables after test
-	defer func() {
-		os.Setenv("TERM", origTerm)
-		os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
-		os.Setenv("TERM_PROGRAM", origTermProgram)
-		os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
-	}()
-
+func TestHyperlink(t *testing.T) {
 	testCases := []struct {
-		name                string
-		term                string
-		kittyWindow         string
-		termProgram         string
-		ghosttyResourcesDir string
-		expected            bool
+		name               string
+		url                string
+		text               string
+		hyperlinkSupported bool
+		expected           string
 	}{
 		{
-			name:                "No supported terminals",
-			term:                "xterm",
-			kittyWindow:         "",
-			termProgram:         "",
-			ghosttyResourcesDir: "",
-			expected:            false,
+			name:               "Supported terminal with different text",
+			url:                "https://example.com",
+			text:               "Click here",
+			hyperlinkSupported: true,
+			expected:           "\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07",
+		},
+		{
+			name:               "Supported terminal with same text as URL",
+			url:                "https://example.com",
+			text:               "https://example.com",
+			hyperlinkSupported: true,
+			expected:           "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
 		},
 		{
-			name:                "Kitty supported",
-			term:                "xterm-kitty",
-			kittyWindow:         "",
-			termProgram:         "",
-			ghosttyResourcesDir: "",
-			expected:            true,
+			name:               "Supported terminal with empty text",
+			url:                "https://example.com",
+			text:               "",
+			hyperlinkSupported: true,
+			expected:           "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
 		},
 		{
-			name:                "Ghostty supported",
-			term:                "xterm",
-			kittyWindow:         "",
-			termProgram:         "ghostty",
-			ghosttyResourcesDir: "",
-			expected:            true,
+			name:               "Unsupported terminal with different text",
+			url:                "https://example.com",
+			text:               "Click here",
+			hyperlinkSupported: false,
+			expected:           "Click here &lt;https://example.com&gt;",
 		},
 		{
-			name:                "Both Kitty and Ghostty supported",
-			term:                "xterm-kitty",
-			kittyWindow:         "1",
-			termProgram:         "ghostty",
-			ghosttyResourcesDir: "/usr/share/ghostty",
-			expected:            true,
+			name:               "Unsupported terminal with same text as URL",
+			url:                "https://example.com",
+			text:               "https://example.com",
+			hyperlinkSupported: false,
+			expected:           "&lt;https://example.com&gt;",
+		},
+		{
+			name:               "Unsupported terminal with empty text",
+			url:                "https://example.com",
+			text:               "",
+			hyperlinkSupported: false,
+			expected:           "&lt;https://example.com&gt;",
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			os.Setenv("TERM", tc.term)
-			os.Setenv("KITTY_WINDOW_ID", tc.kittyWindow)
-			os.Setenv("TERM_PROGRAM", tc.termProgram)
-			os.Setenv("GHOSTTY_RESOURCES_DIR", tc.ghosttyResourcesDir)
+			// Save original env vars
+			origTerm := os.Getenv("TERM")
+			origTermProgram := os.Getenv("TERM_PROGRAM")
+			origVteVersion := os.Getenv("VTE_VERSION")
+			origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
+			origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+			origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
+
+			// Set env to control hyperlink support
+			if tc.hyperlinkSupported {
+				os.Setenv("TERM", "xterm-kitty")
+			} else {
+				// Clear all environment variables that could indicate hyperlink support
+				os.Setenv("TERM", "xterm")
+				os.Unsetenv("TERM_PROGRAM")
+				os.Unsetenv("VTE_VERSION")
+				os.Unsetenv("KITTY_WINDOW_ID")
+				os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+				os.Unsetenv("WEZTERM_EXECUTABLE")
+			}
+
+			result := hyperlink(tc.url, tc.text)
+
+			// Restore original env vars
+			os.Setenv("TERM", origTerm)
+			os.Setenv("TERM_PROGRAM", origTermProgram)
+			os.Setenv("VTE_VERSION", origVteVersion)
+			os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
+			os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+			os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
 
-			result := imageProtocolSupported()
 			if result != tc.expected {
-				t.Errorf("Expected %t, got %t", tc.expected, result)
+				t.Errorf("Expected %q, got %q", tc.expected, result)
 			}
 		})
 	}
@@ -244,7 +297,7 @@ func TestProcessBody(t *testing.T) {
 		{
 			name:     "With image HTML",
 			input:    `<img src="http://example.com/img.png" alt="alt text">`,
-			expected: "[Click here to view image: alt text]",
+			expected: "[Image: alt text, http://example.com/img.png]",
 		},
 		{
 			name:     "With headers HTML",
@@ -258,8 +311,8 @@ func TestProcessBody(t *testing.T) {
 		},
 		{
 			name:     "With image Markdown",
-			input:    `![alt text](http://example.com/img.png)>`,
-			expected: "[Click here to view image: alt text]",
+			input:    `![alt text](http://example.com/img.png)`,
+			expected: "[Image: alt text, http://example.com/img.png]",
 		},
 		{
 			name:     "With headers Markdown",
@@ -281,3 +334,228 @@ func TestProcessBody(t *testing.T) {
 		})
 	}
 }
+
+func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
+	h1Style := lipgloss.NewStyle()
+	h2Style := lipgloss.NewStyle()
+	bodyStyle := lipgloss.NewStyle()
+
+	testCases := []struct {
+		name                string
+		input               string
+		hyperlinkSupported  bool
+		expectedContains    []string
+		expectedNotContains []string
+	}{
+		{
+			name:                "Link with hyperlink support",
+			input:               `<a href="https://example.com">Click here</a>`,
+			hyperlinkSupported:  true,
+			expectedContains:    []string{"\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07"},
+			expectedNotContains: []string{"<https://example.com>"},
+		},
+		{
+			name:                "Link without hyperlink support",
+			input:               `<a href="https://example.com">Click here</a>`,
+			hyperlinkSupported:  false,
+			expectedContains:    []string{"Click here <https://example.com>"},
+			expectedNotContains: []string{"\x1b]8;;"},
+		},
+		{
+			name:                "Image with hyperlink support",
+			input:               `<img src="https://example.com/image.png" alt="Test image">`,
+			hyperlinkSupported:  true,
+			expectedContains:    []string{"\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"},
+			expectedNotContains: []string{"<https://example.com/image.png>"},
+		},
+		{
+			name:                "Image without hyperlink support",
+			input:               `<img src="https://example.com/image.png" alt="Test image">`,
+			hyperlinkSupported:  false,
+			expectedContains:    []string{"[Image: Test image, https://example.com/image.png]"},
+			expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
+		},
+		{
+			name:                "Markdown link with hyperlink support",
+			input:               `[Visit our site](https://example.com)`,
+			hyperlinkSupported:  true,
+			expectedContains:    []string{"\x1b]8;;https://example.com\x07Visit our site\x1b]8;;\x07"},
+			expectedNotContains: []string{"<https://example.com>"},
+		},
+		{
+			name:                "Markdown link without hyperlink support",
+			input:               `[Visit our site](https://example.com)`,
+			hyperlinkSupported:  false,
+			expectedContains:    []string{"Visit our site <https://example.com>"},
+			expectedNotContains: []string{"\x1b]8;;"},
+		},
+		{
+			name:                "Markdown image with hyperlink support",
+			input:               `![Beautiful sunset](https://example.com/sunset.jpg)`,
+			hyperlinkSupported:  true,
+			expectedContains:    []string{"\x1b]8;;https://example.com/sunset.jpg\x07", "[Click here to view image: Beautiful sunset]"},
+			expectedNotContains: []string{"<https://example.com/sunset.jpg>"},
+		},
+		{
+			name:                "Markdown image without hyperlink support",
+			input:               `![Beautiful sunset](https://example.com/sunset.jpg)`,
+			hyperlinkSupported:  false,
+			expectedContains:    []string{"[Image: Beautiful sunset, https://example.com/sunset.jpg]"},
+			expectedNotContains: []string{"\x1b]8;;", "[Click here to view image:"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Save original env vars
+			origTerm := os.Getenv("TERM")
+			origTermProgram := os.Getenv("TERM_PROGRAM")
+			origVteVersion := os.Getenv("VTE_VERSION")
+			origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
+			origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+			origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
+
+			// Set env to control hyperlink support
+			if tc.hyperlinkSupported {
+				os.Setenv("TERM", "xterm-kitty")
+			} else {
+				// Clear all environment variables that could indicate hyperlink support
+				os.Setenv("TERM", "xterm")
+				os.Unsetenv("TERM_PROGRAM")
+				os.Unsetenv("VTE_VERSION")
+				os.Unsetenv("KITTY_WINDOW_ID")
+				os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+				os.Unsetenv("WEZTERM_EXECUTABLE")
+			}
+
+			processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
+
+			// Restore original env vars
+			os.Setenv("TERM", origTerm)
+			os.Setenv("TERM_PROGRAM", origTermProgram)
+			os.Setenv("VTE_VERSION", origVteVersion)
+			os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
+			os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+			os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
+
+			if err != nil {
+				t.Fatalf("ProcessBody() failed: %v", err)
+			}
+
+			// Check that expected strings are present
+			for _, expected := range tc.expectedContains {
+				if !strings.Contains(processed, expected) {
+					t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
+				}
+			}
+
+			// Check that unexpected strings are not present
+			for _, notExpected := range tc.expectedNotContains {
+				if strings.Contains(processed, notExpected) {
+					t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
+				}
+			}
+		})
+	}
+}
+
+func TestImageAndLinkFallbackBehavior(t *testing.T) {
+	h1Style := lipgloss.NewStyle()
+	h2Style := lipgloss.NewStyle()
+	bodyStyle := lipgloss.NewStyle()
+
+	testCases := []struct {
+		name                   string
+		input                  string
+		hyperlinkSupported     bool
+		imageProtocolSupported bool
+		expectedContains       []string
+		expectedNotContains    []string
+	}{
+		{
+			name:                   "Full support - both image protocol and hyperlinks",
+			input:                  `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
+			hyperlinkSupported:     true,
+			imageProtocolSupported: true,
+			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]"},
+			expectedNotContains:    []string{"<https://example.com>", "[Image:"},
+		},
+		{
+			name:                   "Hyperlink support only - no image protocol",
+			input:                  `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
+			hyperlinkSupported:     true,
+			imageProtocolSupported: false,
+			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]"},
+			expectedNotContains:    []string{"<https://example.com>", "[Image:"},
+		},
+		{
+			name:                   "No support - plain text fallback",
+			input:                  `<img src="https://example.com/image.png" alt="Test image"> and <a href="https://example.com">link</a>`,
+			hyperlinkSupported:     false,
+			imageProtocolSupported: false,
+			expectedContains:       []string{"link <https://example.com>", "[Image: Test image, https://example.com/image.png]"},
+			expectedNotContains:    []string{"\x1b]8;;", "[Click here to view image:"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Save original env vars
+			origTerm := os.Getenv("TERM")
+			origTermProgram := os.Getenv("TERM_PROGRAM")
+			origVteVersion := os.Getenv("VTE_VERSION")
+			origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
+			origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+			origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
+
+			// Set environment for hyperlink support
+			if tc.hyperlinkSupported {
+				os.Setenv("TERM", "xterm-kitty")
+			} else {
+				os.Setenv("TERM", "xterm")
+				os.Unsetenv("TERM_PROGRAM")
+				os.Unsetenv("VTE_VERSION")
+				os.Unsetenv("KITTY_WINDOW_ID")
+				os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+				os.Unsetenv("WEZTERM_EXECUTABLE")
+			}
+
+			// Set environment for image protocol support
+			if tc.imageProtocolSupported && !tc.hyperlinkSupported {
+				// This case shouldn't happen in practice, but test it anyway
+				os.Setenv("KITTY_WINDOW_ID", "1")
+			} else if !tc.imageProtocolSupported {
+				os.Unsetenv("KITTY_WINDOW_ID")
+				os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+			}
+
+			processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
+
+			// Restore original env vars
+			os.Setenv("TERM", origTerm)
+			os.Setenv("TERM_PROGRAM", origTermProgram)
+			os.Setenv("VTE_VERSION", origVteVersion)
+			os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
+			os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+			os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
+
+			if err != nil {
+				t.Fatalf("ProcessBody() failed: %v", err)
+			}
+
+			// Check that expected strings are present
+			for _, expected := range tc.expectedContains {
+				if !strings.Contains(processed, expected) {
+					t.Errorf("Expected processed body to contain %q, but it didn't.\nProcessed: %q", expected, processed)
+				}
+			}
+
+			// Check that unexpected strings are not present
+			for _, notExpected := range tc.expectedNotContains {
+				if strings.Contains(processed, notExpected) {
+					t.Errorf("Expected processed body to NOT contain %q, but it did.\nProcessed: %q", notExpected, processed)
+				}
+			}
+		})
+	}
+}