346 lines
9.8 KiB
TypeScript
346 lines
9.8 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import ReportModal from "../../components/ReportModal";
|
|
import type { Medication } from "../../types";
|
|
import { formatDate, formatDateTime } from "../../utils/formatters";
|
|
|
|
function createMedication(overrides: Partial<Medication> = {}): Medication {
|
|
return {
|
|
id: 1,
|
|
name: "Aspirin",
|
|
genericName: "Acetylsalicylic acid",
|
|
takenBy: ["Alice"],
|
|
packageType: "blister",
|
|
packCount: 2,
|
|
blistersPerPack: 2,
|
|
pillsPerBlister: 10,
|
|
looseTablets: 0,
|
|
blisters: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00.000Z" }],
|
|
updatedAt: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("ReportModal", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("renders and closes when cancel is clicked", () => {
|
|
const onClose = vi.fn();
|
|
render(<ReportModal isOpen={true} onClose={onClose} medications={[createMedication()]} />);
|
|
|
|
expect(screen.getByText(/report\.title/i)).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: /common\.close/i }));
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("generates text report and closes modal", async () => {
|
|
const onClose = vi.fn();
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
1: {
|
|
dosesTaken: 2,
|
|
dosesSkipped: 0,
|
|
firstDoseAt: "2026-01-01T08:00:00.000Z",
|
|
lastDoseAt: "2026-01-02T08:00:00.000Z",
|
|
refills: [],
|
|
},
|
|
}),
|
|
});
|
|
|
|
render(<ReportModal isOpen={true} onClose={onClose} medications={[createMedication()]} />);
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatTxt/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
"/api/medications/report-data",
|
|
expect.objectContaining({ method: "POST" })
|
|
);
|
|
});
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
expect(URL.createObjectURL).toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders shared formatter output in exported text reports", async () => {
|
|
const onClose = vi.fn();
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
1: {
|
|
dosesTaken: 1,
|
|
automaticDosesTaken: 0,
|
|
dosesSkipped: 0,
|
|
firstDoseAt: "2026-02-03T12:00:00.000Z",
|
|
lastDoseAt: null,
|
|
refills: [],
|
|
},
|
|
}),
|
|
});
|
|
|
|
render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({
|
|
medicationStartDate: "2026-02-01",
|
|
blisters: [{ usage: 1, every: 1, start: "2026-02-02T08:30:00.000Z" }],
|
|
}),
|
|
]}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatTxt/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(URL.createObjectURL).toHaveBeenCalled();
|
|
});
|
|
|
|
const [blob] = (URL.createObjectURL as ReturnType<typeof vi.fn>).mock.calls.at(-1) ?? [];
|
|
expect(blob).toBeInstanceOf(Blob);
|
|
|
|
const content = await (blob as Blob).text();
|
|
|
|
expect(content).toContain(formatDate("2026-02-01"));
|
|
expect(content).toContain(formatDateTime("2026-02-02T08:30:00.000Z"));
|
|
expect(content).toContain(formatDate("2026-02-03T12:00:00.000Z"));
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("exports bottle current stock separately from configured capacity", async () => {
|
|
const onClose = vi.fn();
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
1: {
|
|
dosesTaken: 0,
|
|
automaticDosesTaken: 0,
|
|
dosesSkipped: 0,
|
|
firstDoseAt: null,
|
|
lastDoseAt: null,
|
|
refills: [],
|
|
},
|
|
}),
|
|
});
|
|
|
|
render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({
|
|
packageType: "bottle",
|
|
packCount: 0,
|
|
blistersPerPack: 1,
|
|
pillsPerBlister: 1,
|
|
totalPills: 100,
|
|
looseTablets: 20,
|
|
stockAdjustment: 50,
|
|
}),
|
|
]}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatTxt/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(URL.createObjectURL).toHaveBeenCalled();
|
|
});
|
|
|
|
const [blob] = (URL.createObjectURL as ReturnType<typeof vi.fn>).mock.calls.at(-1) ?? [];
|
|
const content = await (blob as Blob).text();
|
|
|
|
expect(content).toContain("report.docTotalCapacity: 100");
|
|
expect(content).toContain("report.docCurrentStock: 70 common.pills");
|
|
expect(content).not.toContain("report.docCurrentStock: 100 common.pills");
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("generates printable report when PDF format is selected", async () => {
|
|
const onClose = vi.fn();
|
|
const mockWrite = vi.fn();
|
|
const mockClose = vi.fn();
|
|
const mockPrint = vi.fn();
|
|
const openSpy = vi.spyOn(window, "open").mockReturnValue({
|
|
document: {
|
|
write: mockWrite,
|
|
close: mockClose,
|
|
},
|
|
onload: null,
|
|
print: mockPrint,
|
|
} as unknown as Window);
|
|
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
1: {
|
|
dosesTaken: 1,
|
|
automaticDosesTaken: 0,
|
|
dosesSkipped: 0,
|
|
firstDoseAt: "2026-03-03T12:00:00.000Z",
|
|
lastDoseAt: null,
|
|
refills: [
|
|
{
|
|
packsAdded: 1,
|
|
loosePillsAdded: 0,
|
|
quantityAdded: 20,
|
|
usedPrescription: false,
|
|
refillDate: "2026-03-04",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
});
|
|
|
|
render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({
|
|
medicationStartDate: "2026-03-01",
|
|
blisters: [{ usage: 1, every: 1, start: "2026-03-02T08:30:00.000Z" }],
|
|
}),
|
|
]}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(openSpy).toHaveBeenCalled();
|
|
expect(mockWrite).toHaveBeenCalled();
|
|
expect(mockClose).toHaveBeenCalled();
|
|
});
|
|
|
|
const [html] = mockWrite.mock.calls.at(-1) ?? [];
|
|
expect(html).toContain(formatDate("2026-03-01"));
|
|
expect(html).toContain(formatDateTime("2026-03-02T08:30:00.000Z"));
|
|
expect(html).toContain(formatDate("2026-03-03T12:00:00.000Z"));
|
|
expect(html).toContain(formatDate("2026-03-04"));
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("shows person filter and supports deselect/select all", () => {
|
|
const onClose = vi.fn();
|
|
render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({ id: 1, name: "Alice Med", takenBy: ["Alice"] }),
|
|
createMedication({ id: 2, name: "Bob Med", takenBy: ["Bob"] }),
|
|
]}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/report\.filterByPerson/i)).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "Alice" }));
|
|
expect(screen.getByText("Alice Med")).toBeInTheDocument();
|
|
expect(screen.queryByText("Bob Med")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.deselectAll/i }));
|
|
expect(screen.getByRole("button", { name: /report\.generate/i })).toBeDisabled();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.selectAll/i }));
|
|
expect(screen.getByRole("button", { name: /report\.generate/i })).not.toBeDisabled();
|
|
});
|
|
|
|
it("sends the selected person filter with the report request and clears it for all people", async () => {
|
|
const onClose = vi.fn();
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
1: {
|
|
dosesTaken: 2,
|
|
automaticDosesTaken: 0,
|
|
dosesSkipped: 1,
|
|
firstDoseAt: "2026-01-01T08:00:00.000Z",
|
|
lastDoseAt: "2026-01-02T08:00:00.000Z",
|
|
refills: [],
|
|
},
|
|
2: {
|
|
dosesTaken: 1,
|
|
automaticDosesTaken: 0,
|
|
dosesSkipped: 0,
|
|
firstDoseAt: "2026-01-01T08:00:00.000Z",
|
|
lastDoseAt: "2026-01-02T08:00:00.000Z",
|
|
refills: [],
|
|
},
|
|
}),
|
|
});
|
|
|
|
const firstRender = render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({ id: 1, name: "Alice Med", takenBy: ["Alice"] }),
|
|
createMedication({ id: 2, name: "Bob Med", takenBy: ["Bob"] }),
|
|
]}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "Alice" }));
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatTxt/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
"/api/medications/report-data",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
body: JSON.stringify({ medicationIds: [1], takenByFilter: ["Alice"] }),
|
|
})
|
|
);
|
|
});
|
|
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockClear();
|
|
firstRender.unmount();
|
|
render(
|
|
<ReportModal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
medications={[
|
|
createMedication({ id: 1, name: "Alice Med", takenBy: ["Alice"] }),
|
|
createMedication({ id: 2, name: "Bob Med", takenBy: ["Bob"] }),
|
|
]}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatTxt/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
"/api/medications/report-data",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
body: JSON.stringify({ medicationIds: [1, 2], takenByFilter: undefined }),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
it("generates markdown report and keeps modal open on fetch error", async () => {
|
|
const onClose = vi.fn();
|
|
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: false });
|
|
|
|
render(<ReportModal isOpen={true} onClose={onClose} medications={[createMedication()]} />);
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /report\.formatMd/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /report\.generate/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
});
|