From 9a74f8e8f202de4d8ce0c29bd7cbe528e9a6837a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 21:51:10 +0200 Subject: [PATCH] feat(lists): empty states for measurements, mood, medications, achievements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the EmptyState primitive into the four feature lists. Each path now exposes an icon + localized title + description + (where it applies) a CTA bound to the parent dialog so brand-new accounts get a single-click path to logging their first datapoint. - /measurements — Activity icon, "Add your first measurement" CTA wired to the page's Add dialog. Filter-aware: switches to "no measurements match this filter" with a "Show all types" reset whenever a non-default type filter is active. - /mood — Smile icon, "Log your first mood" CTA, identical filter-aware split for the mood-level filter. - /medications — Refactor inline icon+text+button block to the shared primitive. Pill icon, "Add first medication" CTA → openCreate. - /achievements — Trophy icon. Reuses the existing achievements.noneUnlockedYet string to keep C4 i18n sweep work minimal. New unit test (measurement-list-empty) asserts the primitive mounts, the Add-first CTA respects the optional onAddFirst prop (no orphaned button when the parent doesn't wire it), and the reset-filter CTA stays out of the default-filter render. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/measurement-list-empty.test.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/components/measurements/__tests__/measurement-list-empty.test.tsx diff --git a/src/components/measurements/__tests__/measurement-list-empty.test.tsx b/src/components/measurements/__tests__/measurement-list-empty.test.tsx new file mode 100644 index 000000000..c209bdf4e --- /dev/null +++ b/src/components/measurements/__tests__/measurement-list-empty.test.tsx @@ -0,0 +1,91 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +/** + * v1.4.15 phase-C5 — `/measurements` empty state. + * + * Before this phase the list rendered a single sentence + * ("No measurements yet") in a dashed-border box with no CTA. Brand- + * new accounts had to scroll back up to find the header "Add" button. + * The new EmptyState surface mounts the primitive with an Activity + * icon, localized copy, and an "Add your first measurement" CTA wired + * to the parent dialog. + * + * The filter-aware copy switches to "no measurements match this + * filter" + a "Show all types" reset CTA when a non-default filter is + * active — guarded indirectly by the absence assertion here (the test + * runs the default ALL filter; reset CTA must NOT appear). + */ + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/measurements", +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: { measurements: [], meta: { total: 0 } }, + isLoading: false, + }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), + useMutation: () => ({ + mutate: vi.fn(), + mutateAsync: vi.fn(), + isPending: false, + isError: false, + error: null, + }), +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", username: "marc", role: "USER" }, + isAuthenticated: true, + isLoading: false, + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { MeasurementList } from "../measurement-list"; + +function render(props: { onAddFirst?: () => void } = {}) { + return renderToStaticMarkup( + + + , + ); +} + +describe("MeasurementList — empty state", () => { + it("renders the EmptyState primitive when no measurements exist", () => { + const html = render(); + expect(html).toContain('role="status"'); + expect(html).toContain('aria-live="polite"'); + expect(html).toContain("border-dashed"); + }); + + it("renders the localized title and description", () => { + const html = render(); + expect(html).toContain("No measurements yet"); + expect(html).toContain( + "Log your weight, blood pressure, pulse, or other vitals", + ); + }); + + it("exposes the Add-first CTA when onAddFirst is wired", () => { + const html = render({ onAddFirst: () => {} }); + expect(html).toContain("Add your first measurement"); + }); + + it("hides the Add-first CTA when no callback is provided", () => { + const html = render(); + // The fallback is the parent's header CTA; the empty card itself + // must not render a button that does nothing. + expect(html).not.toContain("Add your first measurement"); + }); + + it("does not render the reset-filter CTA on the default filter", () => { + const html = render(); + expect(html).not.toContain("Show all types"); + }); +});