mirror of
https://github.com/khoaliber/LetterFeed.git
synced 2026-03-02 21:19:13 +00:00
v0.1.0
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { AddNewsletterDialog } from "../AddNewsletterDialog"
|
||||
import * as api from "@/lib/api"
|
||||
|
||||
// Mock the API module
|
||||
jest.mock("@/lib/api", () => ({
|
||||
...jest.requireActual("@/lib/api"),
|
||||
createNewsletter: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
describe("AddNewsletterDialog", () => {
|
||||
const handleOpenChange = jest.fn()
|
||||
const handleSuccess = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("allows user to fill out the form and submit", async () => {
|
||||
mockedApi.createNewsletter.mockResolvedValueOnce({
|
||||
id: 1,
|
||||
name: "My New Newsletter",
|
||||
is_active: true,
|
||||
senders: [{ id: 1, email: "test@example.com", newsletter_id: 1 }],
|
||||
entries_count: 0,
|
||||
})
|
||||
|
||||
render(<AddNewsletterDialog isOpen={true} onOpenChange={handleOpenChange} onSuccess={handleSuccess} />)
|
||||
|
||||
// Fill out the form
|
||||
fireEvent.change(screen.getByLabelText(/Newsletter Name/i), { target: { value: "My New Newsletter" } })
|
||||
fireEvent.change(screen.getByPlaceholderText(/Enter email address/i), { target: { value: "test@example.com" } })
|
||||
|
||||
// Submit the form
|
||||
fireEvent.click(screen.getByRole("button", { name: /Register Newsletter/i }))
|
||||
|
||||
// Wait for the async operation to complete
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.createNewsletter).toHaveBeenCalledWith({
|
||||
name: "My New Newsletter",
|
||||
sender_emails: ["test@example.com"],
|
||||
})
|
||||
expect(handleSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("allows adding and removing email fields", () => {
|
||||
render(<AddNewsletterDialog isOpen={true} onOpenChange={() => {}} onSuccess={() => {}} />)
|
||||
|
||||
// Initial state
|
||||
expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1)
|
||||
|
||||
// Add another email
|
||||
fireEvent.click(screen.getByRole("button", { name: /Add Another Email/i }))
|
||||
expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(2)
|
||||
|
||||
// Remove the first email
|
||||
fireEvent.click(screen.getAllByRole("button", { name: /Remove/i })[0])
|
||||
expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("closes the dialog when cancel is clicked", () => {
|
||||
render(<AddNewsletterDialog isOpen={true} onOpenChange={handleOpenChange} onSuccess={handleSuccess} />)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }))
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(handleSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { EditNewsletterDialog } from "../EditNewsletterDialog"
|
||||
import { Newsletter } from "@/lib/api"
|
||||
import * as api from "@/lib/api"
|
||||
|
||||
// Mock the API module
|
||||
jest.mock("@/lib/api", () => ({
|
||||
...jest.requireActual("@/lib/api"),
|
||||
updateNewsletter: jest.fn(),
|
||||
deleteNewsletter: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
const mockNewsletter: Newsletter = {
|
||||
id: 1,
|
||||
name: "Existing Newsletter",
|
||||
is_active: true,
|
||||
senders: [{ id: 1, email: "current@example.com", newsletter_id: 1 }],
|
||||
entries_count: 5,
|
||||
}
|
||||
|
||||
describe("EditNewsletterDialog", () => {
|
||||
const handleOpenChange = jest.fn()
|
||||
const handleSuccess = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Mock window.confirm for the delete action
|
||||
window.confirm = jest.fn(() => true)
|
||||
})
|
||||
|
||||
it("renders with initial newsletter data and allows updates", async () => {
|
||||
mockedApi.updateNewsletter.mockResolvedValueOnce({ ...mockNewsletter, name: "Updated Name" })
|
||||
|
||||
render(
|
||||
<EditNewsletterDialog
|
||||
newsletter={mockNewsletter}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
// Check that initial data is present
|
||||
const nameInput = screen.getByLabelText(/Newsletter Name/i)
|
||||
expect(nameInput).toHaveValue("Existing Newsletter")
|
||||
expect(screen.getByDisplayValue("current@example.com")).toBeInTheDocument()
|
||||
|
||||
// Update the name
|
||||
fireEvent.change(nameInput, { target: { value: "Updated Name" } })
|
||||
|
||||
// Submit
|
||||
fireEvent.click(screen.getByRole("button", { name: /Save Changes/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.updateNewsletter).toHaveBeenCalledWith(1, {
|
||||
name: "Updated Name",
|
||||
sender_emails: ["current@example.com"],
|
||||
})
|
||||
expect(handleSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("calls deleteNewsletter when delete button is clicked and confirmed", async () => {
|
||||
mockedApi.deleteNewsletter.mockResolvedValueOnce()
|
||||
|
||||
render(
|
||||
<EditNewsletterDialog
|
||||
newsletter={mockNewsletter}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Delete Newsletter/i }))
|
||||
|
||||
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete the "Existing Newsletter" newsletter?')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.deleteNewsletter).toHaveBeenCalledWith(1)
|
||||
expect(handleSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not call deleteNewsletter when delete is not confirmed", () => {
|
||||
window.confirm = jest.fn(() => false) // User clicks "Cancel"
|
||||
|
||||
render(
|
||||
<EditNewsletterDialog
|
||||
newsletter={mockNewsletter}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Delete Newsletter/i }))
|
||||
|
||||
expect(mockedApi.deleteNewsletter).not.toHaveBeenCalled()
|
||||
expect(handleSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
24
frontend/components/letterfeed/__tests__/EmptyState.test.tsx
Normal file
24
frontend/components/letterfeed/__tests__/EmptyState.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { EmptyState } from "../EmptyState"
|
||||
|
||||
describe("EmptyState", () => {
|
||||
it("renders the correct content", () => {
|
||||
render(<EmptyState onAddNewsletter={() => {}} />)
|
||||
|
||||
expect(screen.getByText("No newsletters registered")).toBeInTheDocument()
|
||||
expect(screen.getByText("Get started by adding your first newsletter")).toBeInTheDocument()
|
||||
expect(screen.getByRole("button", { name: /Add Your First Newsletter/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onAddNewsletter when the button is clicked", () => {
|
||||
const handleAddNewsletter = jest.fn()
|
||||
render(<EmptyState onAddNewsletter={handleAddNewsletter} />)
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /Add Your First Newsletter/i })
|
||||
fireEvent.click(addButton)
|
||||
|
||||
expect(handleAddNewsletter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
30
frontend/components/letterfeed/__tests__/Header.test.tsx
Normal file
30
frontend/components/letterfeed/__tests__/Header.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { Header } from "../Header"
|
||||
|
||||
describe("Header", () => {
|
||||
it("renders the title and description", () => {
|
||||
render(<Header onOpenAddNewsletter={() => {}} onOpenSettings={() => {}} />)
|
||||
expect(screen.getByText("LetterFeed")).toBeInTheDocument()
|
||||
expect(screen.getByText("Read your newsletters as RSS feeds!")).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)
|
||||
})
|
||||
|
||||
it('calls onOpenSettings when "Settings" button is clicked', () => {
|
||||
const handleOpenSettings = jest.fn()
|
||||
render(<Header onOpenAddNewsletter={() => {}} onOpenSettings={handleOpenSettings} />)
|
||||
|
||||
const settingsButton = screen.getByRole("button", { name: /Settings/i })
|
||||
fireEvent.click(settingsButton)
|
||||
expect(handleOpenSettings).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { LoadingSpinner } from "../LoadingSpinner"
|
||||
|
||||
describe("LoadingSpinner", () => {
|
||||
it("renders the spinner with the correct animation class", () => {
|
||||
render(<LoadingSpinner />)
|
||||
const spinner = screen.getByTestId("loading-spinner")
|
||||
expect(spinner).toBeInTheDocument()
|
||||
expect(spinner).toHaveClass("animate-spin")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { NewsletterCard } from "../NewsletterCard"
|
||||
import { Newsletter } from "@/lib/api"
|
||||
|
||||
// Mock the getFeedUrl function
|
||||
jest.mock("@/lib/api", () => ({
|
||||
...jest.requireActual("@/lib/api"), // import and retain all actual implementations
|
||||
getFeedUrl: jest.fn((id) => `http://mock-api/feeds/${id}`),
|
||||
}))
|
||||
|
||||
const mockNewsletter: Newsletter = {
|
||||
id: 1,
|
||||
name: "Tech Weekly",
|
||||
is_active: true,
|
||||
senders: [
|
||||
{ id: 1, email: "contact@techweekly.com", newsletter_id: 1 },
|
||||
{ id: 2, email: "updates@techweekly.com", newsletter_id: 1 },
|
||||
],
|
||||
entries_count: 42,
|
||||
}
|
||||
|
||||
describe("NewsletterCard", () => {
|
||||
it("renders newsletter details correctly", () => {
|
||||
const handleEdit = jest.fn()
|
||||
render(<NewsletterCard newsletter={mockNewsletter} onEdit={handleEdit} />)
|
||||
|
||||
// Check for the title
|
||||
expect(screen.getByText("Tech Weekly")).toBeInTheDocument()
|
||||
|
||||
// Check for the entries count
|
||||
expect(screen.getByText("42 entries")).toBeInTheDocument()
|
||||
|
||||
// Check for sender emails
|
||||
expect(screen.getByText("contact@techweekly.com")).toBeInTheDocument()
|
||||
expect(screen.getByText("updates@techweekly.com")).toBeInTheDocument()
|
||||
|
||||
// Check for the RSS feed link
|
||||
const feedLink = screen.getByRole("link")
|
||||
expect(feedLink).toHaveAttribute("href", "http://mock-api/feeds/1")
|
||||
expect(feedLink).toHaveTextContent("http://mock-api/feeds/1")
|
||||
})
|
||||
|
||||
it('calls the onEdit function with the correct newsletter when the edit button is clicked', () => {
|
||||
const handleEdit = jest.fn()
|
||||
render(<NewsletterCard newsletter={mockNewsletter} onEdit={handleEdit} />)
|
||||
|
||||
const editButton = screen.getByRole("button", { name: /edit newsletter/i })
|
||||
fireEvent.click(editButton)
|
||||
|
||||
expect(handleEdit).toHaveBeenCalledTimes(1)
|
||||
expect(handleEdit).toHaveBeenCalledWith(mockNewsletter)
|
||||
})
|
||||
|
||||
it('displays "entry" for a single entry', () => {
|
||||
const singleEntryNewsletter = { ...mockNewsletter, entries_count: 1 }
|
||||
const handleEdit = jest.fn()
|
||||
render(<NewsletterCard newsletter={singleEntryNewsletter} onEdit={handleEdit} />)
|
||||
|
||||
expect(screen.getByText("1 entry")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { NewsletterList } from "../NewsletterList"
|
||||
import { Newsletter } from "@/lib/api"
|
||||
|
||||
// Mock the child component to isolate the list component's behavior
|
||||
jest.mock("../NewsletterCard", () => ({
|
||||
NewsletterCard: ({ newsletter }: { newsletter: Newsletter }) => (
|
||||
<div data-testid={`newsletter-card-${newsletter.id}`}>{newsletter.name}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockNewsletters: Newsletter[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Newsletter One",
|
||||
is_active: true,
|
||||
senders: [],
|
||||
entries_count: 10,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Newsletter Two",
|
||||
is_active: true,
|
||||
senders: [],
|
||||
entries_count: 5,
|
||||
},
|
||||
]
|
||||
|
||||
describe("NewsletterList", () => {
|
||||
it("renders a list of newsletter cards", () => {
|
||||
render(<NewsletterList newsletters={mockNewsletters} onEditNewsletter={() => {}} />)
|
||||
|
||||
// Check that both newsletters are rendered
|
||||
expect(screen.getByText("Newsletter One")).toBeInTheDocument()
|
||||
expect(screen.getByText("Newsletter Two")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("newsletter-card-1")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("newsletter-card-2")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders nothing when the newsletter list is empty", () => {
|
||||
const { container } = render(<NewsletterList newsletters={[]} onEditNewsletter={() => {}} />)
|
||||
// The main div should be empty
|
||||
expect(container.firstChild).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
131
frontend/components/letterfeed/__tests__/SettingsDialog.test.tsx
Normal file
131
frontend/components/letterfeed/__tests__/SettingsDialog.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from "react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import { SettingsDialog } from "../SettingsDialog"
|
||||
import { Settings } from "@/lib/api"
|
||||
import * as api from "@/lib/api"
|
||||
|
||||
// Mock the API module
|
||||
jest.mock("@/lib/api", () => ({
|
||||
...jest.requireActual("@/lib/api"),
|
||||
updateSettings: jest.fn(),
|
||||
testImapConnection: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
const mockSettings: Settings = {
|
||||
id: 1,
|
||||
imap_server: "imap.example.com",
|
||||
imap_username: "user@example.com",
|
||||
search_folder: "INBOX",
|
||||
move_to_folder: "Archive",
|
||||
mark_as_read: true,
|
||||
email_check_interval: 30,
|
||||
auto_add_new_senders: false,
|
||||
locked_fields: ["imap_server"], // Mock a locked field
|
||||
}
|
||||
|
||||
const mockFolderOptions = ["INBOX", "Sent", "Archive", "Spam"]
|
||||
|
||||
describe("SettingsDialog", () => {
|
||||
const handleOpenChange = jest.fn()
|
||||
const handleSuccess = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders settings and respects locked fields", () => {
|
||||
render(
|
||||
<SettingsDialog
|
||||
settings={mockSettings}
|
||||
folderOptions={mockFolderOptions}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
// Check that a locked field is disabled
|
||||
expect(screen.getByLabelText(/IMAP Server/i)).toBeDisabled()
|
||||
// Check that a non-locked field is enabled
|
||||
expect(screen.getByLabelText(/IMAP Username/i)).toBeEnabled()
|
||||
|
||||
// Check that values are set correctly
|
||||
expect(screen.getByLabelText(/IMAP Username/i)).toHaveValue(mockSettings.imap_username)
|
||||
expect(screen.getByLabelText(/Mark emails as read/i)).toBeChecked()
|
||||
})
|
||||
|
||||
it("allows updating and saving settings", async () => {
|
||||
mockedApi.updateSettings.mockResolvedValueOnce(mockSettings)
|
||||
|
||||
render(
|
||||
<SettingsDialog
|
||||
settings={mockSettings}
|
||||
folderOptions={mockFolderOptions}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
// Change a setting
|
||||
fireEvent.change(screen.getByLabelText(/IMAP Username/i), { target: { value: "new.user@example.com" } })
|
||||
|
||||
// Save
|
||||
fireEvent.click(screen.getByRole("button", { name: /Save Settings/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
imap_username: "new.user@example.com",
|
||||
})
|
||||
)
|
||||
expect(handleSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("handles successful connection test", async () => {
|
||||
mockedApi.testImapConnection.mockResolvedValueOnce({ message: "Connection successful!" })
|
||||
|
||||
render(
|
||||
<SettingsDialog
|
||||
settings={mockSettings}
|
||||
folderOptions={mockFolderOptions}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Test Connection/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Connection successful!")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("connection-status")).toHaveClass("text-green-600")
|
||||
})
|
||||
})
|
||||
|
||||
it("handles failed connection test", async () => {
|
||||
mockedApi.testImapConnection.mockRejectedValueOnce(new Error("Authentication failed"))
|
||||
|
||||
render(
|
||||
<SettingsDialog
|
||||
settings={mockSettings}
|
||||
folderOptions={mockFolderOptions}
|
||||
isOpen={true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Test Connection/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Authentication failed")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("connection-status")).toHaveClass("text-red-600")
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user