diff options
Diffstat (limited to 'src/client/pages')
| -rw-r--r-- | src/client/pages/DeckDetailPage.test.tsx | 203 | ||||
| -rw-r--r-- | src/client/pages/DeckDetailPage.tsx | 45 |
2 files changed, 242 insertions, 6 deletions
diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx index de22b08..0589073 100644 --- a/src/client/pages/DeckDetailPage.test.tsx +++ b/src/client/pages/DeckDetailPage.test.tsx @@ -372,4 +372,207 @@ describe("DeckDetailPage", () => { // No description should be shown expect(screen.queryByText("Common Japanese words")).toBeNull(); }); + + describe("Delete Card", () => { + it("shows Delete button for each card", async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ deck: mockDeck }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: mockCards }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Hello")).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + expect(deleteButtons.length).toBe(2); + }); + + it("opens delete confirmation modal when Delete button is clicked", async () => { + const user = userEvent.setup(); + + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ deck: mockDeck }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: mockCards }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Hello")).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + const firstDeleteButton = deleteButtons[0]; + if (firstDeleteButton) { + await user.click(firstDeleteButton); + } + + expect(screen.getByRole("dialog")).toBeDefined(); + expect( + screen.getByRole("heading", { name: "Delete Card" }), + ).toBeDefined(); + }); + + it("closes delete modal when Cancel is clicked", async () => { + const user = userEvent.setup(); + + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ deck: mockDeck }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: mockCards }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Hello")).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + const firstDeleteButton = deleteButtons[0]; + if (firstDeleteButton) { + await user.click(firstDeleteButton); + } + + expect(screen.getByRole("dialog")).toBeDefined(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("deletes card and refreshes list on confirmation", async () => { + const user = userEvent.setup(); + + mockFetch + // Initial load + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ deck: mockDeck }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: mockCards }), + }) + // Delete request + .mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }) + // Refresh cards after deletion + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: [mockCards[1]] }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Hello")).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + const firstDeleteButton = deleteButtons[0]; + if (firstDeleteButton) { + await user.click(firstDeleteButton); + } + + // Find the Delete button in the modal (not the card list) + const modalDeleteButtons = screen.getAllByRole("button", { + name: "Delete", + }); + const confirmDeleteButton = modalDeleteButtons.find((btn) => + btn.closest('[role="dialog"]'), + ); + if (confirmDeleteButton) { + await user.click(confirmDeleteButton); + } + + // Wait for modal to close and list to refresh + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + // Verify DELETE request was made + expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-1/cards/card-1", { + method: "DELETE", + headers: { Authorization: "Bearer access-token" }, + }); + + // Verify card count updated + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Cards (1)" }), + ).toBeDefined(); + }); + }); + + it("displays error when delete fails", async () => { + const user = userEvent.setup(); + + mockFetch + // Initial load + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ deck: mockDeck }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ cards: mockCards }), + }) + // Delete request fails + .mockResolvedValueOnce({ + ok: false, + status: 500, + json: async () => ({ error: "Failed to delete card" }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Hello")).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + const firstDeleteButton = deleteButtons[0]; + if (firstDeleteButton) { + await user.click(firstDeleteButton); + } + + // Find the Delete button in the modal + const modalDeleteButtons = screen.getAllByRole("button", { + name: "Delete", + }); + const confirmDeleteButton = modalDeleteButtons.find((btn) => + btn.closest('[role="dialog"]'), + ); + if (confirmDeleteButton) { + await user.click(confirmDeleteButton); + } + + // Error should be displayed in the modal + await waitFor(() => { + expect(screen.getByRole("alert").textContent).toContain( + "Failed to delete card", + ); + }); + }); + }); }); diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx index 57e4af9..cdc216a 100644 --- a/src/client/pages/DeckDetailPage.tsx +++ b/src/client/pages/DeckDetailPage.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { Link, useParams } from "wouter"; import { ApiClientError, apiClient } from "../api"; import { CreateCardModal } from "../components/CreateCardModal"; +import { DeleteCardModal } from "../components/DeleteCardModal"; import { EditCardModal } from "../components/EditCardModal"; interface Card { @@ -38,6 +39,7 @@ export function DeckDetailPage() { const [error, setError] = useState<string | null>(null); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [editingCard, setEditingCard] = useState<Card | null>(null); + const [deletingCard, setDeletingCard] = useState<Card | null>(null); const fetchDeck = useCallback(async () => { if (!deckId) return; @@ -241,13 +243,34 @@ export function DeckDetailPage() { <span>Lapses: {card.lapses}</span> </div> </div> - <button - type="button" - onClick={() => setEditingCard(card)} - style={{ marginLeft: "1rem" }} + <div + style={{ + display: "flex", + gap: "0.5rem", + marginLeft: "1rem", + }} > - Edit - </button> + <button + type="button" + onClick={() => setEditingCard(card)} + > + Edit + </button> + <button + type="button" + onClick={() => setDeletingCard(card)} + style={{ + backgroundColor: "#dc3545", + color: "white", + border: "none", + padding: "0.5rem 1rem", + borderRadius: "4px", + cursor: "pointer", + }} + > + Delete + </button> + </div> </div> </li> ))} @@ -274,6 +297,16 @@ export function DeckDetailPage() { onCardUpdated={fetchCards} /> )} + + {deckId && ( + <DeleteCardModal + isOpen={deletingCard !== null} + deckId={deckId} + card={deletingCard} + onClose={() => setDeletingCard(null)} + onCardDeleted={fetchCards} + /> + )} </div> ); } |
