import { fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { DashboardPage } from "../../pages/DashboardPage"; // Mock data for tests with medications const mockMeds = [ { id: 1, name: "Aspirin", packCount: 1, blistersPerPack: 2, pillsPerBlister: 10, looseTablets: 5, takenBy: ["John"], blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00Z" }], intakeRemindersEnabled: true, notes: "Take with food", expiryDate: "2025-12-31", imageUrl: null, updatedAt: null, }, { id: 2, name: "Vitamin D", packCount: 0, blistersPerPack: 1, pillsPerBlister: 30, looseTablets: 3, takenBy: [], blisters: [{ usage: 1, every: 1, start: "2024-01-01T08:00:00Z" }], intakeRemindersEnabled: false, notes: null, expiryDate: null, imageUrl: null, updatedAt: null, }, ]; const mockCoverage = { all: [ { name: "Aspirin", medsLeft: 25, daysLeft: 25, depletionDate: "2025-02-15", depletionTime: Date.now() + 25 * 86400000, nextDose: null, }, { name: "Vitamin D", medsLeft: 3, daysLeft: 3, depletionDate: "2025-01-25", depletionTime: Date.now() + 3 * 86400000, nextDose: null, }, ], low: [ { name: "Vitamin D", medsLeft: 3, daysLeft: 3, depletionDate: "2025-01-25", depletionTime: Date.now() + 3 * 86400000, nextDose: null, }, ], }; const mockFutureDays = [ { dateStr: "Mon, Jan 22", date: new Date(), isPast: false, meds: [ { medName: "Aspirin", total: 1, doses: [{ id: `1-0-${Date.now()}`, timeStr: "09:00", when: Date.now(), usage: 1, takenBy: ["John"] }], lastWhen: Date.now(), }, ], }, ]; const mockPastDays = [ { dateStr: "Sun, Jan 21", date: new Date(Date.now() - 86400000), isPast: true, meds: [ { medName: "Aspirin", total: 1, doses: [ { id: `1-0-${Date.now() - 86400000}`, timeStr: "09:00", when: Date.now() - 86400000, usage: 1, takenBy: ["John"], }, ], lastWhen: Date.now() - 86400000, }, ], }, ]; // Default mock factory const createMockAppContext = (overrides = {}) => ({ meds: [], settings: { lowStockThreshold: 30, criticalStockThreshold: 7, expiryWarningDays: 30, lowStockDays: 7, normalStockDays: 30, highStockDays: 90, emailEnabled: false, shoutrrrEnabled: false, reminderDaysBefore: 7, notificationEmail: "", lastAutoEmailSent: null, lastNotificationType: null, lastNotificationChannel: null, }, scheduleDays: 30, setScheduleDays: vi.fn(), showPastDays: false, setShowPastDays: vi.fn(), pastDays: [], futureDays: [], takenDoses: new Set(), dismissedDoses: new Set(), markDoseTaken: vi.fn(), undoDoseTaken: vi.fn(), coverage: { all: [], low: [] }, coverageByMed: {}, depletionByMed: {}, stockThresholds: { lowStockDays: 7, normalStockDays: 30, highStockDays: 90, criticalStockDays: 7, expiryWarningDays: 30, }, manuallyExpandedDays: new Set(), manuallyCollapsedDays: new Set(), toggleDayCollapse: vi.fn(), openMedDetail: vi.fn(), openUserFilter: vi.fn(), openShareDialog: vi.fn(), openScheduleLightbox: vi.fn(), missedPastDoseIds: [], getDayStockStatus: vi.fn(() => "success"), getDoseId: vi.fn((id, person) => (person ? `${id}-${person}` : id)), showClearMissedConfirm: false, setShowClearMissedConfirm: vi.fn(), clearingMissed: false, dismissMissedDoses: vi.fn(), ...overrides, }); let mockContextValue = createMockAppContext(); // Mock the context vi.mock("../../context", () => ({ useAppContext: () => mockContextValue, })); vi.mock("../../components/Auth", () => ({ useAuth: () => ({ user: { id: 1, username: "testuser" }, }), })); describe("DashboardPage", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext(); }); it("renders dashboard page", () => { render( ); // Should render the dashboard section const section = document.querySelector("section.grid"); expect(section).toBeInTheDocument(); }); it("renders reorder section title", () => { render( ); expect(screen.getByText(/dashboard\.reorder\.title/i)).toBeInTheDocument(); }); it("renders overview section title", () => { render( ); expect(screen.getByText(/dashboard\.overview\.title/i)).toBeInTheDocument(); }); it("renders schedule section title", () => { render( ); expect(screen.getByText(/dashboard\.schedules\.title/i)).toBeInTheDocument(); }); it("renders empty state when no medications", () => { render( ); // With no meds, should show the dashboard cards const cards = document.querySelectorAll(".card"); expect(cards.length).toBeGreaterThan(0); }); it("renders schedule days selector", () => { render( ); // Should have schedule days select dropdown const select = document.querySelector(".schedule-days-select"); expect(select).toBeInTheDocument(); }); it("renders timeline section", () => { render( ); // Should have timeline div const timeline = document.querySelector(".timeline"); expect(timeline).toBeInTheDocument(); }); it("renders table headers for overview", () => { render( ); // Should have table headers expect(screen.getByText(/table\.name/i)).toBeInTheDocument(); expect(screen.getByText(/table\.daysLeft/i)).toBeInTheDocument(); }); it("renders multiple cards", () => { render( ); // Dashboard has multiple cards const cards = document.querySelectorAll(".card"); expect(cards.length).toBeGreaterThan(2); }); it("renders card heads", () => { render( ); // Should have card heads for each section const cardHeads = document.querySelectorAll(".card-head"); expect(cardHeads.length).toBeGreaterThan(0); }); it("renders table headers", () => { render( ); // Should have table head const tableHead = document.querySelector(".table-head"); expect(tableHead).toBeInTheDocument(); }); it("renders table structure", () => { render( ); // Should have table class const table = document.querySelector(".table"); expect(table).toBeInTheDocument(); }); it("renders no meds message for reorder section", () => { render( ); // When no meds, should show empty state expect(screen.getByText(/dashboard\.reorder\.noMeds/i)).toBeInTheDocument(); }); }); describe("DashboardPage interactions", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext(); }); it("has schedule days options", () => { render( ); // Should have 30, 90, 180 day options const select = document.querySelector(".schedule-days-select"); expect(select).toBeInTheDocument(); const options = select?.querySelectorAll("option"); expect(options?.length).toBe(3); }); it("can change schedule days", () => { render( ); const select = document.querySelector(".schedule-days-select") as HTMLSelectElement; expect(select).toBeInTheDocument(); fireEvent.change(select, { target: { value: "90" } }); }); }); describe("DashboardPage structure", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext(); }); it("renders multiple section grids", () => { render( ); const sections = document.querySelectorAll("section.grid"); expect(sections.length).toBeGreaterThan(0); }); it("renders card head actions", () => { render( ); const cardHeadActions = document.querySelector(".card-head-actions"); expect(cardHeadActions).toBeInTheDocument(); }); it("renders all table columns", () => { render( ); // Should have all expected table columns expect(screen.getByText(/table\.name/i)).toBeInTheDocument(); expect(screen.getByText(/table\.stock(?!Details)/i)).toBeInTheDocument(); expect(screen.getByText(/table\.stockDetails/i)).toBeInTheDocument(); expect(screen.getByText(/table\.daysLeft/i)).toBeInTheDocument(); expect(screen.getByText(/table\.runsOut/i)).toBeInTheDocument(); expect(screen.getByText(/table\.expiry/i)).toBeInTheDocument(); expect(screen.getByText(/table\.status/i)).toBeInTheDocument(); }); }); describe("DashboardPage with medications", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0], "Vitamin D": mockCoverage.all[1], }, depletionByMed: { Aspirin: Date.now() + 25 * 86400000, "Vitamin D": Date.now() + 3 * 86400000, }, futureDays: mockFutureDays, }); }); it("renders medication rows in overview table", () => { render( ); // Should show medication names (may appear in multiple places) const aspirinElements = screen.getAllByText("Aspirin"); const vitaminDElements = screen.getAllByText("Vitamin D"); expect(aspirinElements.length).toBeGreaterThan(0); expect(vitaminDElements.length).toBeGreaterThan(0); }); it("renders low stock section with low stock medications", () => { render( ); // Should show the low stock medication name const vitaminDElements = screen.getAllByText("Vitamin D"); expect(vitaminDElements.length).toBeGreaterThan(0); }); it("renders taken by badges", () => { render( ); // Should show taken by badge for Aspirin const johnBadges = screen.getAllByText("John"); expect(johnBadges.length).toBeGreaterThan(0); }); it("renders medication icons for reminders and notes", () => { render( ); // Aspirin has intakeRemindersEnabled and notes const reminderIcons = document.querySelectorAll(".reminder-icon"); expect(reminderIcons.length).toBeGreaterThan(0); const notesIcons = document.querySelectorAll(".notes-icon"); expect(notesIcons.length).toBeGreaterThan(0); }); it("renders schedule timeline with future doses", () => { // Need showFutureDays: true for day-blocks to render mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0], }, futureDays: mockFutureDays, showFutureDays: true, }); render( ); // Should show day block const dayBlocks = document.querySelectorAll(".day-block"); expect(dayBlocks.length).toBeGreaterThan(0); }); it("calls openMedDetail when clicking medication row", () => { const openMedDetail = vi.fn(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0] }, openMedDetail, }); render( ); // Click on medication row const aspirinRow = screen.getAllByText("Aspirin")[0].closest(".table-row"); if (aspirinRow) { fireEvent.click(aspirinRow); expect(openMedDetail).toHaveBeenCalled(); } }); it("calls openUserFilter when clicking taken by badge", () => { const openUserFilter = vi.fn(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0] }, openUserFilter, }); render( ); // Click on taken by badge const johnBadge = screen.getAllByText("John")[0]; fireEvent.click(johnBadge); expect(openUserFilter).toHaveBeenCalledWith("John"); }); }); describe("DashboardPage with email notifications", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, settings: { ...createMockAppContext().settings, emailEnabled: true, emailStockReminders: true, notificationEmail: "test@example.com", }, }); }); it("renders reminder status bar when email enabled", () => { render( ); // Should show reminder status bar const statusBar = document.querySelector(".reminder-status-bar"); expect(statusBar).toBeInTheDocument(); }); it("hides reorder reminder card when reminders are enabled (to avoid redundancy)", () => { render( ); // Reorder card should NOT be shown when reminders are active (Reminder Bar shows the info instead) expect(screen.queryByText(/dashboard\.reorder\.sendReminder/i)).not.toBeInTheDocument(); }); }); describe("DashboardPage with shoutrrr notifications", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, settings: { ...createMockAppContext().settings, shoutrrrEnabled: true, shoutrrrStockReminders: true, }, }); }); it("renders notification status bar when shoutrrr enabled", () => { render( ); // Should show status bar const statusBar = document.querySelector(".reminder-status-bar"); expect(statusBar).toBeInTheDocument(); }); }); describe("DashboardPage with past days", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, pastDays: mockPastDays, futureDays: mockFutureDays, showPastDays: false, missedPastDoseIds: [`1-0-${Date.now() - 86400000}-John`], }); }); it("renders past days toggle when past days exist", () => { render( ); // Should show past days toggle const toggle = document.querySelector(".past-days-toggle"); expect(toggle).toBeInTheDocument(); }); it("shows missed dose warning count", () => { render( ); // Should show warning with missed count const warning = document.querySelector(".past-days-warning"); expect(warning).toBeInTheDocument(); }); it("toggles past days visibility", () => { const setShowPastDays = vi.fn(); mockContextValue = createMockAppContext({ pastDays: mockPastDays, showPastDays: false, setShowPastDays, missedPastDoseIds: [], }); render( ); const toggle = document.querySelector(".past-days-toggle"); if (toggle) { fireEvent.click(toggle); expect(setShowPastDays).toHaveBeenCalledWith(true); } }); it("shows clear missed doses button when there are missed doses", () => { render( ); // Should show clear missed button const clearBtn = document.querySelector(".clear-missed-btn"); expect(clearBtn).toBeInTheDocument(); }); }); describe("DashboardPage with expanded past days", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0] }, pastDays: mockPastDays, futureDays: mockFutureDays, showPastDays: true, manuallyExpandedDays: new Set(["Sun, Jan 21"]), getDayStockStatus: vi.fn(() => "success"), }); }); it("renders past day blocks when showPastDays is true", () => { render( ); // Should show past day block const pastDayBlocks = document.querySelectorAll(".day-block.past"); expect(pastDayBlocks.length).toBeGreaterThan(0); }); }); describe("DashboardPage dose interactions", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); }); it("calls markDoseTaken when clicking take button", () => { const markDoseTaken = vi.fn(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0] }, depletionByMed: { Aspirin: Date.now() + 25 * 86400000 }, futureDays: mockFutureDays, markDoseTaken, }); render( ); // Find and click take button const takeBtn = document.querySelector(".dose-btn.take"); if (takeBtn) { fireEvent.click(takeBtn); expect(markDoseTaken).toHaveBeenCalled(); } }); it("calls undoDoseTaken when clicking undo button", () => { const undoDoseTaken = vi.fn(); const doseId = `1-0-${Date.now()}-John`; mockContextValue = createMockAppContext({ meds: mockMeds, coverage: mockCoverage, coverageByMed: { Aspirin: mockCoverage.all[0] }, depletionByMed: { Aspirin: Date.now() + 25 * 86400000 }, futureDays: mockFutureDays, takenDoses: new Set([doseId]), undoDoseTaken, getDoseId: vi.fn(() => doseId), }); render( ); // Find and click undo button const undoBtn = document.querySelector(".dose-btn.undo"); if (undoBtn) { fireEvent.click(undoBtn); expect(undoDoseTaken).toHaveBeenCalled(); } }); }); describe("DashboardPage good stock state", () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockContextValue = createMockAppContext({ meds: mockMeds, coverage: { all: [ { name: "Aspirin", medsLeft: 100, daysLeft: 100, depletionDate: "2025-05-01", depletionTime: Date.now() + 100 * 86400000, nextDose: null, }, ], low: [], }, }); }); it("shows all good message when no low stock", () => { render( ); // Should show all good message expect(screen.getByText(/dashboard\.reorder\.allGood/i)).toBeInTheDocument(); }); });