5c09f97cb3
- Export DashboardPage helper functions for testability - Add new test files: App, SharedSchedule, AppContext, UnsavedChangesContext, useUnsavedChangesWarning - Expand existing test coverage for Auth, MedDetailModal, MobileEditModal, DashboardPage, MedicationsPage, PlannerPage, and more - Add edge case and error handling tests across components, hooks, and pages
215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>)
|
|
.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<typeof vi.fn>)
|
|
.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<typeof vi.fn>)
|
|
.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<typeof vi.fn>)
|
|
.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<typeof vi.fn>).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<typeof vi.fn>)
|
|
.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);
|
|
});
|
|
});
|