mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(onboarding): add an optional anamnesis step on baseline
A collapsible "About your health" card on the baseline step captures the genuinely-new baseline the app can act on — chronic conditions and allergies — without re-asking the height/sex/birthdate already gathered above. Both write through the existing encrypted self-context path (PUT /api/coach/about-me), so there is no new model and no new contract; the form preserves any existing free-text about-me on save and only sends a field the user actually typed. The card is collapsed by default with a clear optional framing, so the step stays clear and the progress bar keeps its five dots; every field is editable later in Settings. Copy is warm and medically grounded — context, not a clinical form — with keys across all six locales. Also re-truths the goals body copy now that the selection genuinely surfaces on the dashboard.
This commit is contained in:
1 parent
5f601426a9
commit
c8cd5aa226
9 files changed
+363
-10
No files matched your search
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "Was möchtest du erfassen?",
|
||||
"body": "Wähle die Bereiche, die dir wichtig sind – das hilft dir zu entscheiden, was du verfolgen und als Nächstes einrichten willst.",
|
||||
"body": "Wähle die Bereiche, die dir wichtig sind – wir zeigen sie zuerst auf deinem Dashboard an, und sie helfen dir zu entscheiden, was du als Nächstes einrichtest.",
|
||||
"helpHint": "Ein bis drei Schwerpunkte funktionieren erfahrungsgemäß am besten – mehr geht aber jederzeit.",
|
||||
"options": {
|
||||
"weightManagement": "Gewicht im Griff",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "Erkenne das Muster",
|
||||
"body": "Tageszusammenfassungen, Trend-Diagramme und ein Coach, der zeigt was sich verändert hat — alles auf deinem Gerät."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "Zu deiner Gesundheit (optional)",
|
||||
"subtitle": "Etwas Kontext, damit HealthLog deine Werte fairer einordnet.",
|
||||
"intro": "Teile so viel oder so wenig, wie du möchtest – jedes Feld ist optional und jederzeit in den Einstellungen änderbar.",
|
||||
"conditionsLabel": "Bestehende Erkrankungen",
|
||||
"conditionsPlaceholder": "z. B. Bluthochdruck, Typ-2-Diabetes",
|
||||
"conditionsHint": "Hilft HealthLog, deine Werte im Kontext einzuordnen, statt Erwartbares zu melden.",
|
||||
"allergiesLabel": "Allergien oder Unverträglichkeiten",
|
||||
"allergiesPlaceholder": "z. B. Penicillin, Laktose",
|
||||
"privacyNote": "Verschlüsselt gespeichert und nur für besseren Kontext genutzt. Medikamente fragen wir hier nicht ab – die kannst du jederzeit auf der Medikamenten-Seite ergänzen."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "What would you like to track?",
|
||||
"body": "Pick the areas you care about — it helps you decide what to track and set up next.",
|
||||
"body": "Pick the areas you care about — we'll surface them first on your dashboard, and they help you decide what to set up next.",
|
||||
"helpHint": "One to three priorities tend to work best — you can always add more later.",
|
||||
"options": {
|
||||
"weightManagement": "Weight management",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "See the pattern",
|
||||
"body": "Daily summaries, trend charts, and a coach that surfaces what changed — all on your own device."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "About your health (optional)",
|
||||
"subtitle": "A little context so HealthLog reads your numbers more fairly.",
|
||||
"intro": "Share as much or as little as you like — every field is optional and you can edit it anytime in Settings.",
|
||||
"conditionsLabel": "Ongoing conditions",
|
||||
"conditionsPlaceholder": "e.g. high blood pressure, type 2 diabetes",
|
||||
"conditionsHint": "Helps HealthLog frame your readings in context rather than flagging the expected.",
|
||||
"allergiesLabel": "Allergies or intolerances",
|
||||
"allergiesPlaceholder": "e.g. penicillin, lactose",
|
||||
"privacyNote": "Stored encrypted and used only to give you better context. We don't ask for your medications here — add those anytime from the Medications page."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "¿Qué te gustaría registrar?",
|
||||
"body": "Elige las áreas que te interesan: te ayuda a decidir qué seguir y qué configurar a continuación.",
|
||||
"body": "Elige las áreas que te interesan: las mostraremos primero en tu panel y te ayudarán a decidir qué configurar a continuación.",
|
||||
"helpHint": "Una a tres prioridades suelen funcionar mejor — siempre podrás añadir más más adelante.",
|
||||
"options": {
|
||||
"weightManagement": "Control del peso",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "Descubre los patrones",
|
||||
"body": "Resúmenes diarios, gráficas de tendencias y un coach que destaca lo que cambió — todo en tu propio dispositivo."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "Sobre tu salud (opcional)",
|
||||
"subtitle": "Un poco de contexto para que HealthLog interprete tus valores de forma más justa.",
|
||||
"intro": "Comparte lo que quieras: cada campo es opcional y puedes editarlo cuando quieras en Ajustes.",
|
||||
"conditionsLabel": "Afecciones actuales",
|
||||
"conditionsPlaceholder": "p. ej. hipertensión, diabetes tipo 2",
|
||||
"conditionsHint": "Ayuda a HealthLog a interpretar tus lecturas en contexto en lugar de marcar lo esperado.",
|
||||
"allergiesLabel": "Alergias o intolerancias",
|
||||
"allergiesPlaceholder": "p. ej. penicilina, lactosa",
|
||||
"privacyNote": "Se almacena cifrado y solo se usa para darte mejor contexto. Aquí no te pedimos tu medicación: puedes añadirla cuando quieras desde la página de Medicación."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "Que souhaitez-vous suivre ?",
|
||||
"body": "Sélectionnez les domaines qui comptent pour vous : cela vous aide à décider quoi suivre et configurer ensuite.",
|
||||
"body": "Sélectionnez les domaines qui comptent pour vous : nous les afficherons en premier sur votre tableau de bord, et ils vous aident à décider quoi configurer ensuite.",
|
||||
"helpHint": "Un à trois axes prioritaires fonctionnent généralement le mieux, mais vous pourrez en ajouter plus tard.",
|
||||
"options": {
|
||||
"weightManagement": "Gestion du poids",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "Repérez les tendances",
|
||||
"body": "Résumés quotidiens, graphiques de tendances et un coach qui met en avant ce qui a changé — le tout sur votre appareil."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "À propos de votre santé (facultatif)",
|
||||
"subtitle": "Un peu de contexte pour que HealthLog interprète vos valeurs plus justement.",
|
||||
"intro": "Partagez autant ou aussi peu que vous le souhaitez : chaque champ est facultatif et modifiable à tout moment dans les Réglages.",
|
||||
"conditionsLabel": "Affections en cours",
|
||||
"conditionsPlaceholder": "p. ex. hypertension, diabète de type 2",
|
||||
"conditionsHint": "Aide HealthLog à replacer vos mesures dans leur contexte plutôt que de signaler l'attendu.",
|
||||
"allergiesLabel": "Allergies ou intolérances",
|
||||
"allergiesPlaceholder": "p. ex. pénicilline, lactose",
|
||||
"privacyNote": "Stocké chiffré et utilisé uniquement pour un meilleur contexte. Nous ne demandons pas vos médicaments ici — ajoutez-les quand vous voulez depuis la page Médicaments."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "Cosa vuoi monitorare?",
|
||||
"body": "Scegli gli ambiti che ti interessano: ti aiuta a decidere cosa monitorare e configurare in seguito.",
|
||||
"body": "Scegli gli ambiti che ti interessano: li mostreremo per primi nella tua dashboard e ti aiutano a decidere cosa configurare in seguito.",
|
||||
"helpHint": "Da una a tre priorità funzionano di solito meglio — potrai aggiungerne altre in qualsiasi momento.",
|
||||
"options": {
|
||||
"weightManagement": "Gestione del peso",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "Scopri lo schema",
|
||||
"body": "Riepiloghi giornalieri, grafici di tendenza e un coach che evidenzia i cambiamenti — tutto sul tuo dispositivo."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "Sulla tua salute (facoltativo)",
|
||||
"subtitle": "Un po' di contesto perché HealthLog interpreti i tuoi valori in modo più equo.",
|
||||
"intro": "Condividi quanto vuoi: ogni campo è facoltativo e modificabile in qualsiasi momento nelle Impostazioni.",
|
||||
"conditionsLabel": "Condizioni in corso",
|
||||
"conditionsPlaceholder": "es. ipertensione, diabete di tipo 2",
|
||||
"conditionsHint": "Aiuta HealthLog a inquadrare le tue misurazioni nel contesto invece di segnalare l'atteso.",
|
||||
"allergiesLabel": "Allergie o intolleranze",
|
||||
"allergiesPlaceholder": "es. penicillina, lattosio",
|
||||
"privacyNote": "Memorizzato cifrato e usato solo per darti un contesto migliore. Qui non chiediamo i tuoi farmaci: aggiungili quando vuoi dalla pagina Farmaci."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
+12
-1
@@ -3648,7 +3648,7 @@
|
||||
},
|
||||
"goals": {
|
||||
"title": "Co chcesz śledzić?",
|
||||
"body": "Wybierz obszary, które są dla Ciebie ważne — to pomaga zdecydować, co śledzić i co skonfigurować w następnej kolejności.",
|
||||
"body": "Wybierz obszary, które są dla Ciebie ważne — pokażemy je najpierw na pulpicie i pomogą Ci zdecydować, co skonfigurować dalej.",
|
||||
"helpHint": "Od jednego do trzech priorytetów zwykle sprawdza się najlepiej — możesz dodać kolejne w dowolnej chwili.",
|
||||
"options": {
|
||||
"weightManagement": "Kontrola wagi",
|
||||
@@ -3733,6 +3733,17 @@
|
||||
"title": "Zobacz wzorce",
|
||||
"body": "Codzienne podsumowania, wykresy trendów i coach, który wskazuje co się zmieniło — wszystko na Twoim urządzeniu."
|
||||
}
|
||||
},
|
||||
"anamnesis": {
|
||||
"title": "O Twoim zdrowiu (opcjonalnie)",
|
||||
"subtitle": "Trochę kontekstu, aby HealthLog rzetelniej odczytywał Twoje wyniki.",
|
||||
"intro": "Podziel się tym, czym chcesz — każde pole jest opcjonalne i możesz je zmienić w dowolnej chwili w Ustawieniach.",
|
||||
"conditionsLabel": "Aktualne schorzenia",
|
||||
"conditionsPlaceholder": "np. nadciśnienie, cukrzyca typu 2",
|
||||
"conditionsHint": "Pomaga HealthLog ujmować Twoje pomiary w kontekście, zamiast oznaczać to, czego należy się spodziewać.",
|
||||
"allergiesLabel": "Alergie lub nietolerancje",
|
||||
"allergiesPlaceholder": "np. penicylina, laktoza",
|
||||
"privacyNote": "Przechowywane w postaci zaszyfrowanej i używane tylko po to, by dać Ci lepszy kontekst. Nie pytamy tu o leki — dodasz je w dowolnej chwili na stronie Leki."
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useState } from "react";
|
||||
import { ChevronDown, HeartHandshake } from "lucide-react";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* v1.17.1 — optional "About your health" card on the onboarding
|
||||
* Baseline step (3).
|
||||
*
|
||||
* Captures the genuinely-new baseline the app can act on — chronic
|
||||
* conditions and allergies / intolerances — that the height/sex/dob
|
||||
* fields above do NOT already cover. Both write through the existing
|
||||
* encrypted self-context path (`PUT /api/coach/about-me`), so there is
|
||||
* no new model and no new contract; the parent BaselineForm preserves
|
||||
* any existing free-text `aboutMe` on save.
|
||||
*
|
||||
* Collapsed by default with a prominent skip framing — the step stays
|
||||
* übersichtlich and every field is optional and editable later in
|
||||
* Settings → AI. Warm, medically-grounded copy: framed as "so
|
||||
* HealthLog reads your numbers in context," never as a clinical form.
|
||||
*
|
||||
* Controlled: the parent owns the values so it can persist them in the
|
||||
* same submit as the profile fields.
|
||||
*/
|
||||
export interface AnamnesisValue {
|
||||
conditions: string;
|
||||
allergies: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `PUT /api/coach/about-me` body for the anamnesis answers,
|
||||
* or `null` when the card was left untouched (so an untouched collapsed
|
||||
* card never round-trips). Preserves the existing free-text `aboutMe`
|
||||
* (the PUT schema requires it and an empty value clears it) and only
|
||||
* includes conditions / allergies when the user typed something — so an
|
||||
* unrelated stored value is never cleared. Field-by-field assembly: no
|
||||
* mass-spread of the value object into the body.
|
||||
*/
|
||||
export function buildAnamnesisAboutMeBody(
|
||||
baseAboutMe: string,
|
||||
value: AnamnesisValue,
|
||||
): Record<string, unknown> | null {
|
||||
const conditions = value.conditions.trim();
|
||||
const allergies = value.allergies.trim();
|
||||
if (!conditions && !allergies) return null;
|
||||
const body: Record<string, unknown> = { aboutMe: baseAboutMe };
|
||||
if (conditions) body.conditions = conditions;
|
||||
if (allergies) body.allergies = allergies;
|
||||
return body;
|
||||
}
|
||||
|
||||
export interface AnamnesisCardProps {
|
||||
value: AnamnesisValue;
|
||||
onChange: (next: AnamnesisValue) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/** Per-field cap mirrors ABOUT_ME_FIELD_MAX_CHARS on the server. */
|
||||
const FIELD_MAX = 500;
|
||||
|
||||
export function AnamnesisCard({ value, onChange, disabled }: AnamnesisCardProps) {
|
||||
const { t } = useTranslations();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const conditionsId = useId();
|
||||
const allergiesId = useId();
|
||||
const panelId = useId();
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={panelId}
|
||||
className="flex w-full items-center gap-3 rounded-xl p-5 text-left"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-full"
|
||||
>
|
||||
<HeartHandshake className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 space-y-0.5">
|
||||
<span className="text-foreground block text-sm font-medium">
|
||||
{t("onboarding.anamnesis.title")}
|
||||
</span>
|
||||
<span className="text-muted-foreground block text-xs leading-relaxed">
|
||||
{t("onboarding.anamnesis.subtitle")}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"text-muted-foreground size-4 shrink-0 transition-transform duration-150 ease-out motion-reduce:transition-none",
|
||||
expanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{expanded ? (
|
||||
<div id={panelId} className="space-y-5 px-5 pb-5">
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
{t("onboarding.anamnesis.intro")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={conditionsId}>
|
||||
{t("onboarding.anamnesis.conditionsLabel")}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={conditionsId}
|
||||
value={value.conditions}
|
||||
onChange={(e) =>
|
||||
onChange({ ...value, conditions: e.target.value })
|
||||
}
|
||||
disabled={disabled}
|
||||
maxLength={FIELD_MAX}
|
||||
rows={2}
|
||||
placeholder={t("onboarding.anamnesis.conditionsPlaceholder")}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("onboarding.anamnesis.conditionsHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={allergiesId}>
|
||||
{t("onboarding.anamnesis.allergiesLabel")}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={allergiesId}
|
||||
value={value.allergies}
|
||||
onChange={(e) =>
|
||||
onChange({ ...value, allergies: e.target.value })
|
||||
}
|
||||
disabled={disabled}
|
||||
maxLength={FIELD_MAX}
|
||||
rows={2}
|
||||
placeholder={t("onboarding.anamnesis.allergiesPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
{t("onboarding.anamnesis.privacyNote")}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
import { apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
import {
|
||||
AnamnesisCard,
|
||||
buildAnamnesisAboutMeBody,
|
||||
type AnamnesisValue,
|
||||
} from "@/components/onboarding/AnamnesisCard";
|
||||
|
||||
/**
|
||||
* v1.4.25 W14b-Content — onboarding step 3 (baseline).
|
||||
@@ -35,9 +40,12 @@ import { apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
*
|
||||
* Submit flow on "Save and continue":
|
||||
* 1. PUT profile (best-effort — empty fields are skipped).
|
||||
* 2. POST step:4 — flips `onboardingCompletedAt` server-side and
|
||||
* 2. PUT /api/coach/about-me — only when the optional anamnesis card
|
||||
* (v1.17.1) was filled; preserves any existing `aboutMe` and
|
||||
* writes conditions / allergies encrypted at rest.
|
||||
* 3. POST step:4 — flips `onboardingCompletedAt` server-side and
|
||||
* clears the proxy cookie.
|
||||
* 3. router.push("/onboarding/4") — the done screen.
|
||||
* 4. router.push("/onboarding/4") — the done screen.
|
||||
*
|
||||
* "Skip" advances without writing profile data; the wizard still
|
||||
* completes onboarding (the user can fill profile later from
|
||||
@@ -66,6 +74,37 @@ export function BaselineForm() {
|
||||
const [form, setForm] = useState<BaselineFormState>(EMPTY_FORM);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// v1.17.1 — optional anamnesis (conditions + allergies). Persisted
|
||||
// through the existing encrypted self-context path
|
||||
// (`PUT /api/coach/about-me`). We read the current self-context once
|
||||
// so a returning/resuming user's free-text `aboutMe` is preserved on
|
||||
// save (the PUT schema requires `aboutMe`, and an empty value clears
|
||||
// it). A fresh user has no self-context, so `baseAboutMe` stays "".
|
||||
const [anamnesis, setAnamnesis] = useState<AnamnesisValue>({
|
||||
conditions: "",
|
||||
allergies: "",
|
||||
});
|
||||
const [baseAboutMe, setBaseAboutMe] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void apiGet<{ aboutMe: string | null }>("/api/coach/about-me")
|
||||
.then((ctx) => {
|
||||
if (active && typeof ctx.aboutMe === "string") {
|
||||
setBaseAboutMe(ctx.aboutMe);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Non-fatal — onboarding must never block on the self-context
|
||||
// read. A fresh user has none; on error we keep the "" base,
|
||||
// which the PUT below only sends when the user actually typed
|
||||
// an anamnesis answer.
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
function patch<K extends keyof BaselineFormState>(
|
||||
key: K,
|
||||
value: BaselineFormState[K],
|
||||
@@ -92,6 +131,14 @@ export function BaselineForm() {
|
||||
if (Object.keys(profileBody).length > 0) {
|
||||
await apiPut("/api/auth/profile", profileBody);
|
||||
}
|
||||
|
||||
// Anamnesis — only write when the user actually filled a field
|
||||
// (the helper returns null for an untouched card, so a
|
||||
// collapsed card never round-trips).
|
||||
const aboutMeBody = buildAnamnesisAboutMeBody(baseAboutMe, anamnesis);
|
||||
if (aboutMeBody) {
|
||||
await apiPut("/api/coach/about-me", aboutMeBody);
|
||||
}
|
||||
}
|
||||
await apiPost("/api/onboarding/step", { step: 4 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
@@ -210,6 +257,12 @@ export function BaselineForm() {
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<AnamnesisCard
|
||||
value={anamnesis}
|
||||
onChange={setAnamnesis}
|
||||
disabled={saving}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href="/onboarding/2">{t("onboarding.shell.back")}</Link>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* v1.17.1 — optional onboarding anamnesis card.
|
||||
*
|
||||
* Vitest runs in the Node environment here (no jsdom), so the
|
||||
* expand/collapse interaction is left to e2e. This file locks the two
|
||||
* contracts that matter for correctness:
|
||||
* 1. the about-me PUT body builder (what actually persists), and
|
||||
* 2. the collapsed SSR shape + i18n wiring.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import {
|
||||
AnamnesisCard,
|
||||
buildAnamnesisAboutMeBody,
|
||||
} from "../AnamnesisCard";
|
||||
|
||||
describe("buildAnamnesisAboutMeBody", () => {
|
||||
const empty = { conditions: "", allergies: "" };
|
||||
|
||||
it("returns null when both fields are blank (untouched card)", () => {
|
||||
expect(buildAnamnesisAboutMeBody("", empty)).toBeNull();
|
||||
expect(
|
||||
buildAnamnesisAboutMeBody("", { conditions: " ", allergies: "\n" }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves the existing aboutMe so it is never cleared", () => {
|
||||
const body = buildAnamnesisAboutMeBody("I'm an athlete.", {
|
||||
conditions: "Hypertension",
|
||||
allergies: "",
|
||||
});
|
||||
expect(body).toMatchObject({
|
||||
aboutMe: "I'm an athlete.",
|
||||
conditions: "Hypertension",
|
||||
});
|
||||
// Allergies omitted (blank) so the stored value isn't cleared.
|
||||
expect(body).not.toHaveProperty("allergies");
|
||||
});
|
||||
|
||||
it("includes only the fields the user actually filled (no mass spread)", () => {
|
||||
const body = buildAnamnesisAboutMeBody("", {
|
||||
conditions: " Type 2 diabetes ",
|
||||
allergies: " Penicillin ",
|
||||
});
|
||||
expect(body).toEqual({
|
||||
aboutMe: "",
|
||||
conditions: "Type 2 diabetes",
|
||||
allergies: "Penicillin",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AnamnesisCard> (collapsed SSR)", () => {
|
||||
function render() {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<AnamnesisCard
|
||||
value={{ conditions: "", allergies: "" }}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it("renders the title + a collapsed (aria-expanded=false) toggle", () => {
|
||||
const html = render();
|
||||
expect(html).toContain("About your health (optional)");
|
||||
expect(html).toContain('aria-expanded="false"');
|
||||
});
|
||||
|
||||
it("keeps the questions hidden until expanded", () => {
|
||||
const html = render();
|
||||
// The conditions textarea label only renders inside the expanded
|
||||
// panel; collapsed SSR must not include it.
|
||||
expect(html).not.toContain("Ongoing conditions");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user