add test for view/html (#44)

drew created

Change summary

.github/workflows/ci.yml |   3 
view/html.go             |  13 ++-
view/html_test.go        | 139 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 148 insertions(+), 7 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -20,8 +20,5 @@ jobs:
       - name: Tidy modules
         run: go mod tidy
 
-      - name: Build
-        run: go build -v ./...
-
       - name: Test
         run: go test -v ./...

view/html.go 🔗

@@ -62,7 +62,8 @@ func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (st
 
 	doc.Find("style, script").Remove()
 
-	// Style headers
+	// Style headers by setting their text content.
+	// We use SetText so the h1/h2 tags remain in the document for spacing logic.
 	doc.Find("h1").Each(func(i int, s *goquery.Selection) {
 		s.SetText(h1Style.Render(s.Text()))
 	})
@@ -71,8 +72,9 @@ func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (st
 		s.SetText(h2Style.Render(s.Text()))
 	})
 
-	// Add newlines after block elements for better spacing
-	doc.Find("p, div").Each(func(i int, s *goquery.Selection) {
+	// Add newlines after block elements for better spacing.
+	// THIS IS THE KEY FIX: Include h1 and h2 in the selector.
+	doc.Find("p, div, h1, h2").Each(func(i int, s *goquery.Selection) {
 		s.After("\n\n")
 	})
 
@@ -106,5 +108,8 @@ func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (st
 
 	re := regexp.MustCompile(`\n{3,}`)
 	text = re.ReplaceAllString(text, "\n\n")
+
+	text = strings.TrimSpace(text)
+
 	return bodyStyle.Render(text), nil
-}
+}

view/html_test.go 🔗

@@ -0,0 +1,139 @@
+package view
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+func TestDecodeQuotedPrintable(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "Simple case",
+			input:    "Hello=2C world=21",
+			expected: "Hello, world!",
+		},
+		{
+			name:     "With soft line break",
+			input:    "This is a long line that gets wrapped=\r\n and continues here.",
+			expected: "This is a long line that gets wrapped and continues here.",
+		},
+		{
+			name:     "No encoding",
+			input:    "Just a plain string.",
+			expected: "Just a plain string.",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			decoded, err := decodeQuotedPrintable(tc.input)
+			if err != nil {
+				t.Fatalf("decodeQuotedPrintable() failed: %v", err)
+			}
+			if decoded != tc.expected {
+				t.Errorf("Expected %q, got %q", tc.expected, decoded)
+			}
+		})
+	}
+}
+
+func TestMarkdownToHTML(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "Heading",
+			input:    "# Hello",
+			expected: "<h1>Hello</h1>",
+		},
+		{
+			name:     "Bold",
+			input:    "**bold text**",
+			expected: "<p><strong>bold text</strong></p>",
+		},
+		{
+			name:     "Link",
+			input:    "[link](http://example.com)",
+			expected: `<p><a href="http://example.com">link</a></p>`,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			html := markdownToHTML([]byte(tc.input))
+			// Trim newlines for consistent comparison
+			if strings.TrimSpace(string(html)) != tc.expected {
+				t.Errorf("Expected %s, got %s", tc.expected, html)
+			}
+		})
+	}
+}
+
+func TestProcessBody(t *testing.T) {
+	h1Style := lipgloss.NewStyle().SetString("H1")
+	h2Style := lipgloss.NewStyle().SetString("H2")
+	bodyStyle := lipgloss.NewStyle().SetString("BODY")
+
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "Simple HTML",
+			input:    "<p>Hello, world!</p>",
+			expected: "Hello, world!",
+		},
+		{
+			name:     "With link HTML",
+			input:    `<a href="http://example.com">Click here</a>`,
+			expected: "Click here",
+		},
+		{
+			name: "With image HTML",
+			input: `<img src="http://example.com/img.png" alt="alt text">`,
+			expected: "[Click here to view image: alt text]",
+		},
+		{
+			name:     "With headers HTML",
+			input:    "<h1>Header 1</h1>",
+			expected: "Header 1",
+		},
+		{
+			name:     "With link Markdown",
+			input:    `[Click here](http://example.com)`,
+			expected: "Click here",
+		},
+		{
+			name: "With image Markdown",
+			input: `![alt text](http://example.com/img.png)>`,
+			expected: "[Click here to view image: alt text]",
+		},
+		{
+			name:     "With headers Markdown",
+			input:    "# Header 1",
+			expected: "Header 1",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
+			if err != nil {
+				t.Fatalf("ProcessBody() failed: %v", err)
+			}
+			// Use Contains because styles add ANSI codes
+			if !strings.Contains(processed, tc.expected) {
+				t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expected)
+			}
+		})
+	}
+}