mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Bucket A of the v1.4.3 polish pass.
Tile-strip width drifted past the chart-strip width by 16px on >=md
because the strip used `-mx-2 px-2` to bleed two pixels past the
parent for a snap-scroll feel. The bleed is real on mobile — the
horizontal-scroll affordance only reads as a scroll if the first/last
tiles touch the viewport edge — but on tablet/desktop where the row
fits without scrolling it just made the tile group visibly wider than
the charts below. Now `md:mx-0 md:px-0` drops the bleed once charts
fit inline, so the two strips share the same content box.
Tile heights varied because the blood-pressure tile rendered as two
stacked TrendCards (sys + dia) inside one tile slot while every
other tile was a single TrendCard. The cleaner fix is one TrendCard
that displays the paired value as `117/79` — TrendCard now accepts an
optional `secondary: { latest, avg7, avg30 }` and the dashboard pushes
ONE BP entry instead of a Fragment of two. Tile count drops from 6
to 5, the strip stops wrapping awkwardly, and the BP card carries
the systolic sparkline + traffic-light tooltip while the diastolic
value rides along under the same heading. Each tile wrapper got
`flex` and the card itself `flex h-full flex-col`, so flex-stretch
inside the strip can do its job and the avg7/avg30 row pins to the
bottom.
Settings nav split: the section formerly titled "Übersicht" was
renamed to "Dashboard" (the term every user already knows from the
main nav), and the embedded Persönliche Zielwerte panel moved out
into its own top-level settings section under `/settings/thresholds`.
Two reasons: the panel is heavy (one card per metric) and was
visually buried behind the layout customizer, and "Übersicht" leaked
internal vocabulary — neither the route slug nor the surrounding nav
entries used it. Tests guarding slug order + section count updated
in lockstep.
Touched:
- messages/de.json, messages/en.json
- src/app/page.tsx (dashboard tile strip)
- src/app/settings/[section]/page.tsx (route wiring)
- src/components/charts/trend-card.tsx (secondary-metric prop)
- src/components/settings/section-slugs.ts
- src/components/settings/settings-shell.tsx (new entry + icon)
- src/components/settings/section-placeholder.tsx
- src/components/settings/dashboard-section.tsx (drop ThresholdsSection)
- src/components/settings/thresholds-settings-section.tsx (new)
- src/components/settings/__tests__/{sections,settings-shell}.test.tsx
Tests + typecheck green (669/669).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
// Stub out next/navigation. The settings sections that read it (Account)
|
|
// only call `useRouter().push()` from inside an effect after mount, so a
|
|
// no-op stub is enough for SSR smoke rendering.
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
|
usePathname: () => "/settings/account",
|
|
}));
|
|
|
|
// Stub TanStack Query — every section reads from it but we only need the
|
|
// SSR markup, not live data. Returning a stable empty payload keeps the
|
|
// "loading" branch from spinning forever and lets the section title render.
|
|
// The About section reads `["api", "version"]` and gates the body on data
|
|
// presence — return a representative payload so the version card paints.
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useQuery: ({ queryKey }: { queryKey: unknown[] }) => {
|
|
if (
|
|
Array.isArray(queryKey) &&
|
|
queryKey[0] === "api" &&
|
|
queryKey[1] === "version"
|
|
) {
|
|
return {
|
|
data: {
|
|
version: "1.4.0",
|
|
buildSha: "abc1234567",
|
|
builtAt: "2026-05-08T12:00:00Z",
|
|
license: "AGPL-3.0",
|
|
repository: "https://github.com/MBombeck/HealthLog",
|
|
changelog:
|
|
"https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md",
|
|
docs: "https://docs.healthlog.dev",
|
|
},
|
|
isLoading: false,
|
|
refetch: vi.fn(),
|
|
};
|
|
}
|
|
return { data: null, isLoading: false, refetch: vi.fn() };
|
|
},
|
|
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
|
|
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
}));
|
|
|
|
// Stub the auth hook — sections check `isAuthenticated` to enable queries.
|
|
// We return `isAuthenticated: true` + a minimal user so the loading-spinner
|
|
// branch in AccountSection paints the real form.
|
|
vi.mock("@/hooks/use-auth", () => ({
|
|
useAuth: () => ({
|
|
user: {
|
|
id: "u1",
|
|
username: "marc",
|
|
email: "marc@example.com",
|
|
heightCm: 180,
|
|
dateOfBirth: "1990-01-01",
|
|
gender: "MALE",
|
|
role: "USER",
|
|
},
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
refetch: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
// Sub-sections of Dashboard own complex data fetching that's already
|
|
// covered by their own tests. Stub at the import boundary.
|
|
vi.mock("@/components/settings/dashboard-layout-section", () => ({
|
|
DashboardLayoutSection: () => <div data-testid="dashboard-layout" />,
|
|
}));
|
|
vi.mock("@/components/settings/thresholds-section", () => ({
|
|
ThresholdsSection: () => <div data-testid="thresholds" />,
|
|
}));
|
|
|
|
import { I18nProvider } from "@/lib/i18n/context";
|
|
import { AboutSection } from "../about-section";
|
|
import { AccountSection } from "../account-section";
|
|
import { AdvancedSection } from "../advanced-section";
|
|
import { AiSection } from "../ai-section";
|
|
import { ApiSection } from "../api-section";
|
|
import { DashboardSection } from "../dashboard-section";
|
|
import { IntegrationsSection } from "../integrations-section";
|
|
import { NotificationsSection } from "../notifications-section";
|
|
|
|
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
|
return renderToStaticMarkup(
|
|
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
describe("settings sections — SSR smoke", () => {
|
|
it("<AccountSection> renders the Account heading and Profile card", () => {
|
|
const html = render(<AccountSection />);
|
|
// Section title resolves via the i18n provider.
|
|
expect(html).toContain("Account");
|
|
// Profile card heading is also painted.
|
|
expect(html).toContain("Profile");
|
|
// No raw key leaks past the i18n layer.
|
|
expect(html).not.toContain("settings.sections.");
|
|
});
|
|
|
|
it("<AboutSection> renders version + license + check-for-updates button", () => {
|
|
const html = render(<AboutSection />);
|
|
expect(html).toContain("About");
|
|
// The new about.* keys resolve.
|
|
expect(html).toContain("Check for updates");
|
|
expect(html).not.toContain("settings.about.");
|
|
});
|
|
|
|
it("<AiSection> renders the AI Insights card", () => {
|
|
const html = render(<AiSection />);
|
|
expect(html).toContain("AI Insights");
|
|
});
|
|
|
|
it("<IntegrationsSection> renders Withings card title", () => {
|
|
const html = render(<IntegrationsSection />);
|
|
expect(html).toContain("Integrations");
|
|
expect(html).toContain("Withings");
|
|
});
|
|
|
|
it("<NotificationsSection> renders heading", () => {
|
|
const html = render(<NotificationsSection />);
|
|
expect(html).toContain("Notifications");
|
|
});
|
|
|
|
it("<DashboardSection> renders heading and embeds the layout customizer", () => {
|
|
// v1.4.3 split: the per-metric thresholds (Persönliche Zielwerte) moved
|
|
// out into their own settings section under `/settings/thresholds`,
|
|
// so the dashboard panel is now layout-only. Verifying the heading
|
|
// says "Dashboard" (not "Übersicht" — the v1.4.2 placeholder) and the
|
|
// layout slot still mounts.
|
|
const html = render(<DashboardSection />);
|
|
expect(html).toContain("Dashboard");
|
|
expect(html).toContain('data-testid="dashboard-layout"');
|
|
expect(html).not.toContain('data-testid="thresholds"');
|
|
});
|
|
|
|
it("<ApiSection> renders endpoints + tokens cards", () => {
|
|
const html = render(<ApiSection />);
|
|
expect(html).toContain("API & Tokens");
|
|
});
|
|
|
|
it("<AdvancedSection> renders export + danger-zone cards", () => {
|
|
const html = render(<AdvancedSection />);
|
|
expect(html).toContain("Advanced");
|
|
});
|
|
|
|
it("<AboutSection> resolves about.* keys in German", () => {
|
|
const html = render(<AboutSection />, "de");
|
|
expect(html).toContain("Auf Updates prüfen");
|
|
});
|
|
});
|