fix: dose tracking broken for per-intake takenBy and after medication edits (#100)
- Remove broken isDoseFromPreviousSchedule that falsely dismissed all past doses after any medication edit (compared dateOnlyMs < updatedAt incorrectly) - Fix takenBy normalization in AppContext: event.takenBy (string|null) was passed through as-is via || operator instead of being properly converted to string[] - Fix DashboardPage: 5 locations treated dose.takenBy as single string instead of iterating the array, causing per-person dose tracking to silently fail - Extract isDoseDismissed and computeMissedPastDoseIds as pure testable functions from AppContext.tsx into utils/schedule.ts - Update SharedSchedule.tsx to use shared isDoseDismissed from utils - Add 22 regression tests covering isDoseDismissed, computeMissedPastDoseIds, and full dose-tracking-survives-medication-edit workflows - Add 'fix bugs, don't test around them' rule to copilot instructions
This commit is contained in:
@@ -3,9 +3,11 @@ import type { Coverage, Medication, StockThresholds } from "../../types";
|
||||
import {
|
||||
buildSchedulePreview,
|
||||
calculateCoverage,
|
||||
computeMissedPastDoseIds,
|
||||
getNextReminderForMed,
|
||||
getReminderStatusText,
|
||||
getStockStatus,
|
||||
isDoseDismissed,
|
||||
} from "../../utils/schedule";
|
||||
|
||||
describe("buildSchedulePreview", () => {
|
||||
@@ -151,6 +153,80 @@ describe("buildSchedulePreview", () => {
|
||||
expect(result.events[i].when).toBeGreaterThanOrEqual(result.events[i - 1].when);
|
||||
}
|
||||
});
|
||||
|
||||
it("dose IDs remain stable when only intake time changes (regression test for dose tracking)", () => {
|
||||
// This is a critical regression test: changing the time-of-day for an intake
|
||||
// must NOT change dose IDs for past days, so that taken-dose tracking survives edits.
|
||||
const medsWithMorningTime: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const medsWithEveningTime: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T21:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medsWithMorningTime, "en", true);
|
||||
const resultAfter = buildSchedulePreview(medsWithEveningTime, "en", true);
|
||||
|
||||
// Get past dose IDs from both schedules
|
||||
const pastIdsBefore = resultBefore.events.filter((e) => e.isPast).map((e) => e.id);
|
||||
const pastIdsAfter = resultAfter.events.filter((e) => e.isPast).map((e) => e.id);
|
||||
|
||||
expect(pastIdsBefore.length).toBeGreaterThan(0);
|
||||
// All past dose IDs must match — changing time must not break dose tracking
|
||||
expect(pastIdsAfter).toEqual(pastIdsBefore);
|
||||
});
|
||||
|
||||
it("dose IDs use date-only timestamp, not full datetime", () => {
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-14T15:30:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(meds, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBeGreaterThan(0);
|
||||
|
||||
// Each dose ID should use a midnight timestamp (date-only), not the intake time
|
||||
for (const event of pastEvents) {
|
||||
const parts = event.id.split("-");
|
||||
const timestampMs = parseInt(parts[2], 10);
|
||||
const date = new Date(timestampMs);
|
||||
expect(date.getHours()).toBe(0);
|
||||
expect(date.getMinutes()).toBe(0);
|
||||
expect(date.getSeconds()).toBe(0);
|
||||
expect(date.getMilliseconds()).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateCoverage", () => {
|
||||
@@ -573,3 +649,501 @@ describe("getReminderStatusText", () => {
|
||||
expect(result.lines.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// isDoseDismissed
|
||||
// =============================================================================
|
||||
|
||||
describe("isDoseDismissed", () => {
|
||||
it("returns false when dismissedUntilDate is undefined", () => {
|
||||
expect(isDoseDismissed("1-0-1710028800000", undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for dose IDs with fewer than 3 parts", () => {
|
||||
expect(isDoseDismissed("1-0", "2024-03-15")).toBe(false);
|
||||
expect(isDoseDismissed("1", "2024-03-15")).toBe(false);
|
||||
expect(isDoseDismissed("", "2024-03-15")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for dose IDs with non-numeric timestamp", () => {
|
||||
expect(isDoseDismissed("1-0-abc", "2024-03-15")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when dose date is before dismissedUntil", () => {
|
||||
// March 10 midnight UTC = 1710028800000
|
||||
const march10midnight = new Date("2024-03-10T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}`, "2024-03-14")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when dose date equals dismissedUntil", () => {
|
||||
const march14midnight = new Date("2024-03-14T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march14midnight}`, "2024-03-14")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when dose date is after dismissedUntil", () => {
|
||||
const march15midnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march15midnight}`, "2024-03-14")).toBe(false);
|
||||
});
|
||||
|
||||
it("works with person-suffixed dose IDs", () => {
|
||||
// Dose ID: medId-intakeIdx-timestamp-person
|
||||
const march10midnight = new Date("2024-03-10T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}-John`, "2024-03-14")).toBe(true);
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}-John`, "2024-03-09")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles single-digit months and days correctly", () => {
|
||||
// January 5 = should produce "2024-01-05"
|
||||
const jan5 = new Date("2024-01-05T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${jan5}`, "2024-01-05")).toBe(true);
|
||||
expect(isDoseDismissed(`1-0-${jan5}`, "2024-01-04")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// computeMissedPastDoseIds
|
||||
// =============================================================================
|
||||
|
||||
describe("computeMissedPastDoseIds", () => {
|
||||
// Helper: create a past day with dose IDs
|
||||
function makePastDay(
|
||||
medName: string,
|
||||
doses: Array<{ id: string; takenBy?: string[] }>
|
||||
): { meds: Array<{ medName: string; doses: Array<{ id: string; takenBy: string[] }> }> } {
|
||||
return {
|
||||
meds: [
|
||||
{
|
||||
medName,
|
||||
doses: doses.map((d) => ({ id: d.id, takenBy: d.takenBy ?? [] })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
it("returns all past dose IDs when none are taken or dismissed", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
expect(result).toEqual([`1-0-${march10}`, `1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes taken doses", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const taken = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([`1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes individually dismissed doses", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const dismissed = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), dismissed);
|
||||
expect(result).toEqual([`1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes doses on or before medication dismissedUntil date", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const march13 = new Date("2024-03-13T00:00:00").getTime();
|
||||
const march14 = new Date("2024-03-14T00:00:00").getTime();
|
||||
const pastDays = [
|
||||
makePastDay("Aspirin", [
|
||||
{ id: `1-0-${march10}` },
|
||||
{ id: `1-0-${march11}` },
|
||||
{ id: `1-0-${march13}` },
|
||||
{ id: `1-0-${march14}` },
|
||||
]),
|
||||
];
|
||||
// Dismiss all doses up through March 12
|
||||
const meds = [{ name: "Aspirin", dismissedUntil: "2024-03-12" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
// March 10 & 11 are dismissed (≤ March 12), March 13 & 14 remain missed
|
||||
expect(result).toEqual([`1-0-${march13}`, `1-0-${march14}`]);
|
||||
});
|
||||
|
||||
it("expands takenBy people into separate dose IDs", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("SharedMed", [{ id: `1-0-${march10}`, takenBy: ["Alice", "Bob"] }])];
|
||||
const meds = [{ name: "SharedMed" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
expect(result).toEqual([`1-0-${march10}-Alice`, `1-0-${march10}-Bob`]);
|
||||
});
|
||||
|
||||
it("excludes expanded person dose IDs that are taken", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("SharedMed", [{ id: `1-0-${march10}`, takenBy: ["Alice", "Bob"] }])];
|
||||
const meds = [{ name: "SharedMed" }];
|
||||
// Alice took it, Bob didn't
|
||||
const taken = new Set([`1-0-${march10}-Alice`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([`1-0-${march10}-Bob`]);
|
||||
});
|
||||
|
||||
it("returns empty array when all doses are taken", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const taken = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles multiple medications independently", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [
|
||||
{
|
||||
meds: [
|
||||
{ medName: "Aspirin", doses: [{ id: `1-0-${march10}`, takenBy: [] as string[] }] },
|
||||
{ medName: "Vitamin D", doses: [{ id: `2-0-${march10}`, takenBy: [] as string[] }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
// Aspirin dismissed, Vitamin D not
|
||||
const meds = [{ name: "Aspirin", dismissedUntil: "2024-03-15" }, { name: "Vitamin D" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
// Only Vitamin D's dose remains missed
|
||||
expect(result).toEqual([`2-0-${march10}`]);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Dose Tracking Regression Tests
|
||||
// =============================================================================
|
||||
|
||||
describe("dose tracking survives medication edits (regression)", () => {
|
||||
beforeEach(() => {
|
||||
vi.setSystemTime(new Date("2024-03-15T12:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("taken dose remains taken after changing intake time", () => {
|
||||
// === BEFORE EDIT: medication with 09:00 morning intake ===
|
||||
const medBefore: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medBefore, "en", true);
|
||||
const pastBefore = resultBefore.events.filter((e) => e.isPast);
|
||||
expect(pastBefore.length).toBe(5); // March 10, 11, 12, 13, 14
|
||||
|
||||
// User marks March 12 dose as taken
|
||||
const march12Dose = pastBefore.find((e) => {
|
||||
const d = new Date(e.when);
|
||||
return d.getDate() === 12;
|
||||
})!;
|
||||
expect(march12Dose).toBeDefined();
|
||||
const takenDoses = new Set([march12Dose.id]);
|
||||
|
||||
// Compute missed doses BEFORE edit
|
||||
const pastDaysBefore = groupEventsIntoPastDays(pastBefore);
|
||||
const missedBefore = computeMissedPastDoseIds(pastDaysBefore, medBefore, takenDoses, new Set());
|
||||
expect(missedBefore).not.toContain(march12Dose.id); // March 12 is taken
|
||||
expect(missedBefore).toHaveLength(4); // March 10, 11, 13, 14 are missed
|
||||
|
||||
// === AFTER EDIT: change intake time to 21:00 evening + updatedAt set ===
|
||||
const medAfter: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T21:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultAfter = buildSchedulePreview(medAfter, "en", true);
|
||||
const pastAfter = resultAfter.events.filter((e) => e.isPast);
|
||||
expect(pastAfter.length).toBe(5); // Same 5 past days
|
||||
|
||||
// Compute missed doses AFTER edit — with same takenDoses set
|
||||
const pastDaysAfter = groupEventsIntoPastDays(pastAfter);
|
||||
const missedAfter = computeMissedPastDoseIds(pastDaysAfter, medAfter, takenDoses, new Set());
|
||||
|
||||
// === CRITICAL ASSERTION: March 12 is STILL marked as taken ===
|
||||
expect(missedAfter).not.toContain(march12Dose.id);
|
||||
// The other 4 days remain missed
|
||||
expect(missedAfter).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("updatedAt does NOT filter out past doses as false dismissals", () => {
|
||||
// This is the core bug that was fixed: isDoseFromPreviousSchedule compared
|
||||
// dateOnlyMs < updatedAt, which falsely dismissed ALL past doses after ANY edit.
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(), // Just edited!
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBe(5); // March 10-14
|
||||
|
||||
// No doses taken, no dismissals
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, med, new Set(), new Set());
|
||||
|
||||
// ALL 5 past days should be missed — updatedAt must NOT cause silent dismissals
|
||||
expect(missed).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("today's doses are not counted as past/missed", () => {
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-15T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const todayEvents = result.events.filter((e) => !e.isPast);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
|
||||
// Today's dose should NOT be past
|
||||
expect(todayEvents.length).toBeGreaterThan(0);
|
||||
// No past events (schedule starts today)
|
||||
expect(pastEvents.length).toBe(0);
|
||||
|
||||
// computeMissedPastDoseIds with empty pastDays => no missed
|
||||
const missed = computeMissedPastDoseIds([], med, new Set(), new Set());
|
||||
expect(missed).toEqual([]);
|
||||
});
|
||||
|
||||
it("dismissedUntil correctly clears missed doses after medication edit", () => {
|
||||
// Scenario: user edits medication, then clicks "Clear missed" which sets dismissedUntil
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
dismissedUntil: "2024-03-14", // Dismissed through yesterday
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBe(5); // March 10-14
|
||||
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, med, new Set(), new Set());
|
||||
|
||||
// All 5 past doses are on or before March 14 → all dismissed by dismissedUntil
|
||||
expect(missed).toEqual([]);
|
||||
});
|
||||
|
||||
it("multiple medications: edit one, other's tracking is unaffected", () => {
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aspirin",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(), // Just edited!
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Vitamin D",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T08:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(meds, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
|
||||
// Mark one dose of each medication as taken on March 12
|
||||
const aspirinMarch12 = pastEvents.find((e) => e.medName === "Aspirin" && new Date(e.when).getDate() === 12)!;
|
||||
const vitDMarch12 = pastEvents.find((e) => e.medName === "Vitamin D" && new Date(e.when).getDate() === 12)!;
|
||||
expect(aspirinMarch12).toBeDefined();
|
||||
expect(vitDMarch12).toBeDefined();
|
||||
|
||||
const takenDoses = new Set([aspirinMarch12.id, vitDMarch12.id]);
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, meds, takenDoses, new Set());
|
||||
|
||||
// Each med has 5 past days, 1 taken = 4 missed each = 8 total
|
||||
expect(missed).toHaveLength(8);
|
||||
// Neither March 12 dose ID should be in missed
|
||||
expect(missed).not.toContain(aspirinMarch12.id);
|
||||
expect(missed).not.toContain(vitDMarch12.id);
|
||||
});
|
||||
|
||||
it("taken doses with per-intake takenBy survive time edit", () => {
|
||||
// Using the modern intakes format: each person gets their own intake entry
|
||||
// Alice = intake index 0, Bob = intake index 1
|
||||
const medBefore: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "SharedMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [],
|
||||
intakes: [
|
||||
{ usage: 1, every: 1, start: "2024-03-10T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false },
|
||||
{ usage: 1, every: 1, start: "2024-03-10T09:00:00", takenBy: "Bob", intakeRemindersEnabled: false },
|
||||
],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medBefore, "en", true);
|
||||
const pastBefore = resultBefore.events.filter((e) => e.isPast);
|
||||
// 2 intakes × 5 days = 10 events
|
||||
expect(pastBefore.length).toBe(10);
|
||||
|
||||
// Alice's March 12 dose (intake index 0)
|
||||
const aliceMarch12 = pastBefore.find((e) => new Date(e.when).getDate() === 12 && e.id.startsWith("1-0-"))!;
|
||||
expect(aliceMarch12).toBeDefined();
|
||||
|
||||
// Bob's March 12 dose (intake index 1)
|
||||
const bobMarch12 = pastBefore.find((e) => new Date(e.when).getDate() === 12 && e.id.startsWith("1-1-"))!;
|
||||
expect(bobMarch12).toBeDefined();
|
||||
|
||||
// Alice takes her dose on March 12 — getDoseId adds person suffix
|
||||
// This matches how DashboardPage marks doses: getDoseId(dose.id, person)
|
||||
const aliceDoseId = `${aliceMarch12.id}-Alice`;
|
||||
const takenDoses = new Set([aliceDoseId]);
|
||||
|
||||
// Now edit: change both intakes to evening
|
||||
const medAfter: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "SharedMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [],
|
||||
intakes: [
|
||||
{ usage: 1, every: 1, start: "2024-03-10T21:00:00", takenBy: "Alice", intakeRemindersEnabled: false },
|
||||
{ usage: 1, every: 1, start: "2024-03-10T21:00:00", takenBy: "Bob", intakeRemindersEnabled: false },
|
||||
],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultAfter = buildSchedulePreview(medAfter, "en", true);
|
||||
const pastAfter = resultAfter.events.filter((e) => e.isPast);
|
||||
expect(pastAfter.length).toBe(10); // Still 10 events
|
||||
|
||||
const pastDays = groupEventsIntoPastDays(pastAfter);
|
||||
const missed = computeMissedPastDoseIds(pastDays, medAfter, takenDoses, new Set());
|
||||
|
||||
// 10 events, each expanded with person suffix = 10 dose IDs, minus 1 taken = 9 missed
|
||||
expect(missed).toHaveLength(9);
|
||||
// Alice's March 12 dose (with -Alice suffix) should NOT be in missed
|
||||
expect(missed).not.toContain(aliceDoseId);
|
||||
// Bob's March 12 dose (with -Bob suffix) should still be missed
|
||||
expect(missed).toContain(`${bobMarch12.id}-Bob`);
|
||||
// Dose IDs should be different between Alice and Bob (different intake index)
|
||||
expect(aliceMarch12.id).not.toBe(bobMarch12.id);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test Helpers
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Group flat schedule events into the pastDays structure expected by computeMissedPastDoseIds.
|
||||
* This mirrors the groupedSchedule logic in AppContext.tsx:
|
||||
* - event.takenBy is `string | null` (per-intake person)
|
||||
* - AppContext normalizes to `string[]`: `event.takenBy ? [event.takenBy] : []`
|
||||
*/
|
||||
function groupEventsIntoPastDays(
|
||||
events: Array<{
|
||||
id: string;
|
||||
medName: string;
|
||||
when: number;
|
||||
usage: number;
|
||||
isPast: boolean;
|
||||
takenBy?: string | string[] | null;
|
||||
}>
|
||||
): Array<{ meds: Array<{ medName: string; doses: Array<{ id: string; takenBy: string[] }> }> }> {
|
||||
const dayMap = new Map<string, Map<string, Array<{ id: string; takenBy: string[] }>>>();
|
||||
|
||||
for (const event of events) {
|
||||
if (!event.isPast) continue;
|
||||
const dateKey = new Date(event.when).toDateString();
|
||||
if (!dayMap.has(dateKey)) dayMap.set(dateKey, new Map());
|
||||
const medMap = dayMap.get(dateKey)!;
|
||||
if (!medMap.has(event.medName)) medMap.set(event.medName, []);
|
||||
// Mirror AppContext normalization: string|null → string[]
|
||||
const takenBy = Array.isArray(event.takenBy)
|
||||
? event.takenBy
|
||||
: typeof event.takenBy === "string"
|
||||
? [event.takenBy]
|
||||
: [];
|
||||
medMap.get(event.medName)!.push({ id: event.id, takenBy });
|
||||
}
|
||||
|
||||
return Array.from(dayMap.values()).map((medMap) => ({
|
||||
meds: Array.from(medMap.entries()).map(([medName, doses]) => ({ medName, doses })),
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user