mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
test(medications): pin card next/last slot symmetry + equal height
Extend the symmetry suite to assert both cards render the byte-identical next/last slot wrapper (same reserved min-height, spacing, muted colour, next-then-last order, no em-dash placeholder, no brighter foreground token) and a constant-height bottom-pinned body across oral / injection / GLP-1 / PRN / one-shot, including the compliance-skeleton path. Update the dose-accent assertions onto text-dose-accent and the GLP-1 rotation test onto the folded-into-line nudge. Clock stays pinned.
This commit is contained in:
1 parent
b3e8f43ab8
commit
2a584f6210
2 files changed
+198
-10
No files matched your search
@@ -359,11 +359,15 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
expect(html).not.toContain("Dosis-Historie");
|
||||
});
|
||||
|
||||
it("renders the injection-site rotation marker (last + recommended)", () => {
|
||||
// The rotation hint surfaces only when `lastSite` and the
|
||||
// recommender disagree. We feed a left-cluster history → the
|
||||
// recommender lands on a right-side or thigh/arm site, so the
|
||||
// hint card renders.
|
||||
it("renders the injection-site rotation nudge folded into the upcoming line", () => {
|
||||
// The rotation nudge surfaces only when `lastSite` and the recommender
|
||||
// disagree. We feed a left-cluster history → the recommender lands on a
|
||||
// right-side or thigh/arm site. The nudge is no longer a separate
|
||||
// bordered block (which made the GLP-1 card taller than the generic card
|
||||
// in a grid row); it is folded into the upcoming-injection line as a
|
||||
// "· Recommended next: …" suffix, so the card keeps the same row
|
||||
// inventory as the generic card. The last site stays visible on the
|
||||
// last-injection line.
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med7p5.id);
|
||||
seedGlp1Details(client, med7p5.id, {
|
||||
@@ -386,9 +390,11 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
client,
|
||||
);
|
||||
|
||||
expect(html).toContain("Last site:");
|
||||
// The recommended-next nudge renders in the upcoming line.
|
||||
expect(html).toContain("Recommended next:");
|
||||
// Last site is the most-recent intake site.
|
||||
// The standalone bordered rotation block is gone (height-neutral fold).
|
||||
expect(html).not.toContain("bg-muted/40 rounded-md border");
|
||||
// Last site stays visible on the last-injection line.
|
||||
expect(html).toContain("Abdomen, lower left");
|
||||
});
|
||||
|
||||
|
||||
@@ -328,8 +328,12 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
label: null,
|
||||
daysOfWeek: null,
|
||||
};
|
||||
// A future next-due so the GLP-1 card's now-gated next-injection line
|
||||
// renders (it no longer prints a "—" placeholder when there is no next).
|
||||
const nextDueAt = new Date("2026-06-05T01:00:00Z").toISOString();
|
||||
const ramiprilFuture = {
|
||||
...ramipril,
|
||||
nextDueAt,
|
||||
schedules: [
|
||||
{
|
||||
id: "s-ramipril-future",
|
||||
@@ -340,6 +344,7 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
};
|
||||
const mounjaroFuture: Glp1Medication = {
|
||||
...mounjaro,
|
||||
nextDueAt,
|
||||
schedules: [
|
||||
{
|
||||
id: "s-mounjaro-future",
|
||||
@@ -373,8 +378,185 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
client,
|
||||
);
|
||||
|
||||
// Both surfaces carry the purple token on the upcoming dose.
|
||||
expect(ramiprilHtml).toContain("text-purple-400");
|
||||
expect(mounjaroHtml).toContain("text-purple-400");
|
||||
// Both surfaces carry the dose-accent token on the upcoming dose. The
|
||||
// token is theme-aware (`--dose-accent`): pixel-identical to the former
|
||||
// Tailwind `text-purple-400` in dark mode, AA-safe on the white Alucard
|
||||
// card in light mode. The hard-coded `text-purple-400` is gone.
|
||||
expect(ramiprilHtml).toContain("text-dose-accent");
|
||||
expect(mounjaroHtml).toContain("text-dose-accent");
|
||||
expect(ramiprilHtml).not.toContain("text-purple-400");
|
||||
expect(mounjaroHtml).not.toContain("text-purple-400");
|
||||
});
|
||||
|
||||
it("the next/last slot is structurally identical (same wrapper, order, colour, spacing) on both cards", () => {
|
||||
// The two cards now route the middle slot through the shared
|
||||
// <MedicationNextLastSlot>. The wrapper, line order (next then last),
|
||||
// colour token and spacing must be byte-identical across types — the
|
||||
// historical divergence (reversed order, `text-foreground/85` vs
|
||||
// `text-muted-foreground`, `space-y-1` vs `space-y-3.5`) is gone.
|
||||
const pastWindow = {
|
||||
windowStart: "01:00",
|
||||
windowEnd: "02:00",
|
||||
label: null,
|
||||
daysOfWeek: null,
|
||||
};
|
||||
// Render with a last-intake set + an upcoming (non-active) window so
|
||||
// BOTH the next and last lines are present, exercising the full slot.
|
||||
const yesterday = new Date("2026-06-01T09:00:00Z").toISOString();
|
||||
const ramiprilBoth = {
|
||||
...ramipril,
|
||||
lastTakenAt: yesterday,
|
||||
nextDueAt: new Date("2026-06-05T01:00:00Z").toISOString(),
|
||||
schedules: [{ id: "s-r-both", ...pastWindow, dose: "5 mg" }],
|
||||
};
|
||||
const mounjaroBoth: Glp1Medication = {
|
||||
...mounjaro,
|
||||
lastTakenAt: yesterday,
|
||||
nextDueAt: new Date("2026-06-05T01:00:00Z").toISOString(),
|
||||
schedules: [{ id: "s-m-both", ...pastWindow, dose: "7.5 mg" }],
|
||||
};
|
||||
|
||||
const client = makeClient();
|
||||
seedCompliance(client, ramiprilBoth.id);
|
||||
seedCompliance(client, mounjaroBoth.id);
|
||||
seedGlp1Details(client, mounjaroBoth.id);
|
||||
|
||||
const ramiprilHtml = render(
|
||||
<MedicationCard
|
||||
medication={ramiprilBoth}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
const mounjaroHtml = render(
|
||||
<Glp1MedicationCard
|
||||
medication={mounjaroBoth}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
for (const html of [ramiprilHtml, mounjaroHtml]) {
|
||||
// Identical reserved-height wrapper with identical spacing.
|
||||
expect(html).toContain('class="min-h-[2.75rem] space-y-3.5 text-sm"');
|
||||
// The slot's lines paint with the muted token on both cards — the
|
||||
// brighter `text-foreground/85` GLP-1 next-line is gone.
|
||||
expect(html).not.toContain("text-foreground/85");
|
||||
// No literal em-dash placeholder for an absent next-injection.
|
||||
expect(html).not.toContain(">—<");
|
||||
// Both lines present (next + last), so two muted <p> in the slot.
|
||||
const slotStart = html.indexOf("min-h-[2.75rem]");
|
||||
const slot = html.slice(slotStart, slotStart + 800);
|
||||
const lines = slot.match(/<p class="text-muted-foreground">/g) ?? [];
|
||||
expect(lines.length).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("both cards keep a constant-height card body and bottom-pin the action row across types", () => {
|
||||
// The body is a flex column (`flex h-full flex-col`) and the action row
|
||||
// carries `mt-auto`, so unequal content can't misalign the take/skip
|
||||
// buttons across a grid row. A compliance skeleton reserves the bars'
|
||||
// footprint while the query is null. Exercise the generic card across
|
||||
// schedule-less types (PRN / one-shot) plus a scheduled oral and GLP-1.
|
||||
const base = {
|
||||
active: true,
|
||||
notificationsEnabled: true,
|
||||
pausedAt: null,
|
||||
lastTakenAt: null,
|
||||
todayEventCount: 0,
|
||||
};
|
||||
const oral = {
|
||||
...base,
|
||||
id: "m-oral",
|
||||
name: "Metformin",
|
||||
dose: "500 mg",
|
||||
category: "DIABETES",
|
||||
schedules: [{ id: "so", ...allDayWindow, dose: "500 mg" }],
|
||||
};
|
||||
const prn = {
|
||||
...base,
|
||||
id: "m-prn",
|
||||
name: "Ibuprofen",
|
||||
dose: "400 mg",
|
||||
category: "OTHER",
|
||||
// PRN: no schedule, no next-due → shortest possible body.
|
||||
schedules: [],
|
||||
};
|
||||
const oneShot = {
|
||||
...base,
|
||||
id: "m-oneshot",
|
||||
name: "Vaccine",
|
||||
dose: "1 dose",
|
||||
category: "OTHER",
|
||||
schedules: [],
|
||||
};
|
||||
const glp1: Glp1Medication = {
|
||||
...mounjaro,
|
||||
id: "m-glp1",
|
||||
};
|
||||
|
||||
const client = makeClient();
|
||||
// Seed compliance for some but NOT others, so the skeleton path
|
||||
// (compliance null) is exercised alongside the loaded path.
|
||||
seedCompliance(client, oral.id);
|
||||
seedGlp1Details(client, glp1.id);
|
||||
// prn + oneShot intentionally left without compliance → skeleton.
|
||||
|
||||
const htmls = [
|
||||
render(
|
||||
<MedicationCard
|
||||
medication={oral}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
),
|
||||
render(
|
||||
<MedicationCard
|
||||
medication={prn}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
),
|
||||
render(
|
||||
<MedicationCard
|
||||
medication={oneShot}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
),
|
||||
render(
|
||||
<Glp1MedicationCard
|
||||
medication={glp1}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
),
|
||||
];
|
||||
|
||||
for (const html of htmls) {
|
||||
// Card body is a bottom-pinning flex column on every type.
|
||||
expect(html).toContain("flex h-full flex-col space-y-3.5");
|
||||
// Action row carries the bottom-pin.
|
||||
expect(html).toContain("mt-auto");
|
||||
// Reserved next/last slot is present on every type (constant height).
|
||||
expect(html).toContain("min-h-[2.75rem] space-y-3.5 text-sm");
|
||||
}
|
||||
|
||||
// The two cards left without compliance render the skeleton so their
|
||||
// body keeps the bars' footprint rather than collapsing ~5rem shorter.
|
||||
expect(htmls[1]).toContain("aria-hidden"); // prn skeleton
|
||||
expect(htmls[2]).toContain("aria-hidden"); // one-shot skeleton
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user