mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(medications): research-mode MDR acknowledgment dialog
Opt-in modal that gates the GLP-1 drug-level chart behind an MDR-aware disclaimer. Reads the live disclaimer version from the server and posts it back on acknowledge so a server-side bump forces re-acknowledgment. Sections cover what the chart is, what it isn't, why population PK is an estimate, the EU 2017/745 + MDCG 2021-24 boundary, and EMA + ASCPT psp4.13099 citations. Stale-version 400 surfaces a localised error toast and keeps the dialog open.
This commit is contained in:
1 parent
6d748de389
commit
3502f57694
8 files changed
+694
No files matched your search
@@ -0,0 +1,275 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.4.25 W19c-Frontend — Research-mode MDR acknowledgment dialog.
|
||||
*
|
||||
* Opt-in gate in front of the GLP-1 drug-level chart. The chart paints
|
||||
* a unit-less Bateman PK curve over the user's logged intakes (research
|
||||
* §2.3 / §9.6.7). Because a "drug-level" surface is the brightest EU
|
||||
* MDR Class IIa edge in HealthLog's surface area (research §11 + §12.4),
|
||||
* the chart never paints until the user has explicitly read and
|
||||
* acknowledged the disclaimer below. The acknowledgment record is
|
||||
* server-stamped with `RESEARCH_MODE_DISCLAIMER_VERSION`; if the copy
|
||||
* changes the user re-acknowledges before the chart unhides.
|
||||
*
|
||||
* The dialog is rendered controlled by the Settings toggle and the
|
||||
* "re-prompt" banner (commit 3 of this phase). It accepts:
|
||||
* - `open` / `onOpenChange` for full external control
|
||||
* - `currentDisclaimerVersion` — the server's authoritative version
|
||||
* string. Acknowledgment posts THIS value, not the imported
|
||||
* constant — server redeploys that bump the constant must be
|
||||
* reflected without a client reload (W19c-Backend phase report).
|
||||
* - `onAcknowledged()` — fires after a successful 200; the caller
|
||||
* re-fetches the research-mode state and renders the toggle in
|
||||
* the new state.
|
||||
*
|
||||
* Copy direction (Marc-Voice, English, no AI/phase/wave mentions):
|
||||
* - "What this is" — single-paragraph framing: the chart is an
|
||||
* estimate from EMA-published population PK.
|
||||
* - "What this isn't" — explicit boundary: not a measurement, not
|
||||
* personalised, not medical advice.
|
||||
* - "Why it's an estimate" — population PK + IIV; per-patient
|
||||
* variation is large.
|
||||
* - "MDR boundary" — cite EU 2017/745 and MDCG 2021-24 verbatim.
|
||||
* - "Citations" — EMA EPAR + Schneck/Urva 2024 (ASCPT psp4.13099).
|
||||
*
|
||||
* Toast surface uses sonner (the project convention) so the user gets
|
||||
* a transient confirmation on either success or rejection.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { AlertTriangle, BookOpenCheck, Loader2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export interface ResearchModeAcknowledgmentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (next: boolean) => void;
|
||||
/**
|
||||
* The version string from `GET /api/auth/me/research-mode`. The
|
||||
* server compares it byte-for-byte against the current constant; a
|
||||
* stale value forces a 400 from the API which we surface as an
|
||||
* error toast and keep the dialog open. Pass `null` while the
|
||||
* GET is in flight; the Acknowledge CTA stays disabled.
|
||||
*/
|
||||
currentDisclaimerVersion: string | null;
|
||||
/**
|
||||
* Fires after the POST returns 200. Caller invalidates the
|
||||
* `research-mode` query so the toggle reflects the new state.
|
||||
*/
|
||||
onAcknowledged?: () => void;
|
||||
}
|
||||
|
||||
export function ResearchModeAcknowledgmentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentDisclaimerVersion,
|
||||
onAcknowledged,
|
||||
}: ResearchModeAcknowledgmentDialogProps) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (version: string) => {
|
||||
const res = await fetch("/api/auth/me/research-mode", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ acknowledged: true, version }),
|
||||
});
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const code =
|
||||
typeof json?.error === "string"
|
||||
? json.error
|
||||
: `http-${res.status}`;
|
||||
throw new Error(code);
|
||||
}
|
||||
return json?.data ?? null;
|
||||
},
|
||||
onSuccess: () => {
|
||||
setErrorMessage(null);
|
||||
toast.success(t("medications.researchMode.dialog.successToast"));
|
||||
queryClient.invalidateQueries({ queryKey: ["research-mode"] });
|
||||
onAcknowledged?.();
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
// The 400 stale-version path is the only one we surface
|
||||
// explicitly — every other failure mode (401, 429, 500, network)
|
||||
// collapses to a generic error so the dialog stays open but the
|
||||
// user knows the acknowledgment didn't persist.
|
||||
const code = err instanceof Error ? err.message : "unknown";
|
||||
const isStale = code === "research-mode.version.stale";
|
||||
const message = isStale
|
||||
? t("medications.researchMode.dialog.staleVersionToast")
|
||||
: t("medications.researchMode.dialog.errorToast");
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
function handleAcknowledge() {
|
||||
if (!currentDisclaimerVersion) return;
|
||||
mutation.mutate(currentDisclaimerVersion);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setErrorMessage(null);
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
data-slot="research-mode-acknowledgment-dialog"
|
||||
className="max-h-[90vh] overflow-y-auto sm:max-w-xl"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<BookOpenCheck className="text-dracula-purple h-5 w-5 shrink-0" />
|
||||
{t("medications.researchMode.dialog.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.researchMode.dialog.intro")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 text-sm">
|
||||
<section
|
||||
aria-labelledby="research-mode-what-it-is"
|
||||
data-slot="research-mode-what-it-is"
|
||||
>
|
||||
<h3
|
||||
id="research-mode-what-it-is"
|
||||
className="text-foreground font-medium"
|
||||
>
|
||||
{t("medications.researchMode.dialog.whatItIsHeader")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t("medications.researchMode.dialog.whatItIs")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="research-mode-what-it-isnt"
|
||||
data-slot="research-mode-what-it-isnt"
|
||||
>
|
||||
<h3
|
||||
id="research-mode-what-it-isnt"
|
||||
className="text-foreground font-medium"
|
||||
>
|
||||
{t("medications.researchMode.dialog.whatItIsntHeader")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t("medications.researchMode.dialog.whatItIsnt")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="research-mode-why-estimate"
|
||||
data-slot="research-mode-why-estimate"
|
||||
>
|
||||
<h3
|
||||
id="research-mode-why-estimate"
|
||||
className="text-foreground font-medium"
|
||||
>
|
||||
{t("medications.researchMode.dialog.whyEstimateHeader")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t("medications.researchMode.dialog.whyEstimate")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="research-mode-mdr"
|
||||
data-slot="research-mode-mdr-boundary"
|
||||
className="bg-warning/10 border-warning/30 rounded-md border-l-4 px-3 py-2"
|
||||
>
|
||||
<h3
|
||||
id="research-mode-mdr"
|
||||
className="text-foreground flex items-center gap-2 font-medium"
|
||||
>
|
||||
<AlertTriangle
|
||||
className="text-warning h-4 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("medications.researchMode.dialog.mdrBoundaryHeader")}
|
||||
</h3>
|
||||
<p className="text-foreground/85 mt-1">
|
||||
{t("medications.researchMode.dialog.mdrBoundary")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="research-mode-citations"
|
||||
data-slot="research-mode-citations"
|
||||
>
|
||||
<h3
|
||||
id="research-mode-citations"
|
||||
className="text-foreground font-medium"
|
||||
>
|
||||
{t("medications.researchMode.dialog.citationsHeader")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t("medications.researchMode.dialog.citations")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{currentDisclaimerVersion && (
|
||||
<p
|
||||
className="text-muted-foreground text-xs italic"
|
||||
data-slot="research-mode-version"
|
||||
>
|
||||
{t("medications.researchMode.dialog.versionLine", {
|
||||
version: currentDisclaimerVersion,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<p
|
||||
role="alert"
|
||||
className="text-destructive text-sm"
|
||||
data-slot="research-mode-error"
|
||||
>
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={mutation.isPending}
|
||||
data-slot="research-mode-cancel"
|
||||
>
|
||||
{t("medications.researchMode.dialog.cancelCta")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAcknowledge}
|
||||
disabled={mutation.isPending || !currentDisclaimerVersion}
|
||||
data-slot="research-mode-acknowledge"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{t("medications.researchMode.dialog.acknowledgeCta")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Radix Dialog uses Portals which `renderToStaticMarkup` does not
|
||||
// realise. We collapse the primitives to plain wrappers so the body of
|
||||
// the dialog is reachable in the static markup.
|
||||
vi.mock("@/components/ui/dialog", () => ({
|
||||
Dialog: ({
|
||||
open,
|
||||
children,
|
||||
}: {
|
||||
open: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => (open ? <div data-slot="mock-dialog">{children}</div> : null),
|
||||
DialogContent: ({
|
||||
children,
|
||||
...rest
|
||||
}: React.ComponentProps<"div"> & { children: React.ReactNode }) => (
|
||||
<div data-slot="mock-dialog-content" {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogHeader: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-slot="mock-dialog-header">{children}</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<h2 data-slot="mock-dialog-title">{children}</h2>
|
||||
),
|
||||
DialogDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<p data-slot="mock-dialog-description">{children}</p>
|
||||
),
|
||||
DialogFooter: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-slot="mock-dialog-footer">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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't");
|
||||
expect(html).toContain("Why it'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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user