Files
HealthLog/src/components/medications/__tests__/ResearchModeAcknowledgmentDialog.test.tsx
T
Marc-André Bombeck 4f09c3f862 chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
2026-05-15 22:59:10 +02:00

289 lines
9.2 KiB
TypeScript

/**
* v1.4.25 W19c-Frontend — Research-mode acknowledgment dialog tests.
*
* The dialog is rendered controlled by Settings (commit 3) and gates
* the GLP-1 drug-level chart (commit 2). The contract is:
*
* 1. Multi-section MDR copy renders (what-is, what-isn't, why,
* MDR boundary, citations).
* 2. Acknowledge CTA fires a `POST /api/auth/me/research-mode` with
* the exact server-supplied version string.
* 3. Cancel CTA fires `onOpenChange(false)` without an API call.
* 4. The stale-version error (server 400 with
* `research-mode.version.stale`) surfaces a localised error toast
* and keeps the dialog open.
*
* Setup mirrors the existing `coach-settings-sheet.test.tsx` pattern:
* SSR via `renderToStaticMarkup` so the Radix Dialog primitives are
* mocked to plain wrappers (Portals don't materialise under SSR), and
* the TanStack Query mutation is captured so we can drive the
* onSuccess / onError branches by hand.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { toast } from "sonner";
import { I18nProvider } from "@/lib/i18n/context";
const mutationCallbacks: {
mutationFn: ((version: string) => Promise<unknown>) | null;
onSuccess: ((data: unknown) => void) | null;
onError: ((err: unknown) => void) | null;
} = { mutationFn: null, onSuccess: null, onError: null };
const lastMutateCall: { args: unknown[] } = { args: [] };
vi.mock("@tanstack/react-query", () => ({
useMutation: (opts: {
mutationFn: (version: string) => Promise<unknown>;
onSuccess?: (data: unknown) => void;
onError?: (err: unknown) => void;
}) => {
mutationCallbacks.mutationFn = opts.mutationFn;
mutationCallbacks.onSuccess = opts.onSuccess ?? null;
mutationCallbacks.onError = opts.onError ?? null;
return {
mutate: (version: string) => {
lastMutateCall.args = [version];
},
isPending: false,
};
},
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
// v1.4.27 R4 RC2 — the dialog now mounts <ResponsiveSheet>. Its
// internals (Sheet on `<md`, Dialog on `md+`) both portal at runtime;
// we collapse the primitive to a plain wrapper so the body of the
// dialog stays reachable in the static markup.
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: ({
open,
title,
description,
footer,
children,
}: {
open: boolean;
title: React.ReactNode;
description?: React.ReactNode;
footer?: React.ReactNode;
children: React.ReactNode;
}) =>
open ? (
<div data-slot="mock-responsive-sheet">
<div data-slot="mock-responsive-sheet-header">
<h2 data-slot="mock-responsive-sheet-title">{title}</h2>
{description ? (
<p data-slot="mock-responsive-sheet-description">{description}</p>
) : null}
</div>
<div data-slot="mock-responsive-sheet-body">{children}</div>
{footer ? (
<div data-slot="mock-responsive-sheet-footer">{footer}</div>
) : null}
</div>
) : null,
}));
import { ResearchModeAcknowledgmentDialog } from "../ResearchModeAcknowledgmentDialog";
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
);
}
beforeEach(() => {
mutationCallbacks.mutationFn = null;
mutationCallbacks.onSuccess = null;
mutationCallbacks.onError = null;
lastMutateCall.args = [];
vi.mocked(toast.success).mockClear();
vi.mocked(toast.error).mockClear();
});
describe("<ResearchModeAcknowledgmentDialog>", () => {
it("renders nothing while `open === false`", () => {
const html = render(
<ResearchModeAcknowledgmentDialog
open={false}
onOpenChange={() => {}}
currentDisclaimerVersion="2026-05-14.1"
/>,
);
expect(html).toBe("");
});
it("renders the title + the five MDR-copy sections while open", () => {
const html = render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={() => {}}
currentDisclaimerVersion="2026-05-14.1"
/>,
);
// Title + primary CTAs.
expect(html).toContain("Enable Research Mode");
expect(html).toContain("I understand — enable");
expect(html).toContain("Cancel");
// Each of the five sections renders its own headline.
expect(html).toContain("What this is");
expect(html).toContain("What this isn&#x27;t");
expect(html).toContain("Why it&#x27;s an estimate");
expect(html).toContain("Regulatory boundary");
expect(html).toContain("Sources");
// Cited regulatory references must appear verbatim in the
// user-facing copy (Marc-Voice + research §11 + §12.4).
expect(html).toContain("2017/745");
expect(html).toContain("MDCG 2021-24");
// Peer-reviewed citation.
expect(html).toContain("10.1002/psp4.13099");
// Version line surfaces the live server value.
expect(html).toContain("Disclaimer version: 2026-05-14.1");
});
it("acknowledge CTA invokes the mutation with the supplied version", async () => {
render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={() => {}}
currentDisclaimerVersion="2026-05-14.1"
/>,
);
// SSR markup can't fire DOM events; invoke the mutation directly
// (the production component calls `mutation.mutate(version)` from
// the click handler with `currentDisclaimerVersion`).
expect(mutationCallbacks.mutationFn).not.toBeNull();
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ data: { enabled: true } }),
});
vi.stubGlobal("fetch", fetchMock);
await mutationCallbacks.mutationFn?.("2026-05-14.1");
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe("/api/auth/me/research-mode");
expect(init.method).toBe("POST");
expect(JSON.parse(init.body as string)).toEqual({
acknowledged: true,
version: "2026-05-14.1",
});
vi.unstubAllGlobals();
});
it("onSuccess fires the localised success toast", () => {
const onAcknowledged = vi.fn();
const onOpenChange = vi.fn();
render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={onOpenChange}
currentDisclaimerVersion="2026-05-14.1"
onAcknowledged={onAcknowledged}
/>,
);
mutationCallbacks.onSuccess?.({ enabled: true });
expect(vi.mocked(toast.success)).toHaveBeenCalledTimes(1);
expect(vi.mocked(toast.success).mock.calls[0][0]).toContain(
"Research Mode enabled",
);
expect(onAcknowledged).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("stale-version error surfaces the localised error toast and keeps the dialog open", () => {
const onOpenChange = vi.fn();
render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={onOpenChange}
currentDisclaimerVersion="2026-05-14.1"
/>,
);
mutationCallbacks.onError?.(new Error("research-mode.version.stale"));
expect(vi.mocked(toast.error)).toHaveBeenCalledTimes(1);
expect(vi.mocked(toast.error).mock.calls[0][0]).toContain(
"disclaimer was updated",
);
// The error path never closes the dialog — the caller stays put
// so the user can reload and re-read the new copy.
expect(onOpenChange).not.toHaveBeenCalled();
});
it("generic error path surfaces the generic error toast", () => {
render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={() => {}}
currentDisclaimerVersion="2026-05-14.1"
/>,
);
mutationCallbacks.onError?.(new Error("http-500"));
expect(vi.mocked(toast.error)).toHaveBeenCalledTimes(1);
expect(vi.mocked(toast.error).mock.calls[0][0]).toContain(
"Could not enable Research Mode",
);
});
it("acknowledge CTA is disabled until the server-supplied version arrives", () => {
const html = render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={() => {}}
currentDisclaimerVersion={null}
/>,
);
// The disabled flag stamps as `disabled=""` on a server-rendered button.
// The acknowledge button carries `data-slot="research-mode-acknowledge"`.
const acknowledgeFragment = html.match(
/<button[^>]*data-slot="research-mode-acknowledge"[^>]*>/,
)?.[0];
expect(acknowledgeFragment).toBeDefined();
expect(acknowledgeFragment).toMatch(/\sdisabled(="|\s|>)/);
// The version line is hidden when null — guard against leaking
// `version: null` text into the UI.
expect(html).not.toContain("Disclaimer version:");
});
it("renders German copy when locale='de'", () => {
const html = render(
<ResearchModeAcknowledgmentDialog
open={true}
onOpenChange={() => {}}
currentDisclaimerVersion="2026-05-14.1"
/>,
"de",
);
expect(html).toContain("Forschungsmodus aktivieren");
expect(html).toContain("Verstanden — aktivieren");
expect(html).toContain("Abbrechen");
expect(html).toContain("2017/745");
});
});