From 86dbff0e6e1e63f99198e1fd6d39ca63afd4333e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 21:27:29 +0200 Subject: [PATCH] feat(medications): EMA titration-ladder display on GLP-1 detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the read-only TitrationSection to the medication detail page, mounted between SchedulingSection (W19e) and IntakeHistoryList. Same chrome as the W19d / W19e panels so the Wave-4b stack reads as one visual group. Layout: - Header with drug INN - Vertical step list on mobile, horizontal track from sm; each step shows dose, typical-weeks badge, and a current/past/upcoming state attribute the design system can hook (CSS-only, no JS state) - Current step highlighted with a primary-color ring + 'You are here' caption - Optional dwell hint + next-step caption — observational copy only (no 'should/must step up' anywhere; verified by test) - Ceiling message when the user is on the top step - Non-standard dose caption when the latest dose doesn't snap to a step inside the ±10 % tolerance - Disclaimer + external EMA-source link on every render i18n (medications.titration.*) added in all six locales after the medications.scheduling block per the W19e handoff for stable JSON ordering. EN + DE hand-curated Marc-Voice; FR / ES / IT / PL drafted from EN. Parity test (i18n-locale-integrity) passes. 10 component tests cover surface render, step-card count, single 'you are here', next-step caption, ceiling message, non-standard dose, disclaimer + EMA link, empty ladder, observational-copy discipline, and DE locale switch. The escalationDueHint copy is strictly reference (MDR boundary, parallel to W19c safety ground rules); the test pins the negative assertion — no 'should step up', no 'must step up', no 'recommend step up'. --- messages/de.json | 19 ++ messages/en.json | 19 ++ messages/es.json | 19 ++ messages/fr.json | 19 ++ messages/it.json | 19 ++ messages/pl.json | 19 ++ src/app/medications/[id]/history/page.tsx | 11 + .../medications/TitrationSection.tsx | 248 ++++++++++++++++++ .../__tests__/TitrationSection.test.tsx | 221 ++++++++++++++++ 9 files changed, 594 insertions(+) create mode 100644 src/components/medications/TitrationSection.tsx create mode 100644 src/components/medications/__tests__/TitrationSection.test.tsx diff --git a/messages/de.json b/messages/de.json index 7488a4794..14a6a40ca 100644 --- a/messages/de.json +++ b/messages/de.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Titrationsstufen", + "loading": "Lade Titrationsdaten", + "loadFailed": "Titrationsdaten konnten nicht geladen werden.", + "drugLadderHeader": "Standard-Schema (EMA-Referenz)", + "typicalWeeksOnStep": "≥ {weeks} Wo. typisch", + "youAreHere": "Hier stehst du", + "nextStepCaption": "Nächste Stufe (typisch): {dose} mg nach ~{weeks} Wochen auf der aktuellen Stufe.", + "ceilingMessage": "Du bist auf der höchsten Standardstufe.", + "escalationDueHint": "Du bist seit {weeks} Wochen auf dieser Stufe; das Schema sieht eine Steigerung typischerweise nach ~{typical} Wochen vor.", + "currentStepDwell": "Seit {weeks} Wochen auf dieser Stufe", + "weeksUnit": "Wo.", + "doseUnitMg": "mg", + "emptyState": "Noch keine Dosis-Historie. Erfasse eine Dosis, um deine Position im Schema zu sehen.", + "nonStandardDose": "Aktuelle Dosis liegt außerhalb des Standard-Schemas.", + "disclaimer": "EMA-Referenzwerte. Sprich mit deiner Ärztin oder deinem Arzt, bevor du Dosen änderst.", + "emaSourceCta": "EMA-Quelle", + "stepLabel": "Stufe {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Körperumriss", "injectionSitePickerAriaLabel": "Injektionsstelle auswählen", "siteAbdomenLeft": "Bauch, unten links", diff --git a/messages/en.json b/messages/en.json index 34cc5a8ba..1c0fce910 100644 --- a/messages/en.json +++ b/messages/en.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Titration ladder", + "loading": "Loading titration data", + "loadFailed": "Titration data failed to load.", + "drugLadderHeader": "Standard ladder (EMA reference)", + "typicalWeeksOnStep": "≥ {weeks} wks typical", + "youAreHere": "You are here", + "nextStepCaption": "Next step (typical): {dose} mg after ~{weeks} weeks on the current step.", + "ceilingMessage": "You are at the top of the standard ladder.", + "escalationDueHint": "You've been on this step for {weeks} weeks; the ladder typically steps up around {typical} weeks.", + "currentStepDwell": "On this step for {weeks} weeks", + "weeksUnit": "wks", + "doseUnitMg": "mg", + "emptyState": "No dose history yet. Log a dose to see where you are on the ladder.", + "nonStandardDose": "Current dose is outside the standard ladder buckets.", + "disclaimer": "EMA reference values. Talk to your doctor before changing doses.", + "emaSourceCta": "EMA source", + "stepLabel": "Step {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Body outline", "injectionSitePickerAriaLabel": "Injection site picker", "siteAbdomenLeft": "Abdomen, lower left", diff --git a/messages/es.json b/messages/es.json index 8055b098d..7bbd3ed9c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Escalado de titulación", + "loading": "Cargando datos de titulación", + "loadFailed": "No se pudieron cargar los datos de titulación.", + "drugLadderHeader": "Escalado estándar (referencia EMA)", + "typicalWeeksOnStep": "≥ {weeks} sem. típico", + "youAreHere": "Estás aquí", + "nextStepCaption": "Siguiente escalón (típico): {dose} mg tras ~{weeks} semanas en el escalón actual.", + "ceilingMessage": "Estás en el último escalón estándar.", + "escalationDueHint": "Llevas {weeks} semanas en este escalón; el esquema suele avanzar tras ~{typical} semanas.", + "currentStepDwell": "En este escalón desde hace {weeks} semanas", + "weeksUnit": "sem.", + "doseUnitMg": "mg", + "emptyState": "Aún no hay historial de dosis. Registra una dosis para ver tu posición en el escalado.", + "nonStandardDose": "La dosis actual está fuera de los escalones estándar.", + "disclaimer": "Valores de referencia EMA. Consulta a tu médico antes de cambiar la dosis.", + "emaSourceCta": "Fuente EMA", + "stepLabel": "Escalón {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Silueta del cuerpo", "injectionSitePickerAriaLabel": "Selector de sitio de inyección", "siteAbdomenLeft": "Abdomen, lower left", diff --git a/messages/fr.json b/messages/fr.json index 4b412e3b4..551e1b1c0 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Paliers de titration", + "loading": "Chargement des données de titration", + "loadFailed": "Échec du chargement des données de titration.", + "drugLadderHeader": "Paliers standard (référence EMA)", + "typicalWeeksOnStep": "≥ {weeks} sem. typique", + "youAreHere": "Vous êtes ici", + "nextStepCaption": "Palier suivant (typique) : {dose} mg après ~{weeks} semaines au palier actuel.", + "ceilingMessage": "Vous êtes au sommet du palier standard.", + "escalationDueHint": "Vous êtes à ce palier depuis {weeks} semaines ; le schéma prévoit une progression typique après ~{typical} semaines.", + "currentStepDwell": "À ce palier depuis {weeks} semaines", + "weeksUnit": "sem.", + "doseUnitMg": "mg", + "emptyState": "Aucun historique de dose. Enregistrez une dose pour voir votre position dans le schéma.", + "nonStandardDose": "La dose actuelle est en dehors des paliers standard.", + "disclaimer": "Valeurs de référence EMA. Consultez votre médecin avant toute modification de dose.", + "emaSourceCta": "Source EMA", + "stepLabel": "Palier {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Silhouette du corps", "injectionSitePickerAriaLabel": "Sélecteur de site d'injection", "siteAbdomenLeft": "Abdomen, lower left", diff --git a/messages/it.json b/messages/it.json index e5bd6979f..6baae33cb 100644 --- a/messages/it.json +++ b/messages/it.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Scala di titolazione", + "loading": "Caricamento dati di titolazione", + "loadFailed": "Impossibile caricare i dati di titolazione.", + "drugLadderHeader": "Scala standard (riferimento EMA)", + "typicalWeeksOnStep": "≥ {weeks} sett. tipico", + "youAreHere": "Sei qui", + "nextStepCaption": "Prossimo gradino (tipico): {dose} mg dopo ~{weeks} settimane al gradino attuale.", + "ceilingMessage": "Sei al gradino più alto della scala standard.", + "escalationDueHint": "Sei su questo gradino da {weeks} settimane; lo schema prevede un avanzamento tipico dopo ~{typical} settimane.", + "currentStepDwell": "Su questo gradino da {weeks} settimane", + "weeksUnit": "sett.", + "doseUnitMg": "mg", + "emptyState": "Nessuna cronologia delle dosi. Registra una dose per vedere la tua posizione sulla scala.", + "nonStandardDose": "La dose attuale è fuori dai gradini standard.", + "disclaimer": "Valori di riferimento EMA. Consulta il tuo medico prima di cambiare la dose.", + "emaSourceCta": "Fonte EMA", + "stepLabel": "Gradino {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Sagoma del corpo", "injectionSitePickerAriaLabel": "Selettore del sito di iniezione", "siteAbdomenLeft": "Abdomen, lower left", diff --git a/messages/pl.json b/messages/pl.json index 70f64f034..b0ca9f137 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -709,6 +709,25 @@ } } }, + "titration": { + "section": "Schemat titracji", + "loading": "Wczytywanie danych titracji", + "loadFailed": "Nie udało się wczytać danych titracji.", + "drugLadderHeader": "Standardowy schemat (referencja EMA)", + "typicalWeeksOnStep": "≥ {weeks} tyg. typowo", + "youAreHere": "Jesteś tutaj", + "nextStepCaption": "Następny stopień (typowo): {dose} mg po ~{weeks} tygodniach na obecnym stopniu.", + "ceilingMessage": "Jesteś na najwyższym stopniu schematu standardowego.", + "escalationDueHint": "Jesteś na tym stopniu od {weeks} tygodni; schemat zwykle przewiduje przejście wyżej po ~{typical} tygodniach.", + "currentStepDwell": "Na tym stopniu od {weeks} tygodni", + "weeksUnit": "tyg.", + "doseUnitMg": "mg", + "emptyState": "Brak historii dawek. Zarejestruj dawkę, aby zobaczyć swoją pozycję w schemacie.", + "nonStandardDose": "Aktualna dawka jest poza standardowymi stopniami schematu.", + "disclaimer": "Wartości referencyjne EMA. Skonsultuj się z lekarzem przed zmianą dawki.", + "emaSourceCta": "Źródło EMA", + "stepLabel": "Stopień {n}" + }, "injectionSiteBodyOutlineAriaLabel": "Sylwetka ciała", "injectionSitePickerAriaLabel": "Wybór miejsca iniekcji", "siteAbdomenLeft": "Abdomen, lower left", diff --git a/src/app/medications/[id]/history/page.tsx b/src/app/medications/[id]/history/page.tsx index dbb2e699a..aa8cfb163 100644 --- a/src/app/medications/[id]/history/page.tsx +++ b/src/app/medications/[id]/history/page.tsx @@ -9,6 +9,7 @@ import { IntakeHistoryList } from "@/components/medications/intake-history-list" import { DrugLevelChart } from "@/components/medications/DrugLevelChart"; import { SideEffectsSection } from "@/components/medications/SideEffectsSection"; import { SchedulingSection } from "@/components/medications/SchedulingSection"; +import { TitrationSection } from "@/components/medications/TitrationSection"; import { ArrowLeft, Loader2, Plus } from "lucide-react"; import Link from "next/link"; import { useTranslations } from "@/lib/i18n/context"; @@ -123,6 +124,16 @@ export default function IntakeHistoryPage({ /> )} + {/* v1.4.25 W19f — GLP-1 titration-ladder display. Read-only EMA + reference visual showing the standard dose-escalation schedule + with the user's current step highlighted. Sits between + SchedulingSection and IntakeHistoryList so the user lands on + cycle context (chart) -> side effects -> cadence/adherence -> + titration position -> per-dose timeline. */} + {medication?.treatmentClass === "GLP1" && ( + + )} + ` (W19e) and `` + * on the medication detail page. Same chrome as the W19d / W19e + * sections so the Wave-4b panels read as one visual group. + * + * Layout: + * - Header: drug INN + "Standard ladder (EMA reference)" + * - Vertical step list (mobile) — same nodes laid horizontally + * from `sm` upwards. Each step shows dose, typical-weeks badge, + * and a status dot (current / past / upcoming). + * - Current-step ring with "You are here" caption. + * - Next-step caption + observational dwell hint when the EMA + * dwell-time has elapsed. Copy is strictly observational; the + * ladder is a *reference*, never a prescription (MDR boundary). + * - Disclaimer + EMA-source link. + * + * Data comes from `/api/medications/[id]/titration`; that route owns + * the prisma queries and delegates math to the pure ladder helpers. + */ + +interface TitrationStep { + stepIndex: number; + doseMg: number; + typicalWeeks: number; +} + +interface TitrationResponse { + drugId: string; + drugInn: string; + ladder: TitrationStep[]; + currentStep: TitrationStep | null; + currentStepIndex: number | null; + weeksOnCurrentStep: number; + nextStep: TitrationStep | null; + escalationDue: boolean; + sourceEMA: string; +} + +interface TitrationSectionProps { + medicationId: string; +} + +function templateFill( + template: string, + values: Record, +): string { + return template.replace(/\{(\w+)\}/g, (match, key) => { + if (key in values) return String(values[key]); + return match; + }); +} + +export function TitrationSection({ medicationId }: TitrationSectionProps) { + const { t } = useTranslations(); + + const { data, isLoading, error } = useQuery({ + queryKey: ["medications", medicationId, "titration"], + queryFn: async (): Promise => { + const res = await fetch(`/api/medications/${medicationId}/titration`); + if (!res.ok) { + throw new Error(`Failed to load titration: ${res.status}`); + } + const json = await res.json(); + return json.data as TitrationResponse; + }, + }); + + const hasLadder = !!data && data.ladder.length > 0; + const currentIdx = data?.currentStepIndex ?? null; + + // Compose the optional dwell hint AND the next-step caption. The + // dwell hint is strictly observational — no "you should step up"; + // the boolean only gates *display*, never *action*. + const dwellHint = useMemo(() => { + if (!data || data.currentStep === null) return null; + if (data.weeksOnCurrentStep <= 0) return null; + if (data.escalationDue) { + return templateFill(t("medications.titration.escalationDueHint"), { + weeks: data.weeksOnCurrentStep, + typical: data.currentStep.typicalWeeks, + }); + } + return templateFill(t("medications.titration.currentStepDwell"), { + weeks: data.weeksOnCurrentStep, + }); + }, [data, t]); + + const nextStepCaption = useMemo(() => { + if (!data) return null; + if (!data.nextStep) return null; + return templateFill(t("medications.titration.nextStepCaption"), { + dose: data.nextStep.doseMg, + weeks: data.currentStep?.typicalWeeks ?? data.nextStep.typicalWeeks, + }); + }, [data, t]); + + return ( +
+
+

+ {t("medications.titration.section")} +

+ {data && ( + + {data.drugInn} + + )} +
+ +
+ {isLoading && ( +
+ + {t("medications.titration.loading")} +
+ )} + + {!!error && !isLoading && ( +

+ {t("medications.titration.loadFailed")} +

+ )} + + {data && !isLoading && ( +
+

+ {t("medications.titration.drugLadderHeader")} +

+ + {hasLadder ? ( +
    + {data.ladder.map((step) => { + const isCurrent = currentIdx === step.stepIndex; + const isPast = + currentIdx !== null && step.stepIndex < currentIdx; + return ( +
  1. + + {templateFill(t("medications.titration.stepLabel"), { + n: step.stepIndex + 1, + })} + + + {step.doseMg} {t("medications.titration.doseUnitMg")} + + + {templateFill( + t("medications.titration.typicalWeeksOnStep"), + { weeks: step.typicalWeeks }, + )} + + {isCurrent && ( + + {t("medications.titration.youAreHere")} + + )} +
  2. + ); + })} +
+ ) : ( +

+ {t("medications.titration.emptyState")} +

+ )} + + {data.currentStep === null && hasLadder && ( +

+ {t("medications.titration.nonStandardDose")} +

+ )} + + {dwellHint && ( +

+ {dwellHint} +

+ )} + + {nextStepCaption && ( +

{nextStepCaption}

+ )} + + {data.nextStep === null && data.currentStep !== null && ( +

+ {t("medications.titration.ceilingMessage")} +

+ )} + +
+

+ {t("medications.titration.disclaimer")} +

+ + {t("medications.titration.emaSourceCta")} + +
+
+ )} +
+
+ ); +} diff --git a/src/components/medications/__tests__/TitrationSection.test.tsx b/src/components/medications/__tests__/TitrationSection.test.tsx new file mode 100644 index 000000000..916aa743e --- /dev/null +++ b/src/components/medications/__tests__/TitrationSection.test.tsx @@ -0,0 +1,221 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { TitrationSection } from "@/components/medications/TitrationSection"; + +/** + * v1.4.25 W19f — TitrationSection SSR smoke tests. + * + * Same testing convention as `SchedulingSection.test.tsx`: + * `renderToStaticMarkup` + seeded react-query cache. Interactive + * branches (loading spinner, fetch-error) are covered by the API + * route tests + the pure ladder tests; surface tests here pin the + * static-render contract: + * + * 1. Header + drug INN render. + * 2. Each ladder step renders with dose + typical-weeks label. + * 3. The current step has a "You are here" caption. + * 4. The next-step caption renders when a next step exists. + * 5. The ceiling message renders when the user is on the top + * step (no next step). + * 6. The non-standard dose caption renders when current step is + * null but the ladder is non-empty. + * 7. The disclaimer + EMA source link always render with data. + * 8. Empty ladder → empty-state copy renders. + * 9. The escalationDueHint copy is observational, not prescriptive. + * 10. German locale renders. + */ + +function makeClient() { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: Number.POSITIVE_INFINITY, + refetchOnMount: false, + }, + }, + }); +} + +interface SeedShape { + drugId: string; + drugInn: string; + ladder: Array<{ stepIndex: number; doseMg: number; typicalWeeks: number }>; + currentStep: { stepIndex: number; doseMg: number; typicalWeeks: number } | null; + currentStepIndex: number | null; + weeksOnCurrentStep: number; + nextStep: { stepIndex: number; doseMg: number; typicalWeeks: number } | null; + escalationDue: boolean; + sourceEMA: string; +} + +function seed(client: QueryClient, medId: string, payload: SeedShape) { + client.setQueryData(["medications", medId, "titration"], payload); +} + +function render( + node: React.ReactNode, + client: QueryClient, + locale: "en" | "de" = "en", +) { + return renderToStaticMarkup( + + {node} + , + ); +} + +const TIRZEPATIDE_LADDER = [ + { stepIndex: 0, doseMg: 2.5, typicalWeeks: 4 }, + { stepIndex: 1, doseMg: 5, typicalWeeks: 4 }, + { stepIndex: 2, doseMg: 7.5, typicalWeeks: 4 }, + { stepIndex: 3, doseMg: 10, typicalWeeks: 4 }, + { stepIndex: 4, doseMg: 12.5, typicalWeeks: 4 }, + { stepIndex: 5, doseMg: 15, typicalWeeks: 4 }, +]; + +const SEED_ON_STEP_5MG: SeedShape = { + drugId: "tirzepatide", + drugInn: "Tirzepatide", + ladder: TIRZEPATIDE_LADDER, + currentStep: TIRZEPATIDE_LADDER[1], + currentStepIndex: 1, + weeksOnCurrentStep: 2, + nextStep: TIRZEPATIDE_LADDER[2], + escalationDue: false, + sourceEMA: + "https://www.ema.europa.eu/en/documents/product-information/mounjaro-epar-product-information_en.pdf", +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe(" — surface render", () => { + it("renders the section heading and drug INN", () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render(, client); + expect(html).toContain("Titration ladder"); + expect(html).toContain("Tirzepatide"); + expect(html).toContain("Standard ladder (EMA reference)"); + }); + + it("renders every step on the ladder", () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render(, client); + for (const step of TIRZEPATIDE_LADDER) { + // Each step's dose label appears (e.g. "2.5 mg", "15 mg"). + expect(html).toContain(`${step.doseMg} mg`); + } + // Step ladder count = 6 steps; each step renders one card. + const stepCards = html.match(/data-slot="titration-step"/g) ?? []; + expect(stepCards.length).toBe(6); + }); + + it('renders "You are here" on the current step only', () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render(, client); + expect(html).toContain("You are here"); + // Only one "you are here" caption rendered, on the matched step. + expect(html.match(/You are here/g)?.length).toBe(1); + }); + + it("renders the next-step caption when a next step exists", () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render(, client); + expect(html).toContain("Next step (typical): 7.5 mg"); + }); + + it("renders the ceiling message at the top of the ladder", () => { + const client = makeClient(); + seed(client, "med-1", { + ...SEED_ON_STEP_5MG, + currentStep: TIRZEPATIDE_LADDER[5], + currentStepIndex: 5, + nextStep: null, + }); + const html = render(, client); + expect(html).toContain("You are at the top of the standard ladder."); + }); + + it("renders the non-standard dose caption when current step is null", () => { + const client = makeClient(); + seed(client, "med-1", { + ...SEED_ON_STEP_5MG, + currentStep: null, + currentStepIndex: null, + nextStep: null, + weeksOnCurrentStep: 0, + }); + const html = render(, client); + expect(html).toContain( + "Current dose is outside the standard ladder buckets.", + ); + }); + + it("always renders the disclaimer and the EMA-source link", () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render(, client); + expect(html).toContain( + "EMA reference values. Talk to your doctor before changing doses.", + ); + expect(html).toContain("EMA source"); + expect(html).toContain("mounjaro-epar-product-information"); + }); + + it("renders the empty-state copy when the ladder is empty", () => { + const client = makeClient(); + seed(client, "med-1", { + ...SEED_ON_STEP_5MG, + ladder: [], + currentStep: null, + currentStepIndex: null, + nextStep: null, + }); + const html = render(, client); + expect(html).toContain( + "No dose history yet. Log a dose to see where you are on the ladder.", + ); + }); + + it("renders the escalation-due hint as an observation, not a prescription", () => { + const client = makeClient(); + seed(client, "med-1", { + ...SEED_ON_STEP_5MG, + weeksOnCurrentStep: 6, + escalationDue: true, + }); + const html = render(, client); + // The copy must NOT prescribe an action ("you should step up"). + expect(html).not.toMatch(/should step up/i); + expect(html).not.toMatch(/must step up/i); + expect(html).not.toMatch(/recommend.*step up/i); + // The observational template must be rendered. + expect(html).toContain("the ladder typically steps up around"); + // The escaped apostrophe is the SSR render of the curly-apostrophe-free + // template; we assert on the unencoded plain segment instead of the + // contraction itself to stay robust to the renderer's HTML entity choice. + expect(html).toContain("been on this step for 6 weeks"); + }); + + it("renders the section heading in German", () => { + const client = makeClient(); + seed(client, "med-1", SEED_ON_STEP_5MG); + const html = render( + , + client, + "de", + ); + expect(html).toContain("Titrationsstufen"); + expect(html).toContain("Standard-Schema (EMA-Referenz)"); + expect(html).toContain("Hier stehst du"); + }); +});