From 09e942c740160cef34e1c1949e01b916da6a3b51 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Sun, 4 Jan 2026 18:05:58 +0400 Subject: [PATCH] fix(view): test fixed for the new hyperlink and image support (#92) (#94) --- view/html.go | 5 +- view/html_test.go | 478 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 381 insertions(+), 102 deletions(-) diff --git a/view/html.go b/view/html.go index bf206bcb8fa8c81095ecf0899ecd1f1148e3b8d6..31c0fdd7cbdfa706f67829e454c5c58a31f3b43a 100644 --- a/view/html.go +++ b/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("<%s>", url) } - return fmt.Sprintf("%s <%s>", text, url) + return fmt.Sprintf("%s <%s>", text, url) } } diff --git a/view/html_test.go b/view/html_test.go index 1d784cadce500e0ee7690867f01c39fb33302e39..d74ae506b6c6e38a4921ff0d33734eb43610e234 100644 --- a/view/html_test.go +++ b/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 <https://example.com>", }, { - 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: "<https://example.com>", + }, + { + name: "Unsupported terminal with empty text", + url: "https://example.com", + text: "", + hyperlinkSupported: false, + expected: "<https://example.com>", }, } 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: `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: `Click here`, + hyperlinkSupported: true, + expectedContains: []string{"\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07"}, + expectedNotContains: []string{""}, + }, + { + name: "Link without hyperlink support", + input: `Click here`, + hyperlinkSupported: false, + expectedContains: []string{"Click here "}, + expectedNotContains: []string{"\x1b]8;;"}, + }, + { + name: "Image with hyperlink support", + input: `Test image`, + hyperlinkSupported: true, + expectedContains: []string{"\x1b]8;;https://example.com/image.png\x07", "[Click here to view image: Test image]"}, + expectedNotContains: []string{""}, + }, + { + name: "Image without hyperlink support", + input: `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{""}, + }, + { + name: "Markdown link without hyperlink support", + input: `[Visit our site](https://example.com)`, + hyperlinkSupported: false, + expectedContains: []string{"Visit our site "}, + 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{""}, + }, + { + 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: `Test image and link`, + 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{"", "[Image:"}, + }, + { + name: "Hyperlink support only - no image protocol", + input: `Test image and link`, + 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{"", "[Image:"}, + }, + { + name: "No support - plain text fallback", + input: `Test image and link`, + hyperlinkSupported: false, + imageProtocolSupported: false, + expectedContains: []string{"link ", "[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) + } + } + }) + } +}