mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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.
This commit is contained in:
1 parent
d98e85cd03
commit
4f09c3f862
314 files changed
+26801
-4419
No files matched your search
@@ -28,4 +28,44 @@ describe("<Input>", () => {
|
||||
expect(html).not.toContain("lpignore");
|
||||
expect(html).not.toContain("1p-ignore");
|
||||
});
|
||||
|
||||
it("derives inputMode=decimal from type=number", () => {
|
||||
const html = renderToStaticMarkup(<Input id="weight" type="number" />);
|
||||
expect(html).toContain('inputMode="decimal"');
|
||||
});
|
||||
|
||||
it("derives inputMode=email from type=email", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<Input id="email" type="email" autoComplete="email" />,
|
||||
);
|
||||
expect(html).toContain('inputMode="email"');
|
||||
});
|
||||
|
||||
it("derives inputMode=url from type=url", () => {
|
||||
const html = renderToStaticMarkup(<Input id="webhook" type="url" />);
|
||||
expect(html).toContain('inputMode="url"');
|
||||
});
|
||||
|
||||
it("derives inputMode=tel from type=tel", () => {
|
||||
const html = renderToStaticMarkup(<Input id="phone" type="tel" />);
|
||||
expect(html).toContain('inputMode="tel"');
|
||||
});
|
||||
|
||||
it("derives inputMode=search from type=search", () => {
|
||||
const html = renderToStaticMarkup(<Input id="filter" type="search" />);
|
||||
expect(html).toContain('inputMode="search"');
|
||||
});
|
||||
|
||||
it("omits inputMode for plain text inputs", () => {
|
||||
const html = renderToStaticMarkup(<Input id="note" type="text" />);
|
||||
expect(html).not.toContain("inputMode=");
|
||||
});
|
||||
|
||||
it("respects an explicit inputMode override on type=number", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<Input id="steps" type="number" inputMode="numeric" />,
|
||||
);
|
||||
expect(html).toContain('inputMode="numeric"');
|
||||
expect(html).not.toContain('inputMode="decimal"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
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("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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user