feat: process now button

This commit is contained in:
Leon
2025-07-16 18:20:21 +02:00
parent ad0d71cd2e
commit 0e19af170d
10 changed files with 238 additions and 28 deletions

View File

@@ -1,6 +1,10 @@
"use client"
import { Button } from "@/components/ui/button"
import { Plus, Settings } from "lucide-react"
import { processEmails } from "@/lib/api"
import { Mail, Plus, Settings } from "lucide-react"
import Image from "next/image"
import { toast } from "sonner"
interface HeaderProps {
onOpenAddNewsletter: () => void
@@ -8,13 +12,35 @@ interface HeaderProps {
}
export function Header({ onOpenAddNewsletter, onOpenSettings }: HeaderProps) {
const handleProcessEmails = async () => {
try {
await processEmails()
toast.success("Email processing started successfully!")
} catch (error) {
const message =
error instanceof Error
? error.message
: "An unexpected error occurred."
console.error(error)
toast.error(message)
}
}
return (
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Image src="/logo.png" alt="LetterFeed Logo" width={48} height={48} className="rounded-lg" />
<Image
src="/logo.png"
alt="LetterFeed Logo"
width={48}
height={48}
className="rounded-lg"
/>
<div>
<h1 className="text-3xl font-bold text-gray-900">LetterFeed</h1>
<p className="text-gray-600 mt-1">Read your newsletters as RSS feeds!</p>
<p className="text-gray-600 mt-1">
Read your newsletters as RSS feeds!
</p>
</div>
</div>
@@ -24,6 +50,11 @@ export function Header({ onOpenAddNewsletter, onOpenSettings }: HeaderProps) {
Add Newsletter
</Button>
<Button variant="outline" onClick={handleProcessEmails}>
<Mail className="w-4 h-4 mr-2" />
Process Now
</Button>
<Button variant="outline" onClick={onOpenSettings}>
<Settings className="w-4 h-4 mr-2" />
Settings

View File

@@ -1,30 +1,120 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { Header } from "../Header"
import { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner"
import * as api from "@/lib/api"
jest.mock("@/lib/api")
const mockedApi = api as jest.Mocked<typeof api>
// Mock the toast functions
jest.mock("sonner", () => {
const original = jest.requireActual("sonner")
return {
...original,
toast: {
success: jest.fn(),
error: jest.fn(),
},
}
})
describe("Header", () => {
it("renders the title and description", () => {
render(<Header onOpenAddNewsletter={() => {}} onOpenSettings={() => {}} />)
const onOpenAddNewsletter = jest.fn()
const onOpenSettings = jest.fn()
const consoleError = jest.spyOn(console, "error").mockImplementation(() => {})
beforeEach(() => {
jest.clearAllMocks()
consoleError.mockClear()
})
afterAll(() => {
consoleError.mockRestore()
})
it("renders the header with title and buttons", () => {
render(
<Header
onOpenAddNewsletter={onOpenAddNewsletter}
onOpenSettings={onOpenSettings}
/>
)
expect(screen.getByText("LetterFeed")).toBeInTheDocument()
expect(screen.getByText("Read your newsletters as RSS feeds!")).toBeInTheDocument()
expect(screen.getByText("Add Newsletter")).toBeInTheDocument()
expect(screen.getByText("Settings")).toBeInTheDocument()
expect(screen.getByText("Process Now")).toBeInTheDocument()
})
it('calls onOpenAddNewsletter when "Add Newsletter" button is clicked', () => {
const handleOpenAdd = jest.fn()
render(<Header onOpenAddNewsletter={handleOpenAdd} onOpenSettings={() => {}} />)
const addButton = screen.getByRole("button", { name: /Add Newsletter/i })
fireEvent.click(addButton)
expect(handleOpenAdd).toHaveBeenCalledTimes(1)
render(
<Header
onOpenAddNewsletter={onOpenAddNewsletter}
onOpenSettings={onOpenSettings}
/>
)
fireEvent.click(screen.getByText("Add Newsletter"))
expect(onOpenAddNewsletter).toHaveBeenCalledTimes(1)
})
it('calls onOpenSettings when "Settings" button is clicked', () => {
const handleOpenSettings = jest.fn()
render(<Header onOpenAddNewsletter={() => {}} onOpenSettings={handleOpenSettings} />)
render(
<Header
onOpenAddNewsletter={onOpenAddNewsletter}
onOpenSettings={onOpenSettings}
/>
)
fireEvent.click(screen.getByText("Settings"))
expect(onOpenSettings).toHaveBeenCalledTimes(1)
})
const settingsButton = screen.getByRole("button", { name: /Settings/i })
fireEvent.click(settingsButton)
expect(handleOpenSettings).toHaveBeenCalledTimes(1)
it('calls the process emails API when "Process Now" button is clicked and shows success toast', async () => {
mockedApi.processEmails.mockResolvedValue({ message: "Success" })
render(
<>
<Header
onOpenAddNewsletter={onOpenAddNewsletter}
onOpenSettings={onOpenSettings}
/>
<Toaster />
</>
)
fireEvent.click(screen.getByText("Process Now"))
await waitFor(() => {
expect(api.processEmails).toHaveBeenCalledTimes(1)
})
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith(
"Email processing started successfully!"
)
})
})
it("shows an error toast if the process emails API call fails", async () => {
mockedApi.processEmails.mockRejectedValue(new Error("Failed to process"))
render(
<>
<Header
onOpenAddNewsletter={onOpenAddNewsletter}
onOpenSettings={onOpenSettings}
/>
<Toaster />
</>
)
fireEvent.click(screen.getByText("Process Now"))
await waitFor(() => {
expect(api.processEmails).toHaveBeenCalledTimes(1)
})
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith("Failed to process")
})
})
})

View File

@@ -0,0 +1,27 @@
"use client"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }