diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-07 17:51:34 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-07 17:51:34 +0900 |
| commit | 020803f7dfd094dcf5157943644a28d601629b35 (patch) | |
| tree | 9d89864d0f0a03f5b5f50504a767da4edf3efa2e /src/client/pages/HomePage.test.tsx | |
| parent | ef40cc0f3b1b3013046820b84e8482f1c6a29533 (diff) | |
| download | kioku-020803f7dfd094dcf5157943644a28d601629b35.tar.gz kioku-020803f7dfd094dcf5157943644a28d601629b35.tar.zst kioku-020803f7dfd094dcf5157943644a28d601629b35.zip | |
feat(client): add create deck modal with form validation
Add CreateDeckModal component that allows users to create new decks
with name and optional description fields. Integrates with HomePage
via a "Create Deck" button that opens the modal, and refreshes the
deck list after successful creation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/pages/HomePage.test.tsx')
| -rw-r--r-- | src/client/pages/HomePage.test.tsx | 155 |
1 files changed, 152 insertions, 3 deletions
diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx index f471924..93351d5 100644 --- a/src/client/pages/HomePage.test.tsx +++ b/src/client/pages/HomePage.test.tsx @@ -21,6 +21,7 @@ vi.mock("../api/client", () => ({ api: { decks: { $get: vi.fn(), + $post: vi.fn(), }, }, }, @@ -38,9 +39,26 @@ vi.mock("../api/client", () => ({ })); // Helper to create mock responses compatible with Hono's ClientResponse -// biome-ignore lint/suspicious/noExplicitAny: Test helper needs flexible typing -function mockResponse(data: { ok: boolean; status?: number; json: () => Promise<any> }) { - return data as unknown as Awaited<ReturnType<typeof apiClient.rpc.api.decks.$get>>; +function mockResponse(data: { + ok: boolean; + status?: number; + // biome-ignore lint/suspicious/noExplicitAny: Test helper needs flexible typing + json: () => Promise<any>; +}) { + return data as unknown as Awaited< + ReturnType<typeof apiClient.rpc.api.decks.$get> + >; +} + +function mockPostResponse(data: { + ok: boolean; + status?: number; + // biome-ignore lint/suspicious/noExplicitAny: Test helper needs flexible typing + json: () => Promise<any>; +}) { + return data as unknown as Awaited< + ReturnType<typeof apiClient.rpc.api.decks.$post> + >; } const mockDecks = [ @@ -288,4 +306,135 @@ describe("HomePage", () => { }); }); }); + + describe("Create Deck", () => { + it("shows Create Deck button", async () => { + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: [] }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.queryByText("Loading decks...")).toBeNull(); + }); + + expect(screen.getByRole("button", { name: "Create Deck" })).toBeDefined(); + }); + + it("opens modal when Create Deck button is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: [] }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.queryByText("Loading decks...")).toBeNull(); + }); + + await user.click(screen.getByRole("button", { name: "Create Deck" })); + + expect(screen.getByRole("dialog")).toBeDefined(); + expect( + screen.getByRole("heading", { name: "Create New Deck" }), + ).toBeDefined(); + }); + + it("closes modal when Cancel is clicked", async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue( + mockResponse({ + ok: true, + json: async () => ({ decks: [] }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.queryByText("Loading decks...")).toBeNull(); + }); + + await user.click(screen.getByRole("button", { name: "Create Deck" })); + expect(screen.getByRole("dialog")).toBeDefined(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("creates deck and refreshes list", async () => { + const user = userEvent.setup(); + const newDeck = { + id: "deck-new", + name: "New Deck", + description: "A new deck", + newCardsPerDay: 20, + createdAt: "2024-01-03T00:00:00Z", + updatedAt: "2024-01-03T00:00:00Z", + }; + + vi.mocked(apiClient.rpc.api.decks.$get) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: [] }), + }), + ) + .mockResolvedValueOnce( + mockResponse({ + ok: true, + json: async () => ({ decks: [newDeck] }), + }), + ); + + vi.mocked(apiClient.rpc.api.decks.$post).mockResolvedValue( + mockPostResponse({ + ok: true, + json: async () => ({ deck: newDeck }), + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.queryByText("Loading decks...")).toBeNull(); + }); + + // Open modal + await user.click(screen.getByRole("button", { name: "Create Deck" })); + + // Fill in form + await user.type(screen.getByLabelText("Name"), "New Deck"); + await user.type( + screen.getByLabelText("Description (optional)"), + "A new deck", + ); + + // Submit + await user.click(screen.getByRole("button", { name: "Create" })); + + // Modal should close + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + // Deck list should be refreshed with new deck + await waitFor(() => { + expect(screen.getByRole("heading", { name: "New Deck" })).toBeDefined(); + }); + expect(screen.getByText("A new deck")).toBeDefined(); + + // API should have been called twice (initial + refresh) + expect(apiClient.rpc.api.decks.$get).toHaveBeenCalledTimes(2); + }); + }); }); |
