mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(measurements): pin groupBy=day, dayKey, and drain cutoff contracts
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.
This commit is contained in:
1 parent
2987b8c99d
commit
aed890c9bb
2 files changed
+73
-34
No files matched your search
@@ -362,13 +362,15 @@ describe("<Glp1MedicationCard> — 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("<Glp1MedicationCard> — 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("<Glp1MedicationCard> — 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("<Glp1MedicationCard> — 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", () => {
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 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 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11"
|
||||
aria-label={t("common.moreOptions")}
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onLogSideEffect(medication)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
<Stethoscope className="mr-2 h-4 w-4" />
|
||||
{t("medications.glp1LogSideEffect")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -482,18 +518,20 @@ export function Glp1MedicationCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="min-h-11 flex-1"
|
||||
onClick={() => recordIntake(false)}
|
||||
disabled={!!intakeLoading}
|
||||
>
|
||||
{intakeLoading === "take" ? (
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Check className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
@@ -506,22 +544,12 @@ export function Glp1MedicationCard({
|
||||
disabled={!!intakeLoading}
|
||||
>
|
||||
{intakeLoading === "skip" ? (
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<SkipForward className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
{t("medications.skipped")}
|
||||
</Button>
|
||||
{onLogSideEffect && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="min-h-11"
|
||||
onClick={() => onLogSideEffect(medication)}
|
||||
>
|
||||
<Stethoscope className="mr-1 h-4 w-4" />
|
||||
{t("medications.glp1LogSideEffect")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in new issue
Block a user