From 1521b4d5e8abe5ac53c3a652915f99120848e662 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 17:01:49 +0400 Subject: [PATCH 1/6] fix: from address added (#47) [skip ci] --- tui/email_view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui/email_view.go b/tui/email_view.go index d7eeb34c23e888bf2e6782cd4037230e5bfae758..a1c75594383d7037175a99de0aeaa6dff752f6cf 100644 --- a/tui/email_view.go +++ b/tui/email_view.go @@ -114,7 +114,7 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *EmailView) View() string { - header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject) + header := fmt.Sprintf("From: %s | Subject: %s", m.email.From, m.email.Subject) styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header) var help string From 198c8afa883908f09ada9b33c43a372baeee3547 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 17:06:59 +0400 Subject: [PATCH 2/6] add email_view test (#44) --- tui/email_view_test.go | 140 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tui/email_view_test.go diff --git a/tui/email_view_test.go b/tui/email_view_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2185370fd5dbcca85c01ed5b976f2c725fe52d7b --- /dev/null +++ b/tui/email_view_test.go @@ -0,0 +1,140 @@ +package tui + +import ( + "testing" + "time" + + "github.com/andrinoff/email-cli/fetcher" + tea "github.com/charmbracelet/bubbletea" +) + +func TestEmailViewUpdate(t *testing.T) { + emailWithAttachments := fetcher.Email{ + From: "test@example.com", + Subject: "Test Email with Attachments", + Body: "This is the body.", + Date: time.Now(), + Attachments: []fetcher.Attachment{ + {Filename: "attachment1.txt", Data: []byte("attachment1")}, + {Filename: "attachment2.txt", Data: []byte("attachment2")}, + }, + } + + emailWithoutAttachments := fetcher.Email{ + From: "test@example.com", + Subject: "Test Email without Attachments", + Body: "This is the body.", + Date: time.Now(), + } + + t.Run("Focus on attachments", func(t *testing.T) { + emailView := NewEmailView(emailWithAttachments, 80, 24) + if emailView.focusOnAttachments { + t.Error("focusOnAttachments should be initially false") + } + + // Tab to focus on attachments + model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + emailView = model.(*EmailView) + + if !emailView.focusOnAttachments { + t.Error("focusOnAttachments should be true after tabbing") + } + + // Tab back to body + model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + emailView = model.(*EmailView) + if emailView.focusOnAttachments { + t.Error("focusOnAttachments should be false after tabbing again") + } + }) + + t.Run("No focus on attachments when there are none", func(t *testing.T) { + emailView := NewEmailView(emailWithoutAttachments, 80, 24) + if emailView.focusOnAttachments { + t.Error("focusOnAttachments should be initially false") + } + // Tab + model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + emailView = model.(*EmailView) + if emailView.focusOnAttachments { + t.Error("focusOnAttachments should remain false when there are no attachments") + } + }) + + t.Run("Navigate attachments", func(t *testing.T) { + emailView := NewEmailView(emailWithAttachments, 80, 24) + // Focus on attachments + model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + emailView = model.(*EmailView) + + if emailView.attachmentCursor != 0 { + t.Errorf("Initial attachmentCursor should be 0, got %d", emailView.attachmentCursor) + } + + // Move down + model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + emailView = model.(*EmailView) + if emailView.attachmentCursor != 1 { + t.Errorf("After one down arrow, attachmentCursor should be 1, got %d", emailView.attachmentCursor) + } + + // Move down again (should not go past the end) + model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + emailView = model.(*EmailView) + if emailView.attachmentCursor != 1 { + t.Errorf("attachmentCursor should not go past the end of the list, got %d", emailView.attachmentCursor) + } + + // Move up + model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyUp}) + emailView = model.(*EmailView) + if emailView.attachmentCursor != 0 { + t.Errorf("After one up arrow, attachmentCursor should be 0, got %d", emailView.attachmentCursor) + } + }) + + t.Run("Download attachment", func(t *testing.T) { + emailView := NewEmailView(emailWithAttachments, 80, 24) + // Focus on attachments + model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + emailView = model.(*EmailView) + + // Move to the second attachment + model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + emailView = model.(*EmailView) + + // Press enter + _, cmd := emailView.Update(tea.KeyMsg{Type: tea.KeyEnter}) + if cmd == nil { + t.Fatal("Expected a command, but got nil") + } + + msg := cmd() + downloadMsg, ok := msg.(DownloadAttachmentMsg) + if !ok { + t.Fatalf("Expected a DownloadAttachmentMsg, but got %T", msg) + } + if downloadMsg.Filename != "attachment2.txt" { + t.Errorf("Expected to download 'attachment2.txt', but got '%s'", downloadMsg.Filename) + } + }) + + t.Run("Reply to email", func(t *testing.T) { + emailView := NewEmailView(emailWithAttachments, 80, 24) + + _, cmd := emailView.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")}) + if cmd == nil { + t.Fatal("Expected a command, but got nil") + } + + msg := cmd() + replyMsg, ok := msg.(ReplyToEmailMsg) + if !ok { + t.Fatalf("Expected a ReplyToEmailMsg, but got %T", msg) + } + if replyMsg.Email.Subject != emailWithAttachments.Subject { + t.Errorf("Expected reply to have subject '%s', but got '%s'", emailWithAttachments.Subject, replyMsg.Email.Subject) + } + }) +} \ No newline at end of file From 87abe1a3548ebfcdfc535b1ceb874b1ea71fcc31 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 18:02:15 +0400 Subject: [PATCH 3/6] add test for view/html (#44) --- .github/workflows/ci.yml | 3 - view/html.go | 13 ++-- view/html_test.go | 139 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 view/html_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b516bf78d703baa9a9a229b7df74d6cfbe2b0f9..305f87237596a6367d9cd6b35f324b0d61be449e 100644 --- a/.github/workflows/ci.yml +++ b/.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 ./... diff --git a/view/html.go b/view/html.go index 1cdf4afb572916be422f6132d5959d5a8f67625f..1ef610880ed0c5ba7f24b8cae3f930182e1b544f 100644 --- a/view/html.go +++ b/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 -} +} \ No newline at end of file diff --git a/view/html_test.go b/view/html_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e785914f0fd6bf26ee291fc8f90c5a998f3f4b05 --- /dev/null +++ b/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: "

Hello

", + }, + { + name: "Bold", + input: "**bold text**", + expected: "

bold text

", + }, + { + name: "Link", + input: "[link](http://example.com)", + expected: `

link

`, + }, + } + + 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: "

Hello, world!

", + expected: "Hello, world!", + }, + { + name: "With link HTML", + input: `Click here`, + expected: "Click here", + }, + { + name: "With image HTML", + input: `alt text`, + expected: "[Click here to view image: alt text]", + }, + { + name: "With headers HTML", + input: "

Header 1

", + 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) + } + }) + } +} \ No newline at end of file From 5a4b793b3e33ab2b9e78fed0b2a71842bbe36d71 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 18:02:56 +0400 Subject: [PATCH 4/6] fix: format everything --- tui/email_view_test.go | 2 +- view/html.go | 2 +- view/html_test.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tui/email_view_test.go b/tui/email_view_test.go index 2185370fd5dbcca85c01ed5b976f2c725fe52d7b..67b4ec5d39ff0a005c35bd4ba176c967e0c4956e 100644 --- a/tui/email_view_test.go +++ b/tui/email_view_test.go @@ -137,4 +137,4 @@ func TestEmailViewUpdate(t *testing.T) { t.Errorf("Expected reply to have subject '%s', but got '%s'", emailWithAttachments.Subject, replyMsg.Email.Subject) } }) -} \ No newline at end of file +} diff --git a/view/html.go b/view/html.go index 1ef610880ed0c5ba7f24b8cae3f930182e1b544f..436ddff54a7edea1392ba2a59df4314f4ac609ae 100644 --- a/view/html.go +++ b/view/html.go @@ -112,4 +112,4 @@ func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (st text = strings.TrimSpace(text) return bodyStyle.Render(text), nil -} \ No newline at end of file +} diff --git a/view/html_test.go b/view/html_test.go index e785914f0fd6bf26ee291fc8f90c5a998f3f4b05..b0d427c5c5ba427cad747d4b6e8a5f25e8239490 100644 --- a/view/html_test.go +++ b/view/html_test.go @@ -98,8 +98,8 @@ func TestProcessBody(t *testing.T) { expected: "Click here", }, { - name: "With image HTML", - input: `alt text`, + name: "With image HTML", + input: `alt text`, expected: "[Click here to view image: alt text]", }, { @@ -113,8 +113,8 @@ func TestProcessBody(t *testing.T) { expected: "Click here", }, { - name: "With image Markdown", - input: `![alt text](http://example.com/img.png)>`, + name: "With image Markdown", + input: `![alt text](http://example.com/img.png)>`, expected: "[Click here to view image: alt text]", }, { @@ -136,4 +136,4 @@ func TestProcessBody(t *testing.T) { } }) } -} \ No newline at end of file +} From a00f49e6d7e8351c2800ac32a4785f2eaf4cb412 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 18:03:57 +0400 Subject: [PATCH 5/6] fix: remove broken labeler --- .github/workflows/labeler.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/workflows/labeler.yml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 76c531859856b10f43181dc24f1e023468cc859d..0000000000000000000000000000000000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Issue Labeler -on: - issues: - types: [opened, edited] - -jobs: - label_issues: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Label issues - # Update the action to the latest version - uses: github/issue-labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - # Correct the path to your configuration file - configuration-path: .github/labeler-config.yml - not-before: "2023-01-01T00:00:00Z" # Optional: only run on new issues From 41f56d6210dfd53c7d592992e01d3ecb4662f6c3 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 4 Aug 2025 18:05:47 +0400 Subject: [PATCH 6/6] fix: release after ci --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4937152eb3b92e78c99904827dd2f4957a957ccc..2a60a3d46c8abff0dcfc296eb2f3e538884ba61c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,9 @@ name: Release on: - push: + workflow_run: + workflows: ["Go CI"] + types: [completed] branches: - master