import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; import { expect, test, vi, beforeEach, afterEach, describe } from "vitest"; import Home from "./page"; // Mock next/navigation const mockPush = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush, }), })); // Default mock values let mockUser: { id: number; email: string; roles: string[]; permissions: string[] } | null = { id: 1, email: "test@example.com", roles: ["regular"], permissions: ["view_counter", "increment_counter", "use_sum"], }; let mockIsLoading = false; const mockLogout = vi.fn(); const mockHasPermission = vi.fn((permission: string) => mockUser?.permissions.includes(permission) ?? false ); const mockHasRole = vi.fn((role: string) => mockUser?.roles.includes(role) ?? false ); vi.mock("./auth-context", () => ({ useAuth: () => ({ user: mockUser, isLoading: mockIsLoading, logout: mockLogout, hasPermission: mockHasPermission, hasRole: mockHasRole, }), Permission: { VIEW_COUNTER: "view_counter", INCREMENT_COUNTER: "increment_counter", USE_SUM: "use_sum", VIEW_AUDIT: "view_audit", }, })); beforeEach(() => { vi.clearAllMocks(); // Reset to authenticated state mockUser = { id: 1, email: "test@example.com", roles: ["regular"], permissions: ["view_counter", "increment_counter", "use_sum"], }; mockIsLoading = false; mockHasPermission.mockImplementation((permission: string) => mockUser?.permissions.includes(permission) ?? false ); mockHasRole.mockImplementation((role: string) => mockUser?.roles.includes(role) ?? false ); }); afterEach(() => { cleanup(); }); describe("Home - Authenticated", () => { test("renders loading state when isLoading is true", () => { mockIsLoading = true; vi.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {})); render(); expect(screen.getByText("Loading...")).toBeDefined(); }); test("renders user email in header", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 42 }), } as Response); render(); expect(screen.getByText("test@example.com")).toBeDefined(); }); test("renders sign out button", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 42 }), } as Response); render(); expect(screen.getByText("Sign out")).toBeDefined(); }); test("clicking sign out calls logout and redirects", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 42 }), } as Response); render(); fireEvent.click(screen.getByText("Sign out")); await waitFor(() => { expect(mockLogout).toHaveBeenCalled(); expect(mockPush).toHaveBeenCalledWith("/login"); }); }); test("renders counter value after fetch", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 42 }), } as Response); render(); await waitFor(() => { expect(screen.getByText("42")).toBeDefined(); }); }); test("fetches counter with credentials", async () => { const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 0 }), } as Response); render(); await waitFor(() => { expect(fetchSpy).toHaveBeenCalledWith( "http://localhost:8000/api/counter", expect.objectContaining({ credentials: "include", }) ); }); }); test("renders increment button", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ ok: true, json: () => Promise.resolve({ value: 0 }), } as Response); render(); expect(screen.getByText("Increment")).toBeDefined(); }); test("clicking increment button calls API with credentials", async () => { const fetchSpy = vi .spyOn(global, "fetch") .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ value: 0 }) } as Response) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ value: 1 }) } as Response); render(); await waitFor(() => expect(screen.getByText("0")).toBeDefined()); fireEvent.click(screen.getByText("Increment")); await waitFor(() => { expect(fetchSpy).toHaveBeenCalledWith( "http://localhost:8000/api/counter/increment", expect.objectContaining({ method: "POST", credentials: "include", }) ); }); }); test("clicking increment updates displayed count", async () => { vi.spyOn(global, "fetch") .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ value: 0 }) } as Response) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ value: 1 }) } as Response); render(); await waitFor(() => expect(screen.getByText("0")).toBeDefined()); fireEvent.click(screen.getByText("Increment")); await waitFor(() => expect(screen.getByText("1")).toBeDefined()); }); }); describe("Home - Unauthenticated", () => { test("redirects to login when not authenticated", async () => { mockUser = null; render(); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith("/login"); }); }); test("returns null when not authenticated", () => { mockUser = null; const { container } = render(); // Should render nothing (just redirects) expect(container.querySelector("main")).toBeNull(); }); test("does not fetch counter when no user", () => { mockUser = null; const fetchSpy = vi.spyOn(global, "fetch"); render(); expect(fetchSpy).not.toHaveBeenCalled(); }); }); describe("Home - Loading State", () => { test("does not redirect while loading", () => { mockIsLoading = true; mockUser = null; render(); expect(mockPush).not.toHaveBeenCalled(); }); test("shows loading indicator while loading", () => { mockIsLoading = true; render(); expect(screen.getByText("Loading...")).toBeDefined(); }); }); describe("Home - Navigation", () => { test("shows My Profile link for regular user", async () => { vi.spyOn(global, "fetch").mockResolvedValue({ json: () => Promise.resolve({ value: 0 }), } as Response); render(); await waitFor(() => { expect(screen.getByText("My Profile")).toBeDefined(); }); }); test("does not show My Profile link for admin user", async () => { mockUser = { id: 1, email: "admin@example.com", roles: ["admin"], permissions: ["view_counter", "view_audit"], }; vi.spyOn(global, "fetch").mockResolvedValue({ json: () => Promise.resolve({ value: 0 }), } as Response); render(); // Wait for render - admin sees admin nav (Audit, Invites) not regular nav await waitFor(() => { expect(screen.getByText("Audit")).toBeDefined(); }); expect(screen.queryByText("My Profile")).toBeNull(); }); });