import { fireEvent, render, screen, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { MedDetailModal } from "../../components/MedDetailModal"; import type { Coverage, Medication, RefillEntry, StockThresholds } from "../../types"; import * as utils from "../../utils"; const defaultSettings: StockThresholds = { lowStockDays: 7, normalStockDays: 30, highStockDays: 90, criticalStockDays: 3, expiryWarningDays: 30, }; const mockMedication: Medication = { id: 1, name: "Test Med", genericName: "Generic Name", packageType: "blister", packCount: 1, blistersPerPack: 1, pillsPerBlister: 30, looseTablets: 0, takenBy: ["John"], blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }], updatedAt: null, expiryDate: "2025-12-31", notes: "Test notes", }; const mockCoverage: Coverage = { name: "Test Med", medsLeft: 25, daysLeft: 25, depletionDate: "2024-04-01", depletionTime: Date.now() + 25 * 86400000, nextDose: null, }; const defaultProps = { selectedMed: mockMedication, coverage: { all: [mockCoverage] }, settings: defaultSettings, showImageLightbox: false, showRefillModal: false, showEditStockModal: false, onClose: vi.fn(), onOpenImageLightbox: vi.fn(), onCloseImageLightbox: vi.fn(), onOpenRefillModal: vi.fn(), onCloseRefillModal: vi.fn(), onOpenEditStockModal: vi.fn(), onCloseEditStockModal: vi.fn(), refillPacks: 0, onRefillPacksChange: vi.fn(), refillLoose: 0, onRefillLooseChange: vi.fn(), usePrescriptionRefill: false, onUsePrescriptionRefillChange: vi.fn(), refillSaving: false, refillHistory: [] as RefillEntry[], refillHistoryExpanded: false, onRefillHistoryExpandedChange: vi.fn(), onSubmitRefill: vi.fn(), editStockFullBlisters: 0, onEditStockFullBlistersChange: vi.fn(), editStockPartialBlisterPills: 0, onEditStockPartialBlisterPillsChange: vi.fn(), editStockLoosePills: 0, onEditStockLoosePillsChange: vi.fn(), editStockSaving: false, onSubmitStockCorrection: vi.fn(), }; describe("MedDetailModal", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders nothing when selectedMed is null", () => { render(); expect(screen.queryByText("Test Med")).not.toBeInTheDocument(); }); it("renders modal when medication is selected", () => { render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); it("displays medication name", () => { render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); it("displays generic name", () => { render(); expect(screen.getByText("Generic Name")).toBeInTheDocument(); }); it("renders close button", () => { render(); const closeButtons = screen.getAllByRole("button", { name: /common\.close/i }); const closeBtn = closeButtons[0]; expect(closeBtn).toBeInTheDocument(); }); it("calls onClose when close button clicked", () => { const onClose = vi.fn(); render(); const closeButtons = screen.getAllByRole("button", { name: /common\.close/i }); const closeBtn = closeButtons[0]; fireEvent.click(closeBtn); expect(onClose).toHaveBeenCalledTimes(1); }); it("calls onClose when overlay clicked", () => { const onClose = vi.fn(); render(); const overlay = document.querySelector(".modal-overlay"); if (overlay) { fireEvent.click(overlay); } expect(onClose).toHaveBeenCalledTimes(1); }); it("does not call onClose when modal content clicked", () => { const onClose = vi.fn(); render(); const content = document.querySelector(".modal-content"); if (content) { fireEvent.click(content); } expect(onClose).not.toHaveBeenCalled(); }); it("displays notes when available", () => { render(); expect(screen.getByText("Test notes")).toBeInTheDocument(); }); it("shows loose pills in stock details for blister medications", () => { const medWithLoose: Medication = { ...mockMedication, pillsPerBlister: 5, looseTablets: 2, }; const coverageWithLoose: Coverage = { ...mockCoverage, medsLeft: 50, }; render(); expect(screen.getByText("+ 2 modal.loosePills", { exact: false })).toBeInTheDocument(); }); it("shows prescription details section when prescription is enabled", () => { const med: Medication = { ...mockMedication, prescriptionEnabled: true, prescriptionAuthorizedRefills: 5, prescriptionRemainingRefills: 2, prescriptionLowRefillThreshold: 1, prescriptionExpiryDate: "2026-12-31", }; render(); expect(screen.getByText(/form\.sections\.prescription/i)).toBeInTheDocument(); expect(screen.getByText(/prescription\.authorizedRefills/i)).toBeInTheDocument(); expect(screen.getByText(/prescription\.remainingRefills/i)).toBeInTheDocument(); expect(screen.getByText(/prescription\.lowThreshold/i)).toBeInTheDocument(); expect(screen.getByText(/prescription\.expiryDate/i)).toBeInTheDocument(); }); it("does not show prescription details section when prescription is disabled", () => { const med: Medication = { ...mockMedication, prescriptionEnabled: false, }; render(); expect(screen.queryByText(/form\.sections\.prescription/i)).not.toBeInTheDocument(); }); it("displays schedule information", () => { render(); // Should have schedule section const scheduleSection = document.querySelector(".med-detail-schedules"); expect(scheduleSection).toBeInTheDocument(); }); it("renders med detail header", () => { render(); const header = document.querySelector(".med-detail-header"); expect(header).toBeInTheDocument(); }); it("renders med detail body", () => { render(); const body = document.querySelector(".med-detail-body"); expect(body).toBeInTheDocument(); }); it("shows configured pack count in package details, independent from current stock", () => { const medWithConfiguredPacks: Medication = { ...mockMedication, packCount: 11, blistersPerPack: 5, pillsPerBlister: 5, }; const lowCurrentStockCoverage: Coverage = { ...mockCoverage, medsLeft: 47, }; render( ); const packsLabel = screen.getByText(/modal\.packs/i); const packsValue = packsLabel.closest(".med-detail-item")?.querySelector(".med-detail-value"); expect(packsValue?.textContent).toBe("11"); }); }); describe("MedDetailModal without coverage", () => { beforeEach(() => { vi.clearAllMocks(); }); it("works without coverage data", () => { render(); // Should still render the medication name expect(screen.getByText("Test Med")).toBeInTheDocument(); }); }); describe("MedDetailModal without optional fields", () => { beforeEach(() => { vi.clearAllMocks(); }); it("works without generic name", () => { const med = { ...mockMedication, genericName: null }; render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); it("works without notes", () => { const med = { ...mockMedication, notes: null }; render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); it("works without takenBy", () => { const med = { ...mockMedication, takenBy: [] }; render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); it("works without expiryDate", () => { const med = { ...mockMedication, expiryDate: null }; render(); expect(screen.getByText("Test Med")).toBeInTheDocument(); }); }); describe("MedDetailModal with refill modal", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows refill modal when open", () => { render(); // Modal should show refill section const modal = document.querySelector(".modal-overlay"); expect(modal).toBeInTheDocument(); }); it("calls onCloseRefillModal when refill modal closed", () => { const onCloseRefillModal = vi.fn(); render(); // Modal close button const closeButtons = document.querySelectorAll("button"); const cancelBtn = Array.from(closeButtons).find( (btn) => btn.textContent?.includes("cancel") || btn.textContent?.includes("Cancel") ); if (cancelBtn) { fireEvent.click(cancelBtn); } }); it("calls onSubmitRefill when refill submitted", () => { const onSubmitRefill = vi.fn(); render(); const submitBtn = document.querySelector(".refill-modal .modal-footer .success") as HTMLButtonElement; fireEvent.click(submitBtn); expect(onSubmitRefill).toHaveBeenCalledWith(mockMedication.id, false); }); it("disables refill submit button when no pills are entered", () => { render(); const submitBtn = document.querySelector(".refill-modal .modal-footer .success") as HTMLButtonElement; expect(submitBtn).toBeDisabled(); }); it("shows singular refill preview text when total refill is one pill", () => { const bottleMed: Medication = { ...mockMedication, packageType: "bottle", packCount: 0, blistersPerPack: 1, pillsPerBlister: 1, looseTablets: 10, }; render(); expect(screen.getByText(/\+1 common\.pill/i)).toBeInTheDocument(); }); it("parses refill packs and loose pills inputs", () => { const onRefillPacksChange = vi.fn(); const onRefillLooseChange = vi.fn(); render( ); const numberInputs = document.querySelectorAll(".refill-modal input[type='number']"); fireEvent.change(numberInputs[0], { target: { value: "3" } }); fireEvent.change(numberInputs[1], { target: { value: "5" } }); expect(onRefillPacksChange).toHaveBeenCalledWith(3); expect(onRefillLooseChange).toHaveBeenCalledWith(5); }); it("uses zero fallback for invalid refill input values", () => { const onRefillPacksChange = vi.fn(); const onRefillLooseChange = vi.fn(); render( ); const numberInputs = document.querySelectorAll(".refill-modal input[type='number']"); fireEvent.change(numberInputs[0], { target: { value: "NaN" } }); fireEvent.change(numberInputs[1], { target: { value: "" } }); expect(onRefillPacksChange).toHaveBeenCalledWith(0); expect(onRefillLooseChange).toHaveBeenCalledWith(0); }); it("shows package size breakdown key for blister stock correction", () => { render(); expect(screen.queryByText("editStock.packageSizeBreakdown")).not.toBeInTheDocument(); expect(document.querySelector(".edit-stock-live-breakdown")).toBeInTheDocument(); }); it("shows numeric package size text for bottle stock correction", () => { const bottleMed: Medication = { ...mockMedication, packageType: "bottle", totalPills: 150, looseTablets: 130, }; render(); expect(screen.getByText("editStock.packageSize_150")).toBeInTheDocument(); }); it("shows bottles-based refill input for liquid container and preview in ml package amount", () => { const liquidMed: Medication = { ...mockMedication, name: "Liquid Med", packageType: "liquid_container", packCount: 1, packageAmountValue: 150, packageAmountUnit: "ml", totalPills: 150, looseTablets: 150, }; render(); const refillModal = document.querySelector(".refill-modal"); expect(refillModal).not.toBeNull(); expect(within(refillModal as HTMLElement).getByText(/form\.bottles/i)).toBeInTheDocument(); expect(screen.queryByText(/refill\.pillsToAdd/i)).not.toBeInTheDocument(); expect(screen.getByText(/\+150 form\.packageAmountUnitMl/i)).toBeInTheDocument(); }); it("maps liquid refill bottle input to package amount in ml", () => { const liquidMed: Medication = { ...mockMedication, name: "Liquid Med", packageType: "liquid_container", packCount: 1, packageAmountValue: 150, packageAmountUnit: "ml", totalPills: 150, looseTablets: 150, }; const onRefillLooseChange = vi.fn(); const onRefillPacksChange = vi.fn(); render( ); const input = document.querySelector(".refill-modal input[type='number']") as HTMLInputElement; fireEvent.change(input, { target: { value: "2" } }); expect(onRefillPacksChange).toHaveBeenCalledWith(2); expect(onRefillLooseChange).toHaveBeenCalledWith(300); }); it("shows tubes-based refill input for tube package and preview in g package amount", () => { const tubeMed: Medication = { ...mockMedication, name: "Tube Med", packageType: "tube", packCount: 4, packageAmountValue: 150, packageAmountUnit: "g", totalPills: 600, looseTablets: 600, }; render(); const refillModal = document.querySelector(".refill-modal"); expect(refillModal).not.toBeNull(); expect(within(refillModal as HTMLElement).getByText(/form\.tubes/i)).toBeInTheDocument(); expect(screen.queryByText(/refill\.pillsToAdd/i)).not.toBeInTheDocument(); expect(screen.getByText(/\+150 form\.packageAmountUnitG/i)).toBeInTheDocument(); }); it("maps tube refill count input to package amount in g", () => { const tubeMed: Medication = { ...mockMedication, name: "Tube Med", packageType: "tube", packCount: 4, packageAmountValue: 150, packageAmountUnit: "g", totalPills: 600, looseTablets: 600, }; const onRefillLooseChange = vi.fn(); const onRefillPacksChange = vi.fn(); render( ); const input = document.querySelector(".refill-modal input[type='number']") as HTMLInputElement; fireEvent.change(input, { target: { value: "2" } }); expect(onRefillPacksChange).toHaveBeenCalledWith(2); expect(onRefillLooseChange).toHaveBeenCalledWith(300); }); }); describe("MedDetailModal actions", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders action buttons", () => { render(); const buttons = document.querySelectorAll("button"); expect(buttons.length).toBeGreaterThan(0); }); it("calls onOpenRefillModal when refill clicked", () => { const onOpenRefillModal = vi.fn(); render(); const buttons = document.querySelectorAll("button"); const refillBtn = Array.from(buttons).find( (btn) => btn.textContent?.includes("refill") || btn.textContent?.includes("Refill") ); if (refillBtn) { fireEvent.click(refillBtn); expect(onOpenRefillModal).toHaveBeenCalled(); } }); it("calls generateICS when export calendar button is clicked", () => { const generateICSSpy = vi.spyOn(utils, "generateICS").mockImplementation(() => "BEGIN:VCALENDAR"); render(); fireEvent.click(screen.getByRole("button", { name: /modal\.exportTooltip/i })); expect(generateICSSpy).toHaveBeenCalledWith(mockMedication); }); it("calls onOpenEditStockModal when stock correction icon is clicked", () => { const onOpenEditStockModal = vi.fn(); render(); fireEvent.click(screen.getByRole("button", { name: /editStock\.buttonLabel/i })); expect(onOpenEditStockModal).toHaveBeenCalledTimes(1); }); it("does not render export calendar button when no blisters exist", () => { const medWithoutBlisters: Medication = { ...mockMedication, blisters: [], }; render(); expect(screen.queryByRole("button", { name: /modal\.exportTooltip/i })).not.toBeInTheDocument(); }); }); describe("MedDetailModal with multiple blisters", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders multiple schedule entries", () => { const med = { ...mockMedication, blisters: [ { usage: 1, every: 1, start: "2024-01-01T09:00:00" }, { usage: 2, every: 7, start: "2024-01-01T20:00:00" }, ], }; render(); const _scheduleEntries = document.querySelectorAll(".schedule-entry"); // Should have multiple schedule entries }); }); describe("MedDetailModal with image", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders medication avatar", () => { render(); const avatar = document.querySelector(".med-avatar"); expect(avatar).toBeInTheDocument(); }); it("shows lightbox when image clicked", () => { const onOpenImageLightbox = vi.fn(); const med = { ...mockMedication, imageUrl: "test-image.jpg" }; render(); const avatar = document.querySelector(".med-avatar"); if (avatar) { fireEvent.click(avatar); } expect(onOpenImageLightbox).toHaveBeenCalledTimes(1); }); it("renders lightbox when enabled and image is present", () => { const med = { ...mockMedication, imageUrl: "test-image.jpg" }; render(); expect(document.querySelector(".lightbox-overlay")).toBeInTheDocument(); }); }); describe("MedDetailModal nested modal overlays", () => { beforeEach(() => { vi.clearAllMocks(); }); it("closes refill modal when clicking refill overlay", () => { const onCloseRefillModal = vi.fn(); render(); const overlays = document.querySelectorAll(".modal-overlay"); fireEvent.click(overlays[1]); expect(onCloseRefillModal).toHaveBeenCalledTimes(1); }); it("closes edit stock modal when clicking edit-stock overlay", () => { const onCloseEditStockModal = vi.fn(); render( ); const overlays = document.querySelectorAll(".modal-overlay"); fireEvent.click(overlays[1]); expect(onCloseEditStockModal).toHaveBeenCalledTimes(1); }); it("renders only edit stock modal in editStockOnly mode", () => { render(); expect(screen.getByText("editStock.title")).toBeInTheDocument(); expect(screen.queryByText("form.sections.schedule")).not.toBeInTheDocument(); }); it("closes edit stock modal on Escape", () => { const onCloseEditStockModal = vi.fn(); render( ); fireEvent.keyDown(document, { key: "Escape" }); expect(onCloseEditStockModal).toHaveBeenCalled(); }); }); describe("MedDetailModal with low stock", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows stock status for low stock", () => { const lowCoverage: Coverage = { name: "Test Med", medsLeft: 3, daysLeft: 3, depletionDate: "2024-01-05", depletionTime: Date.now() + 3 * 86400000, nextDose: null, }; render(); // Should render status indicator const _statusElements = document.querySelectorAll(".danger, .warning, .success"); // Status should be visible }); }); describe("MedDetailModal with refill history", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows refill history when expanded", () => { const refillHistory: RefillEntry[] = [ { id: 1, refillDate: new Date().toISOString(), packsAdded: 1, loosePillsAdded: 0, quantityAdded: 30 }, ]; render(); // Refill history should be visible const modal = document.querySelector(".modal-overlay"); expect(modal).toBeInTheDocument(); }); it("calls onRefillHistoryExpandedChange when toggle clicked", () => { const onRefillHistoryExpandedChange = vi.fn(); const refillHistory: RefillEntry[] = [ { id: 1, refillDate: new Date().toISOString(), packsAdded: 1, loosePillsAdded: 0, quantityAdded: 30 }, ]; render( ); // Click expand toggle if exists const expandButton = document.querySelector('[class*="expand"], [class*="toggle"]'); if (expandButton) { fireEvent.click(expandButton); } }); }); describe("MedDetailModal intake schedule usage display", () => { beforeEach(() => { vi.clearAllMocks(); }); it("does not multiply usage by personCount when intakes have per-intake takenBy", () => { // Two people at medication level, but each intake has its own takenBy const med: Medication = { ...mockMedication, takenBy: ["Alice", "Bob"], blisters: [ { usage: 1, every: 1, start: "2024-01-01T09:00:00" }, { usage: 1, every: 1, start: "2024-01-01T21:00:00" }, ], intakes: [ { usage: 1, every: 1, start: "2024-01-01T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false }, { usage: 1, every: 1, start: "2024-01-01T21:00:00", takenBy: "Bob", intakeRemindersEnabled: false }, ], }; render(); const rows = document.querySelectorAll(".med-schedule-row .med-schedule-usage"); // Each intake should show "1" in usage (not "2") rows.forEach((el) => { expect(el.textContent).toContain("1"); expect(el.textContent).not.toMatch(/^2\b/); }); }); it("multiplies usage by personCount for legacy blisters without per-intake takenBy", () => { // Two people at medication level, legacy blisters without intakes const med: Medication = { ...mockMedication, takenBy: ["Alice", "Bob"], blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }], // No intakes array - legacy format }; render(); const rows = document.querySelectorAll(".med-schedule-row .med-schedule-usage"); // Legacy: 1 pill * 2 people = "2 pills" expect(rows.length).toBe(1); expect(rows[0].textContent).toContain("2"); }); it("shows correct usage for single person with per-intake takenBy", () => { const med: Medication = { ...mockMedication, takenBy: ["Alice"], pillWeightMg: 500, blisters: [{ usage: 2, every: 1, start: "2024-01-01T09:00:00" }], intakes: [{ usage: 2, every: 1, start: "2024-01-01T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false }], }; render(); const rows = document.querySelectorAll(".med-schedule-row .med-schedule-usage"); expect(rows.length).toBe(1); // Should show "2 pills (1000 mg)" - usage=2, not multiplied expect(rows[0].textContent).toContain("2"); expect(rows[0].textContent).toContain("1000"); }); }); describe("MedDetailModal partial blister normalization", () => { beforeEach(() => { vi.clearAllMocks(); }); it("carries partial pills into full blisters when partial reaches pillsPerBlister", () => { const onEditStockFullBlistersChange = vi.fn(); const onEditStockPartialBlisterPillsChange = vi.fn(); const blisterMed: Medication = { ...mockMedication, packCount: 10, blistersPerPack: 5, pillsPerBlister: 5, looseTablets: 0, }; // full=12, partial=4 (one below pillsPerBlister) render( ); // Find the increment button for the partial blister pills stepper // The partial stepper is the second stepper in the modal const incrementButtons = document.querySelectorAll(".stepper-btn.increment"); const partialIncrementBtn = incrementButtons[1]; // full[0], partial[1], loose[2] expect(partialIncrementBtn).not.toBeDisabled(); // Press + on partial: 4 → 5 = pillsPerBlister → normalization carries to full fireEvent.click(partialIncrementBtn); // full should have been called with 13 (12 + 1 carry) expect(onEditStockFullBlistersChange).toHaveBeenCalledWith(13); // partial should have been called with 0 (5 % 5 = 0) expect(onEditStockPartialBlisterPillsChange).toHaveBeenCalledWith(0); }); it("does not carry partial pills into full when below pillsPerBlister", () => { const onEditStockFullBlistersChange = vi.fn(); const onEditStockPartialBlisterPillsChange = vi.fn(); const blisterMed: Medication = { ...mockMedication, packCount: 10, blistersPerPack: 5, pillsPerBlister: 5, looseTablets: 0, }; // full=12, partial=0 render( ); const incrementButtons = document.querySelectorAll(".stepper-btn.increment"); const partialIncrementBtn = incrementButtons[1]; fireEvent.click(partialIncrementBtn); // full should not change (1 partial pill with pbb=5 is NOT a carry) expect(onEditStockFullBlistersChange).toHaveBeenCalledWith(12); // partial should go to 1 expect(onEditStockPartialBlisterPillsChange).toHaveBeenCalledWith(1); }); }); describe("MedDetailModal stock overflow warning", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows overflow warning icon when stock exceeds blister package capacity", () => { const overflowCoverage: Coverage = { name: "Test Med", medsLeft: 49, daysLeft: 49, depletionDate: "2024-03-01", depletionTime: Date.now() + 49 * 86400000, nextDose: null, }; render(); // For blister meds, denominator is package capacity (not current stock), so overflow is shown. const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text"); expect(warningIcon).toBeInTheDocument(); }); it("does not show warning icon when stock is within package capacity", () => { render(); // packageSize = 30, currentStock = 25 < 30 const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text"); expect(warningIcon).not.toBeInTheDocument(); }); it("does not show warning icon when stock equals package capacity", () => { const exactCoverage: Coverage = { name: "Test Med", medsLeft: 30, daysLeft: 30, depletionDate: "2024-02-01", depletionTime: Date.now() + 30 * 86400000, nextDose: null, }; render(); // packageSize = 30, currentStock = 30 — equal, no warning const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text"); expect(warningIcon).not.toBeInTheDocument(); }); }); describe("MedDetailModal amount-based stock display", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows current liquid stock against configured structural capacity", () => { const liquidMed: Medication = { ...mockMedication, id: 20, name: "Liquid Multi", packageType: "liquid_container", packCount: 4, packageAmountValue: 150, packageAmountUnit: "ml", totalPills: 450, looseTablets: 450, }; const liquidCoverage: Coverage = { name: "Liquid Multi", medsLeft: 450, daysLeft: 45, depletionDate: "2024-04-01", depletionTime: Date.now() + 45 * 86400000, nextDose: null, }; render(); expect(screen.getByText("450 / 600 form.packageAmountUnitMl")).toBeInTheDocument(); expect(screen.queryByText("450 / 450 form.packageAmountUnitMl")).not.toBeInTheDocument(); }); }); describe("MedDetailModal bottle package type", () => { const bottleMed: Medication = { id: 2, name: "Bottle Med", genericName: null, packageType: "bottle", packCount: 0, blistersPerPack: 1, pillsPerBlister: 1, looseTablets: 80, totalPills: 100, takenBy: [], blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }], updatedAt: null, expiryDate: null, notes: null, }; const bottleCoverage: Coverage = { name: "Bottle Med", medsLeft: 80, daysLeft: 80, depletionDate: "2024-06-01", depletionTime: Date.now() + 80 * 86400000, nextDose: null, }; const bottleProps = { ...defaultProps, selectedMed: bottleMed, coverage: { all: [bottleCoverage] }, }; beforeEach(() => { vi.clearAllMocks(); }); it("does not show blister fields in stock info section", () => { render(); // Should show current stock expect(screen.getByText(/modal\.currentStock/i)).toBeInTheDocument(); // Should NOT show full blisters or open blister labels expect(screen.queryByText(/table\.fullBlisters/i)).not.toBeInTheDocument(); expect(screen.queryByText(/table\.openBlister/i)).not.toBeInTheDocument(); }); it("shows bottle type in package details section", () => { render(); // Should show package type as bottle expect(screen.getByText(/form\.packageTypeBottle/i)).toBeInTheDocument(); // Should show total capacity expect(screen.getByText(/form\.totalCapacity/i)).toBeInTheDocument(); }); it("shows pills-only refill modal for bottle type", () => { render(); // Should show pills to add label expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument(); // Should NOT show packs label in refill const _refillModal = document.querySelector(".refill-modal"); // Packs label should not be present for bottle type expect(screen.queryByText("refill.packs")).not.toBeInTheDocument(); }); it("parses bottle refill pills input", () => { const onRefillLooseChange = vi.fn(); render(); const input = document.querySelector(".refill-modal input[type='number']") as HTMLInputElement; fireEvent.change(input, { target: { value: "7" } }); expect(onRefillLooseChange).toHaveBeenCalledWith(7); }); it("uses zero fallback for invalid bottle refill input", () => { const onRefillLooseChange = vi.fn(); render(); const input = document.querySelector(".refill-modal input[type='number']") as HTMLInputElement; fireEvent.change(input, { target: { value: "" } }); expect(onRefillLooseChange).toHaveBeenCalledWith(0); }); it("shows looseTablets as total capacity fallback when totalPills is null (backward compat)", () => { // Old medications created before totalPills column existed const oldBottleMed: Medication = { ...bottleMed, totalPills: null, looseTablets: 180, }; const oldCoverage: Coverage = { name: "Bottle Med", medsLeft: 138, daysLeft: 138, depletionDate: "2024-06-01", depletionTime: Date.now() + 138 * 86400000, nextDose: null, }; render(); // Total Capacity should show 180 (looseTablets), not "—" const capacityLabel = screen.getByText(/form\.totalCapacity/i); const capacityValue = capacityLabel.closest(".med-detail-item")?.querySelector(".med-detail-value"); expect(capacityValue?.textContent).toBe("180"); }); it("shows total pills input in edit stock modal for bottle type", () => { render(); // Should show total pills label expect(screen.getByText(/editStock\.totalPills/i)).toBeInTheDocument(); // Should NOT show full blisters or partial blister labels expect(screen.queryByText(/editStock\.fullBlisters/i)).not.toBeInTheDocument(); expect(screen.queryByText(/editStock\.partialBlisterPills/i)).not.toBeInTheDocument(); }); });