import { fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "../App"; type AuthStateMock = { user: { id: number; username: string } | null; authState: { authEnabled: boolean; needsSetup: boolean } | null; loading: boolean; authError: string | null; }; let authMock: AuthStateMock = { user: null, authState: { authEnabled: false, needsSetup: false }, loading: false, authError: null, }; let appContextMock: Record; vi.mock("../components", () => ({ AboutModal: ({ isOpen }: { isOpen: boolean }) => (isOpen ?
about-modal-open
: null), Lightbox: ({ src }: { src: string }) =>
lightbox-open-{src}
, MedDetailModal: () => null, ProfileModal: ({ isOpen }: { isOpen: boolean }) => (isOpen ?
profile-modal-open
: null), ShareDialog: () => null, SharedSchedule: () =>
shared-schedule-page
, UserFilterModal: () => null, })); vi.mock("../components/AppHeader", () => ({ AppHeader: ({ onOpenProfile, onOpenAbout }: { onOpenProfile: () => void; onOpenAbout: () => void }) => (
app-header
), })); vi.mock("../components/Auth", () => ({ AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}, AuthPage: () =>
auth-page
, useAuth: () => authMock, })); vi.mock("../context", () => ({ AppProvider: ({ children }: { children: React.ReactNode }) => <>{children}, UnsavedChangesProvider: ({ children }: { children: React.ReactNode }) => <>{children}, useAppContext: () => appContextMock, })); vi.mock("../pages", () => ({ DashboardPage: () =>
dashboard-page
, MedicationsPage: () =>
medications-page
, PlannerPage: () =>
planner-page
, SchedulePage: () =>
schedule-page
, SettingsPage: () =>
settings-page
, })); describe("App", () => { beforeEach(() => { authMock = { user: null, authState: { authEnabled: false, needsSetup: false }, loading: false, authError: null, }; appContextMock = { meds: [], loadMeds: vi.fn(), settings: {}, showRefillModal: false, setShowRefillModal: vi.fn(), refillPacks: 0, setRefillPacks: vi.fn(), refillLoose: 0, setRefillLoose: vi.fn(), refillSaving: false, refillHistory: [], refillHistoryExpanded: false, setRefillHistoryExpanded: vi.fn(), showEditStockModal: false, setShowEditStockModal: vi.fn(), editStockFullBlisters: 0, setEditStockFullBlisters: vi.fn(), editStockPartialBlisterPills: 0, setEditStockPartialBlisterPills: vi.fn(), editStockSaving: false, openRefillModal: vi.fn(), closeRefillModal: vi.fn(), openEditStockModal: vi.fn(), closeEditStockModal: vi.fn(), showShareDialog: false, sharePeople: [], shareSelectedPerson: "", setShareSelectedPerson: vi.fn(), shareSelectedDays: 7, setShareSelectedDays: vi.fn(), shareGenerating: false, shareLink: null, setShareLink: vi.fn(), shareCopied: false, setShareCopied: vi.fn(), generateShareLink: vi.fn(), copyShareLink: vi.fn(), closeShareDialog: vi.fn(), resetShareDialogState: vi.fn(), coverage: { all: [], low: [] }, selectedMed: null, setSelectedMed: vi.fn(), showImageLightbox: false, setShowImageLightbox: vi.fn(), scheduleLightboxImage: null, setScheduleLightboxImage: vi.fn(), selectedUser: null, setSelectedUser: vi.fn(), openMedDetail: vi.fn(), closeMedDetail: vi.fn(), openImageLightbox: vi.fn(), closeImageLightbox: vi.fn(), closeScheduleLightbox: vi.fn(), closeUserFilter: vi.fn(), openShareDialog: vi.fn(), submitStockCorrection: vi.fn(), submitRefill: vi.fn(), stockThresholds: { lowStockDays: 7, normalStockDays: 30, highStockDays: 90, criticalStockDays: 7, expiryWarningDays: 30, }, }; document.documentElement.classList.remove("modal-open"); document.body.classList.remove("modal-open"); vi.spyOn(window.history, "back").mockImplementation(() => {}); vi.spyOn(window.history, "pushState").mockImplementation(() => {}); vi.clearAllMocks(); }); it("renders public shared schedule route without auth", () => { render( ); expect(screen.getByText("shared-schedule-page")).toBeInTheDocument(); }); it("renders loading state while auth is being checked", () => { authMock = { user: null, authState: null, loading: true, authError: null, }; render( ); expect(screen.getByText("Loading...")).toBeInTheDocument(); }); it("renders connection error state", () => { authMock = { user: null, authState: { authEnabled: false, needsSetup: false }, loading: false, authError: "Backend is unreachable", }; render( ); expect(screen.getByText("Connection Error")).toBeInTheDocument(); expect(screen.getByText("Backend is unreachable")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument(); }); it("reloads page when retry button is clicked", () => { authMock = { user: null, authState: { authEnabled: false, needsSetup: false }, loading: false, authError: "Backend is unreachable", }; const reloadSpy = vi.fn(); Object.defineProperty(window, "location", { value: { ...window.location, reload: reloadSpy }, writable: true, }); render( ); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(reloadSpy).toHaveBeenCalled(); }); it("renders auth page when setup is required", () => { authMock = { user: null, authState: { authEnabled: true, needsSetup: true }, loading: false, authError: null, }; render( ); expect(screen.getByText("auth-page")).toBeInTheDocument(); }); it("renders auth page when auth is enabled and no user is logged in", () => { authMock = { user: null, authState: { authEnabled: true, needsSetup: false }, loading: false, authError: null, }; render( ); expect(screen.getByText("auth-page")).toBeInTheDocument(); }); it("renders app shell when auth is disabled", () => { render( ); expect(screen.getByText("app-header")).toBeInTheDocument(); expect(screen.getByText("dashboard-page")).toBeInTheDocument(); }); it("renders initializing state when auth state is missing", () => { authMock = { user: null, authState: null, loading: false, authError: null, }; render( ); expect(screen.getByText("Initializing...")).toBeInTheDocument(); }); it("renders schedule lightbox when schedule image is set", () => { appContextMock.scheduleLightboxImage = "med-image.png"; render( ); expect(screen.getByText("lightbox-open-med-image.png")).toBeInTheDocument(); }); it("handles popstate by closing selected medication", () => { appContextMock.selectedMed = { id: 1, packCount: 1, looseTablets: 0, updatedAt: null }; render( ); window.dispatchEvent(new PopStateEvent("popstate")); expect(appContextMock.setSelectedMed).toHaveBeenCalledWith(null); }); it("adds modal-open class when modal state is active", () => { appContextMock.showShareDialog = true; render( ); expect(document.documentElement.classList.contains("modal-open")).toBe(true); expect(document.body.classList.contains("modal-open")).toBe(true); }); it("opens profile and about modals from header actions", () => { render( ); fireEvent.click(screen.getByRole("button", { name: "open-profile" })); expect(screen.getByText("profile-modal-open")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "open-about" })); expect(screen.getByText("about-modal-open")).toBeInTheDocument(); expect(window.history.pushState).toHaveBeenCalled(); }); it("handles popstate by resetting share dialog state", () => { appContextMock.showShareDialog = true; render( ); window.dispatchEvent(new PopStateEvent("popstate")); expect(appContextMock.resetShareDialogState).toHaveBeenCalled(); }); it("redirects unknown routes to dashboard", () => { render( ); expect(screen.getByText("dashboard-page")).toBeInTheDocument(); }); it("popstate closes image lightbox before other modals", () => { appContextMock.showImageLightbox = true; appContextMock.scheduleLightboxImage = "img.png"; render( ); window.dispatchEvent(new PopStateEvent("popstate")); expect(appContextMock.setShowImageLightbox).toHaveBeenCalledWith(false); expect(appContextMock.setScheduleLightboxImage).not.toHaveBeenCalledWith(null); }); it("popstate closes schedule lightbox when image lightbox is not open", () => { appContextMock.showImageLightbox = false; appContextMock.scheduleLightboxImage = "img.png"; render( ); window.dispatchEvent(new PopStateEvent("popstate")); expect(appContextMock.setScheduleLightboxImage).toHaveBeenCalledWith(null); }); });