import { act, renderHook, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useMedications } from "../../hooks/useMedications"; describe("useMedications", () => { beforeEach(() => { vi.clearAllMocks(); (global.fetch as ReturnType).mockResolvedValue({ ok: true, json: () => Promise.resolve([]), }); }); afterEach(() => { vi.clearAllMocks(); }); it("initializes with empty state", () => { const { result } = renderHook(() => useMedications()); expect(result.current.meds).toEqual([]); expect(result.current.loading).toBe(false); expect(result.current.saving).toBe(false); expect(result.current.uploadingImage).toBe(false); }); it("loads medications from API", async () => { const mockMeds = [{ id: 1, name: "TestMed", packCount: 1 }]; (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockMeds), }); const { result } = renderHook(() => useMedications()); act(() => { result.current.loadMeds(); }); expect(result.current.loading).toBe(true); await waitFor(() => { expect(result.current.meds).toEqual(mockMeds); }); expect(fetch).toHaveBeenCalledWith("/api/medications", { credentials: "include" }); }); it("handles API error gracefully", async () => { (global.fetch as ReturnType).mockRejectedValueOnce(new Error("Network error")); const { result } = renderHook(() => useMedications()); act(() => { result.current.loadMeds(); }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.meds).toEqual([]); }); it("handles non-array response", async () => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ not: "array" }), }); const { result } = renderHook(() => useMedications()); act(() => { result.current.loadMeds(); }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.meds).toEqual([]); }); it("deletes medication", async () => { const mockMeds = [{ id: 1, name: "TestMed" }]; (global.fetch as ReturnType) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockMeds) }) .mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) }); const mockResetForm = vi.fn(); const { result } = renderHook(() => useMedications()); // First load meds act(() => { result.current.loadMeds(); }); await waitFor(() => { expect(result.current.meds).toEqual(mockMeds); }); // Then delete await act(async () => { await result.current.deleteMed(1, 1, mockResetForm); }); expect(fetch).toHaveBeenCalledWith("/api/medications/1", { method: "DELETE", credentials: "include" }); expect(mockResetForm).toHaveBeenCalled(); }); it("still reloads medications when delete request fails", async () => { (global.fetch as ReturnType) .mockRejectedValueOnce(new Error("Delete failed")) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) }); const mockResetForm = vi.fn(); const { result } = renderHook(() => useMedications()); await act(async () => { await result.current.deleteMed(5, 5, mockResetForm); }); expect(fetch).toHaveBeenCalledWith("/api/medications/5", { method: "DELETE", credentials: "include" }); expect(fetch).toHaveBeenCalledWith("/api/medications", { credentials: "include" }); expect(mockResetForm).toHaveBeenCalled(); }); it("does not call resetForm if editingId does not match", async () => { (global.fetch as ReturnType) .mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) }); const mockResetForm = vi.fn(); const { result } = renderHook(() => useMedications()); await act(async () => { await result.current.deleteMed(1, 2, mockResetForm); }); expect(mockResetForm).not.toHaveBeenCalled(); }); it("uploads medication image", async () => { (global.fetch as ReturnType) .mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) }); const { result } = renderHook(() => useMedications()); const file = new File(["test"], "test.jpg", { type: "image/jpeg" }); await act(async () => { await result.current.uploadMedImage(1, file); }); expect(fetch).toHaveBeenCalledWith( "/api/medications/1/image", expect.objectContaining({ method: "POST", body: expect.any(FormData), }) ); }); it("handles image upload error", async () => { (global.fetch as ReturnType).mockRejectedValueOnce(new Error("Upload failed")); const { result } = renderHook(() => useMedications()); const file = new File(["test"], "test.jpg", { type: "image/jpeg" }); await act(async () => { await result.current.uploadMedImage(1, file); }); expect(result.current.uploadingImage).toBe(false); }); it("deletes medication image", async () => { (global.fetch as ReturnType) .mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) }); const { result } = renderHook(() => useMedications()); await act(async () => { await result.current.deleteMedImage(1); }); expect(fetch).toHaveBeenCalledWith("/api/medications/1/image", { method: "DELETE", credentials: "include" }); }); it("allows setting meds directly", () => { const { result } = renderHook(() => useMedications()); const newMeds = [{ id: 1, name: "NewMed" }] as any; act(() => { result.current.setMeds(newMeds); }); expect(result.current.meds).toEqual(newMeds); }); it("allows setting saving state", () => { const { result } = renderHook(() => useMedications()); act(() => { result.current.setSaving(true); }); expect(result.current.saving).toBe(true); }); });