mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(medications): show the GLP-1 drug-level estimate by default
Render the estimated drug-level-in-blood curve for any recognised GLP-1 medication without requiring the Research Mode opt-in. The chart still self-gates on a recognised brand and on having intake data, and the pharmacokinetic-estimate disclaimer is now always attached so the curve reads as a modelled value, not a measured blood concentration. Drop the now-dead Research-Mode gated-placeholder branch and its copy. The researchModeEnabled flag is unchanged; it still gates the advanced settings surface and its acknowledgment dialog.
This commit is contained in:
1 parent
9e8b536ff6
commit
28af138691
8 files changed
+105
-212
No files matched your search
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "Noch keine Einnahmen erfasst.",
|
||||
"emptyStateCta": "Trage eine Dosis ein, damit die geschätzte Verlaufskurve Gestalt annimmt.",
|
||||
"estimateNote": "Edukative Schätzung aus EMA-publizierter Populations-Pharmakokinetik. Keine Messung.",
|
||||
"gatedTitle": "Forschungsmodus ist aus",
|
||||
"gatedBody": "Aktiviere den Forschungsmodus in den Einstellungen, um die geschätzte Wirkstoffkurve zu sehen. Die Kurve ist opt-in, weil sie eine Lernhilfe und keine klinische Messung ist.",
|
||||
"gatedStaleTitle": "Disclaimer muss neu bestätigt werden",
|
||||
"gatedStaleBody": "Der Disclaimer des Forschungsmodus wurde seit deiner letzten Bestätigung aktualisiert. Lies ihn in den Einstellungen erneut und bestätige, um die Kurve wieder einzublenden.",
|
||||
"gatedCta": "Forschungsmodus in den Einstellungen öffnen",
|
||||
"unknownDrug": "{name} ist nicht im EMA-zugelassenen GLP-1-Katalog enthalten. HealthLog kann dafür keine Populations-Pharmakokinetik-Kurve erzeugen."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Zurück zum Medikament",
|
||||
"subtitle": "Vollständiger Einnahmeverlauf",
|
||||
"plannedSuffix": "geplant",
|
||||
"drugLevelDisclosure": "Geschätzte Wirkstoffkurve anzeigen"
|
||||
"plannedSuffix": "geplant"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Injektionsstelle erfassen",
|
||||
|
||||
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "No intake events yet.",
|
||||
"emptyStateCta": "Log a dose to see the estimated level take shape.",
|
||||
"estimateNote": "Educational estimate from EMA-published population pharmacokinetics. Not a measurement.",
|
||||
"gatedTitle": "Research Mode is off",
|
||||
"gatedBody": "Turn on Research Mode in Settings to see the estimated drug-level curve. The curve is opt-in because it is a learning aid, not a clinical measurement.",
|
||||
"gatedStaleTitle": "Disclaimer needs re-acknowledgment",
|
||||
"gatedStaleBody": "The Research Mode disclaimer was updated since you last acknowledged it. Re-read and confirm in Settings to bring the chart back.",
|
||||
"gatedCta": "Open Research Mode in Settings",
|
||||
"unknownDrug": "{name} is not in the EMA-approved GLP-1 catalog, so HealthLog cannot render a population-PK curve for it."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Back to medication",
|
||||
"subtitle": "Full intake history",
|
||||
"plannedSuffix": "planned",
|
||||
"drugLevelDisclosure": "Show estimated drug-level curve"
|
||||
"plannedSuffix": "planned"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Log injection site",
|
||||
|
||||
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "Sin tomas registradas todavía.",
|
||||
"emptyStateCta": "Registra una dosis para ver la curva estimada cobrar forma.",
|
||||
"estimateNote": "Estimación educativa a partir de la farmacocinética poblacional publicada por la EMA. No es una medición.",
|
||||
"gatedTitle": "Modo Investigación desactivado",
|
||||
"gatedBody": "Activa el modo Investigación en los ajustes para ver la curva estimada del nivel de fármaco. La curva es de inclusión voluntaria porque es una ayuda de aprendizaje, no una medición clínica.",
|
||||
"gatedStaleTitle": "El disclaimer requiere reconfirmación",
|
||||
"gatedStaleBody": "El disclaimer del modo Investigación se actualizó desde tu última confirmación. Relelo en los ajustes y confirma de nuevo para volver a ver la curva.",
|
||||
"gatedCta": "Abrir el modo Investigación en los ajustes",
|
||||
"unknownDrug": "{name} no está en el catálogo de GLP-1 aprobado por la EMA, por lo que HealthLog no puede trazar una curva de farmacocinética poblacional."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Volver al medicamento",
|
||||
"subtitle": "Historial completo de tomas",
|
||||
"plannedSuffix": "planificado",
|
||||
"drugLevelDisclosure": "Mostrar curva estimada del principio activo"
|
||||
"plannedSuffix": "planificado"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Registrar sitio de inyección",
|
||||
|
||||
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "Aucune prise enregistrée.",
|
||||
"emptyStateCta": "Enregistrez une dose pour voir la courbe estimée prendre forme.",
|
||||
"estimateNote": "Estimation éducative à partir de la pharmacocinétique de population publiée par l'EMA. Pas une mesure.",
|
||||
"gatedTitle": "Mode Recherche désactivé",
|
||||
"gatedBody": "Activez le mode Recherche dans les paramètres pour voir la courbe estimée du niveau de médicament. La courbe est en opt-in parce que c'est un outil d'apprentissage, pas une mesure clinique.",
|
||||
"gatedStaleTitle": "Le disclaimer doit être reconfirmé",
|
||||
"gatedStaleBody": "Le disclaimer du mode Recherche a été mis à jour depuis votre dernière confirmation. Relisez-le dans les paramètres et confirmez à nouveau pour réafficher la courbe.",
|
||||
"gatedCta": "Ouvrir le mode Recherche dans les paramètres",
|
||||
"unknownDrug": "{name} n'est pas dans le catalogue GLP-1 approuvé par l'EMA, donc HealthLog ne peut pas tracer de courbe de pharmacocinétique de population."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Retour au médicament",
|
||||
"subtitle": "Historique complet des prises",
|
||||
"plannedSuffix": "prévu",
|
||||
"drugLevelDisclosure": "Afficher la courbe estimée du principe actif"
|
||||
"plannedSuffix": "prévu"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Enregistrer le site d'injection",
|
||||
|
||||
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "Nessuna assunzione registrata.",
|
||||
"emptyStateCta": "Registra una dose per vedere prendere forma la curva stimata.",
|
||||
"estimateNote": "Stima educativa dalla farmacocinetica di popolazione pubblicata dall'EMA. Non è una misura.",
|
||||
"gatedTitle": "Modalità Ricerca disattivata",
|
||||
"gatedBody": "Attiva la modalità Ricerca nelle impostazioni per vedere la curva stimata del livello del farmaco. La curva è opt-in perché è uno strumento di apprendimento, non una misura clinica.",
|
||||
"gatedStaleTitle": "Il disclaimer richiede una nuova conferma",
|
||||
"gatedStaleBody": "Il disclaimer della modalità Ricerca è stato aggiornato dall'ultima tua conferma. Rileggilo nelle impostazioni e conferma di nuovo per rivedere la curva.",
|
||||
"gatedCta": "Apri la modalità Ricerca nelle impostazioni",
|
||||
"unknownDrug": "{name} non è nel catalogo GLP-1 approvato dall'EMA, quindi HealthLog non può tracciare una curva di farmacocinetica di popolazione."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Torna al farmaco",
|
||||
"subtitle": "Cronologia completa delle assunzioni",
|
||||
"plannedSuffix": "pianificato",
|
||||
"drugLevelDisclosure": "Mostra la curva stimata del principio attivo"
|
||||
"plannedSuffix": "pianificato"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Registra il sito di iniezione",
|
||||
|
||||
+1
-7
@@ -1127,11 +1127,6 @@
|
||||
"emptyState": "Brak zarejestrowanych przyjęć.",
|
||||
"emptyStateCta": "Zarejestruj dawkę, aby zobaczyć kształtującą się krzywą szacunkową.",
|
||||
"estimateNote": "Edukacyjne oszacowanie z opublikowanej przez EMA farmakokinetyki populacyjnej. To nie jest pomiar.",
|
||||
"gatedTitle": "Tryb Badawczy wyłączony",
|
||||
"gatedBody": "Włącz tryb Badawczy w Ustawieniach, aby zobaczyć szacowaną krzywą poziomu leku. Krzywa jest opt-in, ponieważ to pomoc edukacyjna, a nie pomiar kliniczny.",
|
||||
"gatedStaleTitle": "Disclaimer wymaga ponownego potwierdzenia",
|
||||
"gatedStaleBody": "Disclaimer trybu Badawczego został zaktualizowany od czasu twojego ostatniego potwierdzenia. Przeczytaj go ponownie w Ustawieniach i potwierdź, aby krzywa znów się pojawiła.",
|
||||
"gatedCta": "Otwórz tryb Badawczy w Ustawieniach",
|
||||
"unknownDrug": "{name} nie znajduje się w katalogu GLP-1 zatwierdzonym przez EMA, więc HealthLog nie może wyrysować krzywej farmakokinetyki populacyjnej."
|
||||
}
|
||||
},
|
||||
@@ -1306,8 +1301,7 @@
|
||||
"history": {
|
||||
"back": "Powrót do leku",
|
||||
"subtitle": "Pełna historia przyjęć",
|
||||
"plannedSuffix": "zaplanowane",
|
||||
"drugLevelDisclosure": "Pokaż szacowaną krzywą stężenia leku"
|
||||
"plannedSuffix": "zaplanowane"
|
||||
}
|
||||
},
|
||||
"logInjectionSiteTitle": "Zarejestruj miejsce iniekcji",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.4.25 W19c-Frontend — Estimated drug-level chart for GLP-1
|
||||
* medications. Opt-in behind Research Mode (the acknowledgment dialog
|
||||
* in this directory gates entry); even when enabled the chart paints
|
||||
* "Estimated level (relative)" on the y-axis without tick labels —
|
||||
* research §2.3 + W19c-Backend round report carry the rationale.
|
||||
* Estimated drug-level chart for GLP-1 medications. Visible by default
|
||||
* for any recognised GLP-1 drug — the curve is the modelled
|
||||
* active-ingredient level the medication concerns. The y-axis paints
|
||||
* "Estimated level (relative)" without tick labels and the persistent
|
||||
* estimate disclaimer makes the pharmacokinetic-ESTIMATE framing
|
||||
* explicit: this is a modelled value, not a measured blood
|
||||
* concentration.
|
||||
*
|
||||
* Inputs (read-only):
|
||||
* - Drug id resolved from the medication name (catalog lookup via
|
||||
@@ -18,10 +20,6 @@
|
||||
* applicable dose by walking the dose-change timeline; intakes
|
||||
* that pre-date every dose-change row fall back to the
|
||||
* medication's headline dose string.
|
||||
* - Research-mode state from `GET /api/auth/me/research-mode`. The
|
||||
* chart gates on `enabled === true && acknowledgedVersion ===
|
||||
* currentDisclaimerVersion` — defence-in-depth alongside the
|
||||
* server-side 400 the POST returns for stale versions.
|
||||
*
|
||||
* Pure math: `computeOneCompartment(drug, doses, asOf)` from
|
||||
* `glp1-pk.ts`. The chart renders a 21-day window so three weekly
|
||||
@@ -38,8 +36,7 @@
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle, Loader2, Syringe } from "lucide-react";
|
||||
import { Loader2, Syringe } from "lucide-react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
@@ -50,7 +47,6 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
@@ -62,7 +58,6 @@ import {
|
||||
type Glp1DrugId,
|
||||
} from "@/lib/medications/glp1-knowledge";
|
||||
import { parseDoseMg } from "@/lib/medications/dose-string";
|
||||
import type { ResearchModeStatus } from "@/lib/medications/research-mode-types";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
|
||||
|
||||
@@ -128,23 +123,6 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
[medication.name],
|
||||
);
|
||||
|
||||
const { data: researchMode, isLoading: rmLoading } =
|
||||
useQuery<ResearchModeStatus | null>({
|
||||
queryKey: queryKeys.researchMode(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/me/research-mode");
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json();
|
||||
return json.data as ResearchModeStatus;
|
||||
},
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const versionsAligned =
|
||||
!!researchMode &&
|
||||
researchMode.acknowledgedVersion === researchMode.currentDisclaimerVersion;
|
||||
const gateOpen = !!researchMode?.enabled && versionsAligned;
|
||||
|
||||
const { data: details, isLoading: detailsLoading } =
|
||||
useQuery<Glp1DetailsResponse | null>({
|
||||
queryKey: queryKeys.medicationGlp1Details(medication.id),
|
||||
@@ -154,9 +132,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
const json = await res.json();
|
||||
return json.data as Glp1DetailsResponse;
|
||||
},
|
||||
// Only fetch when the user has opted in; saves a round-trip for
|
||||
// anyone who hasn't enabled Research Mode.
|
||||
enabled: gateOpen && !!drugId,
|
||||
enabled: !!drugId,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -170,7 +146,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
const json = await res.json();
|
||||
return json.data as { events: IntakeEvent[] };
|
||||
},
|
||||
enabled: gateOpen && !!drugId,
|
||||
enabled: !!drugId,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -194,7 +170,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
// ── Section header always renders so the parent's "Estimated drug
|
||||
// level" anchor stays stable; the body switches between gated /
|
||||
// loading / empty / chart states.
|
||||
const isLoading = rmLoading || (gateOpen && (detailsLoading || intakeLoading));
|
||||
const isLoading = !!drugId && (detailsLoading || intakeLoading);
|
||||
const hasDoses = doses.length > 0;
|
||||
const animationsEnabled = !prefersReducedMotion();
|
||||
// Single-window contract — three weekly cycles with the sample step
|
||||
@@ -214,19 +190,12 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
) : null;
|
||||
|
||||
// Decision tree:
|
||||
// 1. drug not in catalog → gated-unknown-drug placeholder
|
||||
// 2. !researchMode.enabled OR → gated placeholder + Settings CTA
|
||||
// versions !== aligned (the defence-in-depth rule)
|
||||
// 3. loading → skeleton
|
||||
// 4. no intake events → empty-state with log CTA
|
||||
// 5. otherwise → AreaChart
|
||||
// 1. drug not in catalog → unknown-drug placeholder
|
||||
// 2. loading → skeleton
|
||||
// 3. no intake events → empty-state with log CTA
|
||||
// 4. otherwise → AreaChart
|
||||
const body = !drugId ? (
|
||||
<GatedUnknownDrug medicationName={medication.name} />
|
||||
) : !gateOpen ? (
|
||||
<GatedPlaceholder
|
||||
versionsAligned={versionsAligned}
|
||||
knownState={researchMode}
|
||||
/>
|
||||
) : isLoading ? (
|
||||
// v1.4.43 W11-L6 — match the loaded chart height (240 px) so the
|
||||
// dashboard tile doesn't reflow when the levels render.
|
||||
@@ -257,7 +226,10 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
dataSlot="drug-level-chart"
|
||||
>
|
||||
{body}
|
||||
{gateOpen && hasDoses && (
|
||||
{/* The pharmacokinetic-ESTIMATE disclaimer stays attached whenever
|
||||
a recognised GLP-1 drug renders the chart — the curve is a
|
||||
modelled value, never a measured blood concentration. */}
|
||||
{!!drugId && (
|
||||
<p
|
||||
className="text-muted-foreground mt-2 text-xs italic"
|
||||
data-slot="drug-level-chart-disclaimer"
|
||||
@@ -306,52 +278,6 @@ function GatedUnknownDrug({ medicationName }: { medicationName: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function GatedPlaceholder({
|
||||
versionsAligned,
|
||||
knownState,
|
||||
}: {
|
||||
versionsAligned: boolean;
|
||||
knownState: ResearchModeStatus | null | undefined;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
// Three distinguishable states:
|
||||
// - Research Mode is off entirely.
|
||||
// - Research Mode is on but the acknowledged version is stale.
|
||||
// - We don't know yet (server returned null) — still render the
|
||||
// opt-in CTA so the user has a path forward.
|
||||
const stale = !!knownState?.enabled && !versionsAligned;
|
||||
return (
|
||||
<div
|
||||
className="bg-muted/40 border-border space-y-3 rounded-md border p-4 text-sm"
|
||||
data-slot="drug-level-chart-gated"
|
||||
data-stale={stale ? "true" : "false"}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle
|
||||
className="text-warning mt-0.5 h-4 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<p className="text-foreground font-medium">
|
||||
{stale
|
||||
? t("medications.researchMode.chart.gatedStaleTitle")
|
||||
: t("medications.researchMode.chart.gatedTitle")}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{stale
|
||||
? t("medications.researchMode.chart.gatedStaleBody")
|
||||
: t("medications.researchMode.chart.gatedBody")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings/advanced">
|
||||
{t("medications.researchMode.chart.gatedCta")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/**
|
||||
* v1.4.25 W19c-Frontend — Drug-level chart tests.
|
||||
* Drug-level chart tests.
|
||||
*
|
||||
* Pins the gating decision tree (research §2.3 + §12.4 + W19c-Backend
|
||||
* phase report). The chart MUST hide until:
|
||||
* - the medication's brand resolves to a Glp1DrugId, AND
|
||||
* - `enabled === true` AND
|
||||
* - `acknowledgedVersion === currentDisclaimerVersion`.
|
||||
* The estimated drug-level curve is visible by default for any
|
||||
* recognised GLP-1 brand — it does NOT require Research Mode. The
|
||||
* decision tree is now:
|
||||
* - the medication's brand resolves to a Glp1DrugId → render, ELSE
|
||||
* the unknown-drug placeholder.
|
||||
* - no intake events → empty state.
|
||||
* - otherwise → the AreaChart, always with the pharmacokinetic
|
||||
* estimate disclaimer attached.
|
||||
*
|
||||
* Each gate failure renders a distinct placeholder so the SSR markup
|
||||
* carries an unambiguous data-slot for assertion. Recharts in
|
||||
* `renderToStaticMarkup` only paints the static frame (the
|
||||
* Recharts in `renderToStaticMarkup` only paints the static frame (the
|
||||
* `<ResponsiveContainer>` mounts but doesn't measure), so we assert on
|
||||
* `data-slot="drug-level-chart-area"` presence + the gradient
|
||||
* definition + the y-axis having no `<text>` tick labels.
|
||||
@@ -25,8 +26,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
// Capture queryKey routing so different tests can stub different
|
||||
// query results. The component reads three queries:
|
||||
// - ["research-mode"]
|
||||
// query results. The component reads two queries:
|
||||
// - ["medications", id, "glp1-details"]
|
||||
// - ["medications", id, "intake", "drug-level-chart"]
|
||||
const queryResults: Record<string, unknown> = {};
|
||||
@@ -109,15 +109,8 @@ beforeEach(() => {
|
||||
pkCalls.length = 0;
|
||||
});
|
||||
|
||||
describe("<DrugLevelChart> — gating decision tree", () => {
|
||||
describe("<DrugLevelChart> — default-on decision tree", () => {
|
||||
it("renders the unknown-drug placeholder for a non-GLP-1 brand", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-05-14T00:00:00Z",
|
||||
acknowledgedVersion: "2026-05-14.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
});
|
||||
|
||||
const html = render(<DrugLevelChart medication={ramipril} />);
|
||||
|
||||
expect(html).toContain('data-slot="drug-level-chart-unknown-drug"');
|
||||
@@ -128,48 +121,46 @@ describe("<DrugLevelChart> — gating decision tree", () => {
|
||||
expect(pkCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders the gated placeholder when Research Mode is OFF", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: false,
|
||||
acknowledgedAt: null,
|
||||
acknowledgedVersion: null,
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
it("renders the chart for a GLP-1 med WITHOUT any Research Mode opt-in", () => {
|
||||
// No research-mode query is stubbed at all — the chart must still
|
||||
// render the curve for a recognised GLP-1 brand.
|
||||
setQueryResult("medications/med-1/glp1-details", {
|
||||
doseChanges: [
|
||||
{
|
||||
id: "dc-1",
|
||||
effectiveFrom: "2026-04-01T00:00:00Z",
|
||||
doseValue: 7.5,
|
||||
doseUnit: "mg",
|
||||
},
|
||||
],
|
||||
});
|
||||
setQueryResult("medications/med-1/intake/drug-level-chart", {
|
||||
events: [
|
||||
{
|
||||
id: "ev-1",
|
||||
takenAt: "2026-05-13T08:00:00Z",
|
||||
skipped: false,
|
||||
scheduledFor: "2026-05-13T08:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const html = render(<DrugLevelChart medication={mounjaro} />);
|
||||
const html = render(
|
||||
<DrugLevelChart
|
||||
medication={mounjaro}
|
||||
asOf={new Date("2026-05-14T12:00:00Z")}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('data-slot="drug-level-chart-gated"');
|
||||
expect(html).toContain('data-stale="false"');
|
||||
expect(html).toContain("Research Mode is off");
|
||||
expect(html).toContain("Open Research Mode");
|
||||
expect(html).not.toContain('data-slot="drug-level-chart-area"');
|
||||
expect(pkCalls).toHaveLength(0);
|
||||
expect(html).toContain('data-slot="drug-level-chart-area"');
|
||||
// The gated placeholder must never appear now.
|
||||
expect(html).not.toContain('data-slot="drug-level-chart-gated"');
|
||||
// The estimate disclaimer is always attached.
|
||||
expect(html).toContain('data-slot="drug-level-chart-disclaimer"');
|
||||
expect(pkCalls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders the stale-disclaimer placeholder when versions differ", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-04-01T00:00:00Z",
|
||||
acknowledgedVersion: "2026-04-01.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
});
|
||||
|
||||
const html = render(<DrugLevelChart medication={mounjaro} />);
|
||||
|
||||
expect(html).toContain('data-slot="drug-level-chart-gated"');
|
||||
expect(html).toContain('data-stale="true"');
|
||||
expect(html).toContain("re-acknowledgment");
|
||||
expect(html).not.toContain('data-slot="drug-level-chart-area"');
|
||||
expect(pkCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders the empty state when gate is open but no intake events exist", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-05-14T00:00:00Z",
|
||||
acknowledgedVersion: "2026-05-14.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
});
|
||||
it("renders the empty state when no intake events exist", () => {
|
||||
setQueryResult("medications/med-1/glp1-details", { doseChanges: [] });
|
||||
setQueryResult("medications/med-1/intake/drug-level-chart", {
|
||||
events: [],
|
||||
@@ -180,16 +171,41 @@ describe("<DrugLevelChart> — gating decision tree", () => {
|
||||
expect(html).toContain('data-slot="drug-level-chart-empty"');
|
||||
expect(html).toContain("No intake events yet");
|
||||
expect(html).not.toContain('data-slot="drug-level-chart-area"');
|
||||
// The estimate disclaimer stays attached even without data.
|
||||
expect(html).toContain('data-slot="drug-level-chart-disclaimer"');
|
||||
expect(pkCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders the chart when gate is open AND intake events exist", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-05-14T00:00:00Z",
|
||||
acknowledgedVersion: "2026-05-14.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
it("falls back to the headline dose when no dose-change rows exist", () => {
|
||||
// No doseChanges rows — every intake resolves to the headline
|
||||
// "7.5 mg" dose so the curve still renders for a typical med.
|
||||
setQueryResult("medications/med-1/glp1-details", { doseChanges: [] });
|
||||
setQueryResult("medications/med-1/intake/drug-level-chart", {
|
||||
events: [
|
||||
{
|
||||
id: "ev-1",
|
||||
takenAt: "2026-05-13T08:00:00Z",
|
||||
skipped: false,
|
||||
scheduledFor: "2026-05-13T08:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const html = render(
|
||||
<DrugLevelChart
|
||||
medication={mounjaro}
|
||||
asOf={new Date("2026-05-14T12:00:00Z")}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('data-slot="drug-level-chart-area"');
|
||||
expect(pkCalls).toHaveLength(1);
|
||||
const [call] = pkCalls;
|
||||
expect(call.doses).toHaveLength(1);
|
||||
expect(call.doses.every((d) => d.doseMg === 7.5)).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the chart when intake events exist", () => {
|
||||
setQueryResult("medications/med-1/glp1-details", {
|
||||
doseChanges: [
|
||||
{
|
||||
@@ -245,18 +261,11 @@ describe("<DrugLevelChart> — gating decision tree", () => {
|
||||
});
|
||||
|
||||
it("wraps the chart inside the canonical MedicationDetailSection chrome (UI-H1)", () => {
|
||||
// v1.4.28 — the dashboard tile retired and the standalone
|
||||
// mount on `/medications/[id]/history` lifts onto the shared
|
||||
// `<MedicationDetailSection>` chrome alongside Titration /
|
||||
// The standalone mount on `/medications/[id]/history` lifts onto the
|
||||
// shared `<MedicationDetailSection>` chrome alongside Titration /
|
||||
// Scheduling / SideEffects. The aria-labelledby thread + the
|
||||
// `border-border/60 rounded-md border` shell are the
|
||||
// load-bearing seams.
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-05-14T00:00:00Z",
|
||||
acknowledgedVersion: "2026-05-14.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
});
|
||||
const html = render(<DrugLevelChart medication={mounjaro} />);
|
||||
expect(html).toContain('data-slot="drug-level-chart"');
|
||||
expect(html).toContain('aria-labelledby="drug-level-chart-title"');
|
||||
@@ -268,12 +277,6 @@ describe("<DrugLevelChart> — gating decision tree", () => {
|
||||
});
|
||||
|
||||
it("hides the y-axis tick labels (research §2.3, unit-less)", () => {
|
||||
setQueryResult("research-mode", {
|
||||
enabled: true,
|
||||
acknowledgedAt: "2026-05-14T00:00:00Z",
|
||||
acknowledgedVersion: "2026-05-14.1",
|
||||
currentDisclaimerVersion: "2026-05-14.1",
|
||||
});
|
||||
setQueryResult("medications/med-1/glp1-details", { doseChanges: [] });
|
||||
setQueryResult("medications/med-1/intake/drug-level-chart", {
|
||||
events: [
|
||||
|
||||
Reference in new issue
Block a user