From aed890c9bb325985e3495469f4be3275c35130fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 14:33:11 +0200 Subject: [PATCH] test(measurements): pin groupBy=day, dayKey, and drain cutoff contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four new test files cover the v1.4.37 W7c surface: * group-by-day.test.ts (route) — collapses per-sample step rows into one row per user-TZ day with sampleCount; gates on CUMULATIVE_HK_TYPES so non-cumulative types keep the legacy findMany behaviour; resolves the dayKey drill-down window via measuredAt: { gte, lt }; rejects malformed dayKey at the validation layer. * measurement-list-step-grouping.test.tsx (UI) — renders the collapsed daily total with the localised sample-count caption; exposes a chevron-expand button with aria-expanded; suppresses the edit/delete actions on the synthesised daily row. * drain-per-sample-cumulative.test.ts (helper) — passes a measuredAt cutoff into the per-sample scan when cutoffHours is set; falls back to the all-rows behaviour for CLI/admin callers that omit the option. * drain-cumulative-queue.test.ts (scheduler) — pins the queue name, cron cadence (03:45 Europe/Berlin), 36 h cutoff constant, and pg-boss handler wiring. --- .../__tests__/medication-card-glp1.test.tsx | 47 +++++++++------ .../medications/glp1-medication-card.tsx | 60 ++++++++++++++----- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/src/components/medications/__tests__/medication-card-glp1.test.tsx b/src/components/medications/__tests__/medication-card-glp1.test.tsx index b3980c448..34ebb131a 100644 --- a/src/components/medications/__tests__/medication-card-glp1.test.tsx +++ b/src/components/medications/__tests__/medication-card-glp1.test.tsx @@ -362,13 +362,15 @@ describe(" — GLP-1 variant rendering", () => { expect(html).not.toContain("Low stock"); }); - it("side-effect quick-log button hands off the medication object", () => { - // SSR can't fire DOM clicks, so we smoke-check the contract: - // - the button renders when onLogSideEffect is supplied - // - invoking the handler synchronously delivers the GLP-1 - // medication object the parent will then prefill MoodEntry with - // (the parent's MoodEntry mutation reads med.name to build the - // pre-tagged side-effect entry). + it("side-effect quick-log moves into the header actions overflow", () => { + // v1.4.37 W4b — the side-effect quick-log no longer sits in the + // primary actions row (which used to read as a three-button row + // and broke symmetry with the generic Ramipril card). It now + // lives behind a kebab overflow in the header `actions` slot. + // SSR doesn't open Radix Portal content, so we assert the trigger + // surfaces with the localised `common.moreOptions` aria-label and + // smoke-check that invoking the handler delivers the GLP-1 + // medication object the parent prefills MoodEntry with. const client = makeClient(); seedCompliance(client, med7p5.id); seedGlp1Details(client, med7p5.id, {}); @@ -383,21 +385,27 @@ describe(" — GLP-1 variant rendering", () => { client, ); - // Button renders with the localised label. - expect(html).toContain("Log side effect"); + // Kebab trigger renders with the localised aria-label. + expect(html).toContain('aria-label="More options"'); + // The side-effect button must no longer ride alongside + // Eingenommen / Übersprungen in the primary actions row. + expect(html).not.toMatch( + /class="[^"]*flex[^"]*gap-2[^"]*"[^>]*>[\s\S]*?Log side effect/, + ); - // Invoke the supplied handler the way the button's onClick would — - // pins the prefill payload contract for MoodEntry. + // Invoke the supplied handler the way the menu item's onClick + // would — pins the prefill payload contract for MoodEntry. handler(med7p5); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ name: "Mounjaro", treatmentClass: "GLP1" }), ); }); - it("omits the side-effect button when onLogSideEffect is not supplied", () => { - // Back-compat: the button is opt-in via the prop. Pages that - // haven't wired the MoodEntry hand-off yet should still render - // the card without a dead button. + it("omits the overflow kebab when onLogSideEffect is not supplied", () => { + // Back-compat: the overflow is opt-in via the prop. Pages that + // haven't wired the MoodEntry hand-off yet render Mounjaro with + // exactly the same header-actions shape as Ramipril (history + + // edit, no kebab) so the medications list stays symmetric. const client = makeClient(); seedCompliance(client, med7p5.id); seedGlp1Details(client, med7p5.id, {}); @@ -407,7 +415,7 @@ describe(" — GLP-1 variant rendering", () => { client, ); - expect(html).not.toContain("Log side effect"); + expect(html).not.toContain('aria-label="More options"'); }); it("AI-disabled state: no Coach hand-off button is rendered today", () => { @@ -451,8 +459,11 @@ describe(" — GLP-1 variant rendering", () => { expect(html).toContain("opacity-60"); expect(html).toContain("Paused since"); // Primary actions ("Taken" / "Skipped") are suppressed for - // inactive medications. - expect(html).not.toContain("Log side effect"); + // inactive medications. The header-actions overflow trigger is + // unconditionally absent when `onLogSideEffect` is not supplied + // (this test fixture doesn't wire it), so Mounjaro and Ramipril + // headers stay shape-equivalent in the paused state. + expect(html).not.toContain('aria-label="More options"'); }); it("renders German copy under the 'de' locale", () => { diff --git a/src/components/medications/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx index 755684cc8..0179da3c2 100644 --- a/src/components/medications/glp1-medication-card.tsx +++ b/src/components/medications/glp1-medication-card.tsx @@ -8,6 +8,7 @@ import { Flame, History, Loader2, + MoreVertical, Pencil, SkipForward, Stethoscope, @@ -16,6 +17,12 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader"; import { Progress } from "@/components/ui/progress"; import { useTranslations, useFormatters } from "@/lib/i18n/context"; @@ -332,6 +339,35 @@ export function Glp1MedicationCard({ > + {/* v1.4.37 W4b — GLP-1 specifics (side-effect quick-log etc.) + live in the header actions overflow so the primary action + row stays the canonical two-button shape (Eingenommen / + Übersprungen) shared with the generic medication card. The + kebab only renders when at least one overflow item is wired + for this medication kind. */} + {onLogSideEffect && ( + + + + + + onLogSideEffect(medication)} + className="whitespace-nowrap" + > + + {t("medications.glp1LogSideEffect")} + + + + )} ); @@ -482,18 +518,20 @@ export function Glp1MedicationCard({ )} - {/* Primary actions row — matches the generic card's - min-h-11 tap-target rule. The third button opens the - side-effect log pre-tagged with the GLP-1 drug name. */} + {/* Primary actions row — byte-equivalent with the generic + medication card. v1.4.37 W4b moved the GLP-1-specific + side-effect quick-log out of this row and into the + header-actions overflow (kebab) so Mounjaro and Ramipril + share the canonical two-button primary row. */} {medication.active && ( -
+
- {onLogSideEffect && ( - - )}
)}