import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { renderHook, waitFor, act } from "@testing-library/react"; import React from "react"; import { useJsonApiMutation } from "../useJsonApiMutation"; import { MockJsonApiProvider } from "../../../testing/providers/MockJsonApiProvider"; import { createMockModule, createMockResponse, createMockApiData } from "../../../testing"; // Mock the JsonApiRequest module vi.mock("../../../unified/JsonApiRequest", () => ({ JsonApiPost: vi.fn(), JsonApiPut: vi.fn(), JsonApiPatch: vi.fn(), JsonApiDelete: vi.fn(), })); describe("useJsonApiMutation", () => { const mockModule = createMockModule({ name: "articles" }); const mockData = createMockApiData({ type: "articles", id: "1", attributes: { title: "Created Article" }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.resetModules(); }); describe("POST mutations", () => { it("should initialize with idle state", () => { const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); expect(result.current.loading).toBe(false); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.response).toBeNull(); }); it("should execute POST mutation", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiPost as any).mockResolvedValue(mockResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); let mutationResult: any; await act(async () => { mutationResult = await result.current.mutate({ endpoint: "/articles", body: { title: "New Article" }, }); }); expect(result.current.data).toBe(mockData); expect(result.current.error).toBeNull(); expect(mutationResult).toBe(mockData); expect(JsonApiPost).toHaveBeenCalled(); }); it("should handle POST errors", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); const errorResponse = createMockResponse({ ok: false, response: 400, error: "Validation failed", }); (JsonApiPost as any).mockResolvedValue(errorResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); let mutationResult: any; await act(async () => { mutationResult = await result.current.mutate({ endpoint: "/articles", body: {}, }); }); expect(result.current.data).toBeNull(); expect(result.current.error).toBe("Validation failed"); expect(mutationResult).toBeNull(); }); it("should call onSuccess callback", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiPost as any).mockResolvedValue(mockResponse); const onSuccess = vi.fn(); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, onSuccess, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles", body: { title: "New" }, }); }); expect(onSuccess).toHaveBeenCalledWith(mockData); }); it("should call onError callback", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); const errorResponse = createMockResponse({ ok: false, error: "Failed", }); (JsonApiPost as any).mockResolvedValue(errorResponse); const onError = vi.fn(); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, onError, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles", body: {}, }); }); expect(onError).toHaveBeenCalledWith("Failed"); }); }); describe("PUT mutations", () => { it("should execute PUT mutation", async () => { const { JsonApiPut } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiPut as any).mockResolvedValue(mockResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "PUT", classKey: mockModule, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles/1", body: { title: "Updated" }, }); }); expect(JsonApiPut).toHaveBeenCalled(); expect(result.current.data).toBe(mockData); }); }); describe("PATCH mutations", () => { it("should execute PATCH mutation", async () => { const { JsonApiPatch } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiPatch as any).mockResolvedValue(mockResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "PATCH", classKey: mockModule, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles/1", body: { title: "Patched" }, }); }); expect(JsonApiPatch).toHaveBeenCalled(); expect(result.current.data).toBe(mockData); }); }); describe("DELETE mutations", () => { it("should execute DELETE mutation", async () => { const { JsonApiDelete } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiDelete as any).mockResolvedValue(mockResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "DELETE", classKey: mockModule, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles/1", }); }); expect(JsonApiDelete).toHaveBeenCalled(); }); }); describe("reset", () => { it("should reset state", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); const mockResponse = createMockResponse({ data: mockData, ok: true }); (JsonApiPost as any).mockResolvedValue(mockResponse); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles", body: {}, }); }); expect(result.current.data).not.toBeNull(); act(() => { result.current.reset(); }); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.response).toBeNull(); expect(result.current.loading).toBe(false); }); }); describe("loading state", () => { it("should set loading during mutation", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); let resolvePromise: any; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; }); (JsonApiPost as any).mockReturnValue(pendingPromise); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); // Start mutation without awaiting act(() => { result.current.mutate({ endpoint: "/articles", body: {}, }); }); // Should be loading expect(result.current.loading).toBe(true); // Resolve the promise await act(async () => { resolvePromise(createMockResponse({ data: mockData, ok: true })); }); // Loading should be false after completion await waitFor(() => { expect(result.current.loading).toBe(false); }); }); }); describe("error handling", () => { it("should handle thrown exceptions", async () => { const { JsonApiPost } = await import("../../../unified/JsonApiRequest"); (JsonApiPost as any).mockRejectedValue(new Error("Network error")); const { result } = renderHook( () => useJsonApiMutation({ method: "POST", classKey: mockModule, }), { wrapper }, ); await act(async () => { await result.current.mutate({ endpoint: "/articles", body: {}, }); }); expect(result.current.error).toBe("Network error"); expect(result.current.data).toBeNull(); }); }); });