From 2fb6471a685bec1433be3335f377a1a2313e4820 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 1 Jan 2026 23:44:50 +0900 Subject: refactor(client): migrate API calls to typed RPC client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw fetch() calls with apiClient.rpc typed client across all modal and page components. This provides better type safety and eliminates manual auth header handling. - Make handleResponse public for component usage - Update all component tests to mock RPC methods instead of fetch - Change POSTGRES_HOST default to kioku-db for Docker compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/client/components/EditDeckModal.test.tsx | 143 +++++++++------------------ 1 file changed, 48 insertions(+), 95 deletions(-) (limited to 'src/client/components/EditDeckModal.test.tsx') diff --git a/src/client/components/EditDeckModal.test.tsx b/src/client/components/EditDeckModal.test.tsx index c627dd5..fce17f6 100644 --- a/src/client/components/EditDeckModal.test.tsx +++ b/src/client/components/EditDeckModal.test.tsx @@ -4,11 +4,22 @@ import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { apiClient } from "../api/client"; + +const mockPut = vi.fn(); +const mockHandleResponse = vi.fn(); vi.mock("../api/client", () => ({ apiClient: { - getAuthHeader: vi.fn(), + rpc: { + api: { + decks: { + ":id": { + $put: (args: unknown) => mockPut(args), + }, + }, + }, + }, + handleResponse: (res: unknown) => mockHandleResponse(res), }, ApiClientError: class ApiClientError extends Error { constructor( @@ -22,13 +33,10 @@ vi.mock("../api/client", () => ({ }, })); +import { ApiClientError } from "../api/client"; // Import after mock is set up import { EditDeckModal } from "./EditDeckModal"; -// Mock fetch globally -const mockFetch = vi.fn(); -global.fetch = mockFetch; - describe("EditDeckModal", () => { const mockDeck = { id: "deck-123", @@ -46,8 +54,14 @@ describe("EditDeckModal", () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(apiClient.getAuthHeader).mockReturnValue({ - Authorization: "Bearer access-token", + mockPut.mockResolvedValue({ ok: true }); + mockHandleResponse.mockResolvedValue({ + deck: { + id: "deck-123", + name: "Test Deck", + description: "Test description", + newCardsPerDay: 20, + }, }); }); @@ -155,18 +169,6 @@ describe("EditDeckModal", () => { const onClose = vi.fn(); const onDeckUpdated = vi.fn(); - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - deck: { - id: "deck-123", - name: "Updated Deck", - description: "Test description", - newCardsPerDay: 20, - }, - }), - }); - render( { await user.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-123", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer access-token", - }, - body: JSON.stringify({ + expect(mockPut).toHaveBeenCalledWith({ + param: { id: "deck-123" }, + json: { name: "Updated Deck", description: "Test description", - }), + }, }); }); @@ -204,18 +202,6 @@ describe("EditDeckModal", () => { const onClose = vi.fn(); const onDeckUpdated = vi.fn(); - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - deck: { - id: "deck-123", - name: "Test Deck", - description: "New description", - newCardsPerDay: 20, - }, - }), - }); - render( { await user.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-123", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer access-token", - }, - body: JSON.stringify({ + expect(mockPut).toHaveBeenCalledWith({ + param: { id: "deck-123" }, + json: { name: "Test Deck", description: "New description", - }), + }, }); }); @@ -251,18 +233,6 @@ describe("EditDeckModal", () => { it("clears description when input is emptied", async () => { const user = userEvent.setup(); - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - deck: { - id: "deck-123", - name: "Test Deck", - description: null, - newCardsPerDay: 20, - }, - }), - }); - render(); const descInput = screen.getByLabelText("Description (optional)"); @@ -270,16 +240,12 @@ describe("EditDeckModal", () => { await user.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-123", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer access-token", - }, - body: JSON.stringify({ + expect(mockPut).toHaveBeenCalledWith({ + param: { id: "deck-123" }, + json: { name: "Test Deck", description: null, - }), + }, }); }); }); @@ -287,11 +253,6 @@ describe("EditDeckModal", () => { it("trims whitespace from name and description", async () => { const user = userEvent.setup(); - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ deck: { id: "deck-123" } }), - }); - const deckWithWhitespace = { ...mockDeck, name: " Deck ", @@ -302,16 +263,12 @@ describe("EditDeckModal", () => { await user.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-123", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer access-token", - }, - body: JSON.stringify({ + expect(mockPut).toHaveBeenCalledWith({ + param: { id: "deck-123" }, + json: { name: "Deck", description: "Description", - }), + }, }); }); }); @@ -319,7 +276,7 @@ describe("EditDeckModal", () => { it("shows loading state during submission", async () => { const user = userEvent.setup(); - mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves + mockPut.mockImplementation(() => new Promise(() => {})); // Never resolves render(); @@ -344,11 +301,9 @@ describe("EditDeckModal", () => { it("displays API error message", async () => { const user = userEvent.setup(); - mockFetch.mockResolvedValue({ - ok: false, - status: 400, - json: async () => ({ error: "Deck name already exists" }), - }); + mockHandleResponse.mockRejectedValue( + new ApiClientError("Deck name already exists", 400), + ); render(); @@ -364,7 +319,7 @@ describe("EditDeckModal", () => { it("displays generic error on unexpected failure", async () => { const user = userEvent.setup(); - mockFetch.mockRejectedValue(new Error("Network error")); + mockPut.mockRejectedValue(new Error("Network error")); render(); @@ -377,10 +332,12 @@ describe("EditDeckModal", () => { }); }); - it("displays error when not authenticated", async () => { + it("displays error when handleResponse throws", async () => { const user = userEvent.setup(); - vi.mocked(apiClient.getAuthHeader).mockReturnValue(undefined); + mockHandleResponse.mockRejectedValue( + new ApiClientError("Not authenticated", 401), + ); render(); @@ -419,11 +376,7 @@ describe("EditDeckModal", () => { const user = userEvent.setup(); const onClose = vi.fn(); - mockFetch.mockResolvedValue({ - ok: false, - status: 400, - json: async () => ({ error: "Some error" }), - }); + mockHandleResponse.mockRejectedValue(new ApiClientError("Some error", 400)); const { rerender } = render( , -- cgit v1.2.3-70-g09d2