feat: add shared overview and harden frontend session state (#407)

This commit is contained in:
Daniel Volz
2026-03-10 06:26:03 +01:00
committed by GitHub
parent 733fe2f38a
commit 105eb7bc0d
37 changed files with 3281 additions and 1138 deletions
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
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";
@@ -410,6 +410,112 @@ describe("MedDetailModal with refill modal", () => {
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(<MedDetailModal {...defaultProps} selectedMed={liquidMed} showRefillModal={true} refillLoose={150} />);
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(
<MedDetailModal
{...defaultProps}
selectedMed={liquidMed}
showRefillModal={true}
onRefillLooseChange={onRefillLooseChange}
onRefillPacksChange={onRefillPacksChange}
refillLoose={0}
/>
);
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(<MedDetailModal {...defaultProps} selectedMed={tubeMed} showRefillModal={true} refillLoose={150} />);
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(
<MedDetailModal
{...defaultProps}
selectedMed={tubeMed}
showRefillModal={true}
onRefillLooseChange={onRefillLooseChange}
onRefillPacksChange={onRefillPacksChange}
refillLoose={0}
/>
);
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", () => {
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ShareDialog } from "../../components/ShareDialog";
@@ -68,8 +68,9 @@ describe("ShareDialog", () => {
it("shows generated link", () => {
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
const input = screen.getByRole("textbox");
expect(input).toHaveValue("http://example.com/share/abc123");
const inputs = screen.getAllByRole("textbox") as HTMLInputElement[];
expect(inputs[0]).toHaveValue("http://example.com/share/abc123");
expect(inputs[1]).toHaveValue("http://example.com/share/abc123/overview");
});
it("calls onCopyShareLink when copy button is clicked", () => {
@@ -85,13 +86,23 @@ describe("ShareDialog", () => {
it("selects link text when input is clicked", () => {
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
const input = screen.getByRole("textbox") as HTMLInputElement;
const input = screen.getAllByRole("textbox")[0] as HTMLInputElement;
const selectMock = vi.fn();
input.select = selectMock;
fireEvent.click(input);
expect(selectMock).toHaveBeenCalled();
});
it("copies overview link when overview copy button is clicked", async () => {
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
fireEvent.click(screen.getByRole("button", { name: /share\.copyOverviewLink/i }));
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("http://example.com/share/abc123/overview");
});
});
it("calls person and period change callbacks", () => {
render(<ShareDialog {...defaultProps} />);
@@ -294,4 +294,143 @@ describe("UserFilterModal", () => {
expect(screen.queryByText("Med2")).not.toBeInTheDocument();
expect(screen.getByText("Med3")).toBeInTheDocument();
});
it("renders tube intakes as applications and stock in g", () => {
const onClose = vi.fn();
const onOpenMedDetail = vi.fn();
const tubeMedication: Medication = {
...mockMedication,
id: 10,
name: "Tube Med",
genericName: "Tube Generic",
packageType: "tube",
totalPills: 600,
looseTablets: 600,
intakes: [
{
usage: 1,
every: 1,
start: "2024-01-01T21:04:00",
takenBy: "John",
intakeRemindersEnabled: true,
},
],
};
const tubeCoverage: Coverage = {
name: "Tube Med",
medsLeft: 600,
daysLeft: null,
depletionDate: null,
depletionTime: null,
nextDose: null,
};
render(
<UserFilterModal
selectedUser="John"
meds={[tubeMedication]}
coverage={{ all: [tubeCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
expect(screen.getByText(/form\.blisters\.applications_1/)).toBeInTheDocument();
expect(screen.getByText("600/600 form.packageAmountUnitG")).toBeInTheDocument();
expect(screen.queryByText(/600\/600 .*common\.pills/)).not.toBeInTheDocument();
});
it("renders liquid container intakes and stock in ml", () => {
const onClose = vi.fn();
const onOpenMedDetail = vi.fn();
const liquidMedication: Medication = {
...mockMedication,
id: 11,
name: "Liquid Container",
genericName: "Liquid Generic",
packageType: "liquid_container",
totalPills: 150,
looseTablets: 150,
intakes: [
{
usage: 2,
every: 1,
start: "2024-01-01T09:32:00",
intakeUnit: "ml",
takenBy: "John",
intakeRemindersEnabled: true,
},
],
};
const liquidCoverage: Coverage = {
name: "Liquid Container",
medsLeft: 0,
daysLeft: 0,
depletionDate: null,
depletionTime: null,
nextDose: null,
};
render(
<UserFilterModal
selectedUser="John"
meds={[liquidMedication]}
coverage={{ all: [liquidCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
expect(screen.getByText(/2 form\.packageAmountUnitMl common\.daily/)).toBeInTheDocument();
expect(screen.getByText("0/150 form.packageAmountUnitMl")).toBeInTheDocument();
expect(screen.queryByText(/0\/150 .*common\.pills/)).not.toBeInTheDocument();
});
it("renders medicationForm liquid as ml in modal fallback", () => {
const onClose = vi.fn();
const onOpenMedDetail = vi.fn();
const legacyLiquidMedication: Medication = {
...mockMedication,
id: 12,
name: "Legacy Liquid",
medicationForm: "liquid",
packageType: "bottle",
totalPills: 100,
looseTablets: 100,
blisters: [{ usage: 1, every: 1, start: "2024-01-01T10:00:00" }],
};
const legacyLiquidCoverage: Coverage = {
name: "Legacy Liquid",
medsLeft: 40,
daysLeft: 10,
depletionDate: null,
depletionTime: null,
nextDose: null,
};
render(
<UserFilterModal
selectedUser="John"
meds={[legacyLiquidMedication]}
coverage={{ all: [legacyLiquidCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
expect(screen.getByText(/1 form\.packageAmountUnitMl common\.daily/)).toBeInTheDocument();
expect(screen.getByText("40/100 form.packageAmountUnitMl")).toBeInTheDocument();
});
});