mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test(meds): coverage for compose-mode
Five new payload-helper cases pin the multi-schedule encoder, the hydration that opens on the list view, the add and remove guards, and the schedule-id preservation on edit. A new Playwright spec walks a two-schedule create flow end-to-end and verifies both summary cards land before save. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8578924f14
commit
dba844235c
2 files changed
+339
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import { expect, test, type Route } from "@playwright/test";
|
||||
|
||||
import {
|
||||
STUB_MEDICATION_ID,
|
||||
clickNext,
|
||||
clickSave,
|
||||
expectMedicationOnList,
|
||||
expectRedirectToMedication,
|
||||
expectStep,
|
||||
fillStep1Name,
|
||||
fillStep3Dose,
|
||||
openCreateWizard,
|
||||
pickCadenceRow,
|
||||
pickTreatmentRow,
|
||||
stubDashboardAnalytics,
|
||||
} from "./medications-wizard-helpers";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.5.4 — Medication wizard end-to-end — compose-mode.
|
||||
*
|
||||
* Walks the create flow through to Step 8, then taps
|
||||
* "Weiteren Zeitplan hinzufügen" to append a second weekly Wednesday
|
||||
* schedule on the same medication. The list view surfaces both
|
||||
* summary cards before save, and the POST body carries both
|
||||
* schedules so the route can persist them in parallel.
|
||||
*
|
||||
* Use case the maintainer cares about: short-acting + long-acting
|
||||
* insulin on a single medication. Today the wizard collapses to one
|
||||
* schedule on save; compose-mode closes that data-loss gap.
|
||||
*/
|
||||
test.describe("medication wizard — compose-mode", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test("creates a 2-schedule medication end-to-end", async ({ page }) => {
|
||||
await stubDashboardAnalytics(page);
|
||||
|
||||
const listEntry = {
|
||||
id: STUB_MEDICATION_ID,
|
||||
name: "Insulin",
|
||||
dose: "10 IE",
|
||||
category: "DIABETES",
|
||||
active: true,
|
||||
notificationsEnabled: true,
|
||||
pausedAt: null,
|
||||
lastTakenAt: null,
|
||||
startsOn: null,
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{
|
||||
id: "sch_e2e_1",
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
label: null,
|
||||
dose: null,
|
||||
daysOfWeek: null,
|
||||
timesOfDay: ["08:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
reminderGraceMinutes: null,
|
||||
},
|
||||
{
|
||||
id: "sch_e2e_2",
|
||||
windowStart: "20:00",
|
||||
windowEnd: "21:00",
|
||||
label: null,
|
||||
dose: null,
|
||||
daysOfWeek: null,
|
||||
timesOfDay: ["20:00"],
|
||||
rrule: "FREQ=WEEKLY;BYDAY=WE",
|
||||
rollingIntervalDays: null,
|
||||
reminderGraceMinutes: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const capture: { postBody: unknown } = { postBody: null };
|
||||
void page.route("**/api/medications**", async (route: Route) => {
|
||||
const req = route.request();
|
||||
const url = new URL(req.url());
|
||||
if (url.pathname !== "/api/medications") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
if (req.method() === "POST") {
|
||||
capture.postBody = JSON.parse(req.postData() ?? "null");
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: { ...listEntry }, error: null }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (req.method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: [listEntry], error: null }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.fallback();
|
||||
});
|
||||
|
||||
await openCreateWizard(page);
|
||||
|
||||
// Steps 1-4 — medication-global.
|
||||
await fillStep1Name(page, { name: "Insulin" });
|
||||
await clickNext(page);
|
||||
await expectStep(page, 2);
|
||||
await pickTreatmentRow(page, "diabetes");
|
||||
await clickNext(page);
|
||||
await expectStep(page, 3);
|
||||
await fillStep3Dose(page, { amount: "10" });
|
||||
await clickNext(page);
|
||||
await expectStep(page, 4);
|
||||
await clickNext(page);
|
||||
|
||||
// Steps 5-7 — first schedule, daily cadence.
|
||||
await expectStep(page, 5, 8);
|
||||
await pickCadenceRow(page, "daily");
|
||||
await expectStep(page, 5, 7);
|
||||
await clickNext(page);
|
||||
await expectStep(page, 6, 7);
|
||||
await clickNext(page);
|
||||
|
||||
// Step 8 — list view with the first schedule rendered. Tap
|
||||
// "Weiteren Zeitplan hinzufügen" to append a second draft.
|
||||
await expectStep(page, 7, 7);
|
||||
let cards = page.locator('[data-slot="wizard-schedule-card"]');
|
||||
await expect(cards).toHaveCount(1);
|
||||
await page.locator('[data-slot="wizard-schedule-add"]').click();
|
||||
|
||||
// Second schedule — weekdays on Wednesday.
|
||||
await expectStep(page, 5);
|
||||
await pickCadenceRow(page, "weekdays");
|
||||
await clickNext(page);
|
||||
await expectStep(page, 6);
|
||||
const weChip = page.locator(
|
||||
'[data-slot="wizard-weekday-chip"][data-token="WE"]',
|
||||
);
|
||||
if ((await weChip.getAttribute("data-active")) === "false") {
|
||||
await weChip.click();
|
||||
}
|
||||
// Default MO chip — toggle off so the rrule is WE-only.
|
||||
const moChip = page.locator(
|
||||
'[data-slot="wizard-weekday-chip"][data-token="MO"]',
|
||||
);
|
||||
if ((await moChip.getAttribute("data-active")) === "true") {
|
||||
await moChip.click();
|
||||
}
|
||||
await clickNext(page);
|
||||
await expectStep(page, 7);
|
||||
await clickNext(page);
|
||||
|
||||
// Back on Step 8 — both summary cards should land before save.
|
||||
await expectStep(page, 8);
|
||||
cards = page.locator('[data-slot="wizard-schedule-card"]');
|
||||
await expect(cards).toHaveCount(2);
|
||||
|
||||
await clickSave(page);
|
||||
|
||||
await expectRedirectToMedication(page, STUB_MEDICATION_ID);
|
||||
expect(capture.postBody).toMatchObject({
|
||||
name: "Insulin",
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{ rrule: "FREQ=DAILY" },
|
||||
{ rrule: "FREQ=WEEKLY;BYDAY=WE" },
|
||||
],
|
||||
});
|
||||
|
||||
await expectMedicationOnList(page, {
|
||||
name: "Insulin",
|
||||
withNextDueChip: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
addSchedule,
|
||||
buildCreateBody,
|
||||
commitActiveDraft,
|
||||
emptyWizardPayload,
|
||||
hydrateWizardPayload,
|
||||
landingStepForEdit,
|
||||
type MedicationPayload,
|
||||
progressIndices,
|
||||
removeSchedule,
|
||||
rowFromTreatment,
|
||||
setActiveSchedule,
|
||||
summariseCadence,
|
||||
validateStep,
|
||||
type WizardPayload,
|
||||
@@ -576,3 +581,158 @@ describe("hydrateWizardPayload", () => {
|
||||
expect(out.subControls.intervalWeeks).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose-mode — multi-schedule encoder + hydrator", () => {
|
||||
it("buildCreateBody emits every schedule for a 2-schedule medication", () => {
|
||||
// Active draft is a recurring daily schedule (the wizard flat
|
||||
// mirror). A second draft sits in the list as a weekly Wednesday
|
||||
// schedule. Both must land in the encoded body.
|
||||
const base = withCadence("daily");
|
||||
const second = withCadence("weekdays", { weekdays: ["WE"] });
|
||||
const payload: WizardPayload = {
|
||||
...base,
|
||||
treatmentRow: "diabetes",
|
||||
timesOfDay: ["08:00"],
|
||||
startsOn: new Date(Date.UTC(2026, 4, 28)),
|
||||
schedules: [
|
||||
// First draft mirrors the flat fields — buildCreateBody
|
||||
// commits the active draft before encoding.
|
||||
{
|
||||
mode: base.mode,
|
||||
cadence: base.cadence,
|
||||
subControls: base.subControls,
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
{
|
||||
mode: second.mode,
|
||||
cadence: second.cadence,
|
||||
subControls: second.subControls,
|
||||
timesOfDay: ["20:00"],
|
||||
},
|
||||
],
|
||||
activeScheduleIndex: 0,
|
||||
};
|
||||
const body = buildCreateBody(payload);
|
||||
expect(body.schedules).toHaveLength(2);
|
||||
expect(body.schedules[0].rrule).toBe("FREQ=DAILY");
|
||||
expect(body.schedules[0].timesOfDay).toEqual(["08:00"]);
|
||||
expect(body.schedules[1].rrule).toBe("FREQ=WEEKLY;BYDAY=WE");
|
||||
expect(body.schedules[1].timesOfDay).toEqual(["20:00"]);
|
||||
});
|
||||
|
||||
it("buildUpdateBody preserves schedule.id on edit", () => {
|
||||
// Hydrate from a multi-schedule medication, then encode; every
|
||||
// schedule must carry its persisted `id` back through.
|
||||
const initial: MedicationPayload = {
|
||||
id: "med_compose",
|
||||
name: "Insulin",
|
||||
dose: "10 IE",
|
||||
category: "DIABETES",
|
||||
treatmentClass: "GENERIC",
|
||||
notificationsEnabled: true,
|
||||
startsOn: new Date(Date.UTC(2026, 0, 1)),
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{
|
||||
id: "sch_short",
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
timesOfDay: ["08:00", "12:00", "18:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
},
|
||||
{
|
||||
id: "sch_long",
|
||||
windowStart: "22:00",
|
||||
windowEnd: "23:00",
|
||||
timesOfDay: ["22:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
const payload = hydrateWizardPayload(initial);
|
||||
const body = buildCreateBody(payload);
|
||||
expect(body.schedules).toHaveLength(2);
|
||||
expect(body.schedules[0].id).toBe("sch_short");
|
||||
expect(body.schedules[1].id).toBe("sch_long");
|
||||
});
|
||||
|
||||
it("hydrateWizardPayload reads N schedules and lands on Step 8 when N > 1", () => {
|
||||
const initial: MedicationPayload = {
|
||||
id: "med_compose",
|
||||
name: "Insulin",
|
||||
dose: "10 IE",
|
||||
category: "DIABETES",
|
||||
treatmentClass: "GENERIC",
|
||||
notificationsEnabled: true,
|
||||
startsOn: new Date(Date.UTC(2026, 0, 1)),
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{
|
||||
id: "sch_a",
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
timesOfDay: ["08:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
},
|
||||
{
|
||||
id: "sch_b",
|
||||
windowStart: "22:00",
|
||||
windowEnd: "23:00",
|
||||
timesOfDay: ["22:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
const payload = hydrateWizardPayload(initial);
|
||||
expect(payload.schedules).toHaveLength(2);
|
||||
expect(payload.activeScheduleIndex).toBe(0);
|
||||
expect(payload.schedules[0].id).toBe("sch_a");
|
||||
expect(payload.schedules[1].id).toBe("sch_b");
|
||||
expect(landingStepForEdit(payload)).toBe(8);
|
||||
|
||||
// Single-schedule still lands on Step 1.
|
||||
const single = hydrateWizardPayload({
|
||||
...initial,
|
||||
schedules: [initial.schedules[0]],
|
||||
});
|
||||
expect(landingStepForEdit(single)).toBe(1);
|
||||
});
|
||||
|
||||
it("removeSchedule refuses when schedules.length === 1", () => {
|
||||
const payload = emptyWizardPayload();
|
||||
expect(payload.schedules).toHaveLength(1);
|
||||
const after = removeSchedule(payload, 0);
|
||||
// Returns the same payload reference / shape — no mutation.
|
||||
expect(after.schedules).toHaveLength(1);
|
||||
expect(after.activeScheduleIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("addSchedule appends and bumps activeScheduleIndex", () => {
|
||||
const payload = emptyWizardPayload();
|
||||
const after = addSchedule(payload);
|
||||
expect(after.schedules).toHaveLength(2);
|
||||
expect(after.activeScheduleIndex).toBe(1);
|
||||
|
||||
// setActiveSchedule routes the flat mirror back to the first
|
||||
// draft, and a removeSchedule then drops the second.
|
||||
const back = setActiveSchedule(after, 0);
|
||||
expect(back.activeScheduleIndex).toBe(0);
|
||||
const dropped = removeSchedule(back, 1);
|
||||
expect(dropped.schedules).toHaveLength(1);
|
||||
|
||||
// commitActiveDraft writes the flat mirror onto the active slot.
|
||||
const edited = commitActiveDraft({
|
||||
...after,
|
||||
timesOfDay: ["20:00"],
|
||||
});
|
||||
expect(edited.schedules[after.activeScheduleIndex].timesOfDay).toEqual([
|
||||
"20:00",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user