mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
Document vault: card list drops a stray glyph and gives the real filename its own line (was truncating on the meta row); the detail sheet's close button aligns correctly — ResponsiveSheet reserved the right gutter for a close button it wasn't rendering, now conditional on showCloseButton. The open-in-coach action reads 'Coach fragen'. Bulk share: a multi-selection in the overview shares several documents in ONE link (documents-only privacy preserved, owner-scoped, up to 50 per link; >50 refuses with a toast rather than silently truncating). Reminders can link to WAIST_CIRCUMFERENCE (additive MeasurementReminderType enum value, OpenAPI regenerated). The WHO-5 and SCI well-being reminder labels were absent in all six locales and rendered their raw dotted keys (the shared 'measurementReminders.types' prefix is what showed as the stray 'Measurement Reminders Types'); added in every language. A new type-labels-i18n test asserts every dynamic reminder-type key resolves non-empty in all six bundles — the static coverage guard cannot see the dynamic key that let this through.
273 lines
7.6 KiB
TypeScript
273 lines
7.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
// `<ResponsiveSheet>` flips between a `<Sheet side="bottom">` and a
|
|
// centred `<Dialog>` via `useIsMobile()`. Vitest runs in the Node
|
|
// environment without `matchMedia`; the hook returns its SSR-safe
|
|
// default (`false`) and is overridden below for the Sheet branch.
|
|
//
|
|
// shadcn `<Sheet>` / `<Dialog>` both wrap Radix Portals — `renderToStaticMarkup`
|
|
// does not materialise portal trees — so we mock the primitives down
|
|
// to passthrough wrappers so the rendered shape is reachable in the
|
|
// static markup.
|
|
|
|
let mobile = false;
|
|
|
|
vi.mock("@/hooks/use-is-mobile", () => ({
|
|
useIsMobile: () => mobile,
|
|
}));
|
|
|
|
vi.mock("@/components/ui/sheet", () => ({
|
|
Sheet: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
|
|
open ? <div data-slot="mock-sheet">{children}</div> : null,
|
|
SheetContent: ({
|
|
children,
|
|
className,
|
|
...rest
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
[key: string]: unknown;
|
|
}) => (
|
|
<div data-slot="mock-sheet-content" className={className} {...rest}>
|
|
{children}
|
|
</div>
|
|
),
|
|
SheetHeader: ({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) => <div className={className}>{children}</div>,
|
|
SheetFooter: ({
|
|
children,
|
|
className,
|
|
...rest
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
[key: string]: unknown;
|
|
}) => (
|
|
<div className={className} {...rest}>
|
|
{children}
|
|
</div>
|
|
),
|
|
SheetTitle: ({ children }: { children: React.ReactNode }) => (
|
|
<h2>{children}</h2>
|
|
),
|
|
SheetDescription: ({ children }: { children: React.ReactNode }) => (
|
|
<p>{children}</p>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/dialog", () => ({
|
|
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
|
|
open ? <div data-slot="mock-dialog">{children}</div> : null,
|
|
DialogContent: ({
|
|
children,
|
|
className,
|
|
...rest
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
[key: string]: unknown;
|
|
}) => (
|
|
<div data-slot="mock-dialog-content" className={className} {...rest}>
|
|
{children}
|
|
</div>
|
|
),
|
|
DialogHeader: ({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) => <div className={className}>{children}</div>,
|
|
DialogFooter: ({
|
|
children,
|
|
className,
|
|
...rest
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
[key: string]: unknown;
|
|
}) => (
|
|
<div className={className} {...rest}>
|
|
{children}
|
|
</div>
|
|
),
|
|
DialogTitle: ({ children }: { children: React.ReactNode }) => (
|
|
<h2>{children}</h2>
|
|
),
|
|
DialogDescription: ({ children }: { children: React.ReactNode }) => (
|
|
<p>{children}</p>
|
|
),
|
|
}));
|
|
|
|
import { ResponsiveSheet } from "../responsive-sheet";
|
|
|
|
describe("<ResponsiveSheet>", () => {
|
|
it("renders the Dialog branch on `md+` viewports", () => {
|
|
mobile = false;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Add measurement"
|
|
description="Log a new reading"
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain('data-variant="dialog"');
|
|
expect(html).toContain("Add measurement");
|
|
expect(html).toContain("Log a new reading");
|
|
expect(html).toContain("body");
|
|
// Sheet branch markers must be absent.
|
|
expect(html).not.toContain('data-slot="mock-sheet"');
|
|
});
|
|
|
|
it("renders the bottom Sheet branch on narrow viewports", () => {
|
|
mobile = true;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet open onOpenChange={() => {}} title="Add measurement">
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain('data-variant="sheet"');
|
|
expect(html).toContain('data-slot="mock-sheet"');
|
|
expect(html).toContain("Add measurement");
|
|
expect(html).toContain("body");
|
|
mobile = false;
|
|
});
|
|
|
|
it("renders a sticky-pinned footer on the Sheet branch", () => {
|
|
mobile = true;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Add measurement"
|
|
footer={<button type="button">Save</button>}
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain('data-slot="responsive-sheet-footer"');
|
|
expect(html).toContain("sticky");
|
|
expect(html).toContain("Save");
|
|
mobile = false;
|
|
});
|
|
|
|
it("renders a flow-layout footer on the Dialog branch", () => {
|
|
mobile = false;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Add measurement"
|
|
footer={<button type="button">Save</button>}
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain('data-slot="responsive-sheet-footer"');
|
|
expect(html).not.toContain("sticky bottom-0");
|
|
expect(html).toContain("Save");
|
|
});
|
|
|
|
it("hides the visual header but keeps the title accessible via sr-only when hideHeader is set", () => {
|
|
mobile = false;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Edit medication"
|
|
hideHeader
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain("sr-only");
|
|
expect(html).toContain("Edit medication");
|
|
});
|
|
|
|
it("reserves the close-button gutter on the Sheet header by default", () => {
|
|
mobile = true;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet open onOpenChange={() => {}} title="Detail">
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
// Default showCloseButton=true → the primitive paints its absolute X, so
|
|
// the header reserves the right gutter for it.
|
|
expect(html).toContain("pr-12");
|
|
mobile = false;
|
|
});
|
|
|
|
it("drops the Sheet header gutter when showCloseButton is false", () => {
|
|
mobile = true;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Detail"
|
|
showCloseButton={false}
|
|
headerAction={<button type="button">X</button>}
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
// No primitive close button → no reserved gutter; the headerAction sits
|
|
// flush to the header's p-4 right edge.
|
|
expect(html).not.toContain("pr-12");
|
|
mobile = false;
|
|
});
|
|
|
|
it("reserves the Dialog header gutter for headerAction by default", () => {
|
|
mobile = false;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Detail"
|
|
headerAction={<button type="button">X</button>}
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).toContain("pr-9");
|
|
});
|
|
|
|
it("drops the Dialog header gutter when showCloseButton is false", () => {
|
|
mobile = false;
|
|
const html = renderToStaticMarkup(
|
|
<ResponsiveSheet
|
|
open
|
|
onOpenChange={() => {}}
|
|
title="Detail"
|
|
showCloseButton={false}
|
|
headerAction={<button type="button">X</button>}
|
|
>
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
expect(html).not.toContain("pr-9");
|
|
});
|
|
|
|
it("does not throw when onOpenChange is invoked (controlled-state plumbing)", () => {
|
|
mobile = false;
|
|
const onOpenChange = vi.fn();
|
|
// Mounted closed — Dialog mock returns null, smoke-only render
|
|
// confirms no throw + the prop wiring round-trips.
|
|
renderToStaticMarkup(
|
|
<ResponsiveSheet open={false} onOpenChange={onOpenChange} title="closed">
|
|
<p>body</p>
|
|
</ResponsiveSheet>,
|
|
);
|
|
onOpenChange(true);
|
|
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
});
|
|
});
|