diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:07:34 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-07 18:07:34 +0900 |
| commit | 2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7 (patch) | |
| tree | 763dc087b78d4bd5fe04a39f9721995721114705 | |
| parent | 83789dd12efe82b645445fbb46d98bcb2a003b57 (diff) | |
| download | kioku-2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7.tar.gz kioku-2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7.tar.zst kioku-2ca8bfadd49fb8e5f45b6324cff13c35a2858bb7.zip | |
test(client): add deck management integration tests
Add Edit Deck and Delete Deck integration tests to HomePage.test.tsx,
verifying modal interactions and list refresh behavior after CRUD
operations. Completes Phase 4 frontend testing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/dev/roadmap.md | 2 | ||||
| -rw-r--r-- | src/client/pages/HomePage.test.tsx | 287 |
2 files changed, 288 insertions, 1 deletions
diff --git a/docs/dev/roadmap.md b/docs/dev/roadmap.md index f159084..78325db 100644 --- a/docs/dev/roadmap.md +++ b/docs/dev/roadmap.md @@ -88,7 +88,7 @@ Smaller features first to enable early MVP validation. - [x] Create deck modal/form - [x] Edit deck - [x] Delete deck (with confirmation) -- [ ] Add tests +- [x] Add tests **✅ Milestone**: Users can create and manage decks diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx index 93351d5..4dd4a81 100644 --- a/src/client/pages/HomePage.test.tsx +++ b/src/client/pages/HomePage.test.tsx @@ -38,6 +38,10 @@ vi.mock("../api/client", () => ({ }, })); +// Mock fetch globally for Edit/Delete modals +const mockFetch = vi.fn(); +global.fetch = mockFetch; + // Helper to create mock responses compatible with Hono's ClientResponse function mockResponse(data: { ok: boolean; @@ -437,4 +441,287 @@ describe("HomePage", () => { expect(apiClient.rpc.api.decks.$get).toHaveBeenCalledTimes(2); }); }); + + describe("Edit Deck", () => { + it("shows Edit button for each deck", async () => { + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const editButtons = screen.getAllByRole("button", { name: "Edit" }); + expect(editButtons.length).toBe(2); + }); + + it("opens edit modal when Edit button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const editButtons = screen.getAllByRole("button", { name: "Edit" }); + await user.click(editButtons[0]!); + + expect(screen.getByRole("dialog")).toBeDefined(); + expect(screen.getByRole("heading", { name: "Edit Deck" })).toBeDefined(); + expect(screen.getByLabelText("Name")).toHaveProperty( + "value", + "Japanese Vocabulary", + ); + }); + + it("closes edit modal when Cancel is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const editButtons = screen.getAllByRole("button", { name: "Edit" }); + await user.click(editButtons[0]!); + + expect(screen.getByRole("dialog")).toBeDefined(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("edits deck and refreshes list", async () => { + const user = userEvent.setup(); + const updatedDeck = { + ...mockDecks[0], + name: "Updated Japanese", + }; + + vi.mocked(apiClient.rpc.api.decks.$get) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: [updatedDeck, mockDecks[1]] }), + }), + ); + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ deck: updatedDeck }), + }); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + // Click Edit on first deck + const editButtons = screen.getAllByRole("button", { name: "Edit" }); + await user.click(editButtons[0]!); + + // Update name + const nameInput = screen.getByLabelText("Name"); + await user.clear(nameInput); + await user.type(nameInput, "Updated Japanese"); + + // Save + await user.click(screen.getByRole("button", { name: "Save" })); + + // Modal should close + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + // Deck list should be refreshed with updated name + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Updated Japanese" }), + ).toBeDefined(); + }); + + // API should have been called twice (initial + refresh) + expect(apiClient.rpc.api.decks.$get).toHaveBeenCalledTimes(2); + }); + }); + + describe("Delete Deck", () => { + it("shows Delete button for each deck", async () => { + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + expect(deleteButtons.length).toBe(2); + }); + + it("opens delete modal when Delete button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + await user.click(deleteButtons[0]!); + + expect(screen.getByRole("dialog")).toBeDefined(); + expect( + screen.getByRole("heading", { name: "Delete Deck" }), + ).toBeDefined(); + // The deck name appears in both the list and the modal, so check specifically within the dialog + const dialog = screen.getByRole("dialog"); + expect(dialog.textContent).toContain("Japanese Vocabulary"); + }); + + it("closes delete modal when Cancel is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + await user.click(deleteButtons[0]!); + + expect(screen.getByRole("dialog")).toBeDefined(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("deletes deck and refreshes list", async () => { + const user = userEvent.setup(); + + vi.mocked(apiClient.rpc.api.decks.$get) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: mockDecks }), + }), + ) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: [mockDecks[1]] }), + }), + ); + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + renderWithProviders(); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeDefined(); + }); + + // Click Delete on first deck + const deleteButtons = screen.getAllByRole("button", { name: "Delete" }); + await user.click(deleteButtons[0]!); + + // Wait for modal to appear + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeDefined(); + }); + + // Confirm deletion - get the Delete button inside the dialog + const dialog = screen.getByRole("dialog"); + const dialogButtons = dialog.querySelectorAll("button"); + const deleteButton = Array.from(dialogButtons).find( + (btn) => btn.textContent === "Delete", + ); + await user.click(deleteButton!); + + // Modal should close + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + // Deck list should be refreshed without deleted deck + await waitFor(() => { + expect( + screen.queryByRole("heading", { name: "Japanese Vocabulary" }), + ).toBeNull(); + }); + expect( + screen.getByRole("heading", { name: "Spanish Verbs" }), + ).toBeDefined(); + + // API should have been called twice (initial + refresh) + expect(apiClient.rpc.api.decks.$get).toHaveBeenCalledTimes(2); + }); + }); }); |
