mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(ui): Apple Health source badge + restore-route enum sync
The measurements list now paints `APPLE_HEALTH` rows with a Dracula- pink chip whose accent matches the iOS app's brand. The localised label ships in both English and German via the new `measurements.sourceAppleHealth` key; the helper falls back to the SCREAMING_SNAKE value for unknown sources so a missing key would surface immediately instead of silently rendering blank. Admin backup restore guards (`MEASUREMENT_TYPES`, `MEASUREMENT_SOURCES`) gain the seven new HealthKit-shaped types and the new source value so a restore from a backup that contains Apple Health rows doesn't get rejected as malformed. A new badge regression test mirrors the existing BP-badge guard: asserts the localised label paints, the Dracula-pink utility classes land, and the bilingual locale still renders the same brand spelling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5cc0a00eaf
commit
86a80c5280
5 files changed
+125
-5
No files matched your search
@@ -302,6 +302,7 @@
|
||||
"sourceManual": "Manuell",
|
||||
"sourceWithings": "Withings",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"systolicLabel": "Systolisch (mmHg)",
|
||||
"diastolicLabel": "Diastolisch (mmHg)",
|
||||
"pulseLabel": "Puls (bpm)",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"sourceManual": "Manual",
|
||||
"sourceWithings": "Withings",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"systolicLabel": "Systolic (mmHg)",
|
||||
"diastolicLabel": "Diastolic (mmHg)",
|
||||
"pulseLabel": "Pulse (bpm)",
|
||||
|
||||
@@ -80,9 +80,22 @@ const MEASUREMENT_TYPES = new Set([
|
||||
"TOTAL_BODY_WATER",
|
||||
"BONE_MASS",
|
||||
"OXYGEN_SATURATION",
|
||||
// ── v1.4.23 Apple Health additions ──
|
||||
"HEART_RATE_VARIABILITY",
|
||||
"RESTING_HEART_RATE",
|
||||
"ACTIVE_ENERGY_BURNED",
|
||||
"FLIGHTS_CLIMBED",
|
||||
"WALKING_RUNNING_DISTANCE",
|
||||
"VO2_MAX",
|
||||
"BODY_TEMPERATURE",
|
||||
]);
|
||||
|
||||
const MEASUREMENT_SOURCES = new Set(["MANUAL", "WITHINGS", "IMPORT"]);
|
||||
const MEASUREMENT_SOURCES = new Set([
|
||||
"MANUAL",
|
||||
"WITHINGS",
|
||||
"IMPORT",
|
||||
"APPLE_HEALTH",
|
||||
]);
|
||||
const INTAKE_SOURCES = new Set(["WEB", "API", "REMINDER", "IMPORT"]);
|
||||
|
||||
const handler = apiHandler(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* v1.4.23 — Apple Health source badge regression guard.
|
||||
*
|
||||
* Mirrors the BP-badge guard above: any non-MANUAL source must paint a
|
||||
* Badge with the localised source label. APPLE_HEALTH gets the
|
||||
* Dracula-pink chip; the helper falls back to the SCREAMING_SNAKE
|
||||
* value if it's an unknown source so this test would catch a missing
|
||||
* `sourceAppleHealth` translation key as a stray `APPLE_HEALTH`
|
||||
* substring leaking into the DOM.
|
||||
*/
|
||||
|
||||
const baseMeasurements = [
|
||||
{
|
||||
id: "m-apple-1",
|
||||
type: "WEIGHT",
|
||||
value: 81.5,
|
||||
unit: "kg",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: "2026-05-09T08:30:00.000Z",
|
||||
notes: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/measurements",
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({
|
||||
data: { measurements: baseMeasurements, meta: { total: 1 } },
|
||||
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(locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<MeasurementList />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("MeasurementList — APPLE_HEALTH badge", () => {
|
||||
it("renders the localised Apple Health label, never the raw enum", () => {
|
||||
const html = render("en");
|
||||
expect(html).toContain("Apple Health");
|
||||
expect(html).not.toContain(">APPLE_HEALTH<");
|
||||
});
|
||||
|
||||
it("paints the badge with the Dracula-pink utility classes", () => {
|
||||
const html = render("en");
|
||||
// The chip class composition (`border-dracula-pink/50`,
|
||||
// `bg-dracula-pink/15`, `text-dracula-pink`) is what gives the
|
||||
// badge its iOS-accent identity. Asserting all three keeps the
|
||||
// colour contract from drifting silently.
|
||||
expect(html).toMatch(/border-dracula-pink\/50/);
|
||||
expect(html).toMatch(/bg-dracula-pink\/15/);
|
||||
expect(html).toMatch(/text-dracula-pink/);
|
||||
});
|
||||
|
||||
it("renders the localised label in German too", () => {
|
||||
const html = render("de");
|
||||
expect(html).toContain("Apple Health");
|
||||
});
|
||||
});
|
||||
@@ -106,9 +106,10 @@ function toDateTimeLocalValue(isoString: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`)
|
||||
* using the existing `measurements.source*` translation keys instead of
|
||||
* leaking the SCREAMING_SNAKE enum into the table cell.
|
||||
* Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`
|
||||
* / `APPLE_HEALTH`) using the existing `measurements.source*`
|
||||
* translation keys instead of leaking the SCREAMING_SNAKE enum into
|
||||
* the table cell.
|
||||
*/
|
||||
function formatMeasurementSource(
|
||||
source: string,
|
||||
@@ -117,9 +118,22 @@ function formatMeasurementSource(
|
||||
if (source === "WITHINGS") return t("measurements.sourceWithings");
|
||||
if (source === "IMPORT") return t("measurements.sourceImport");
|
||||
if (source === "MANUAL") return t("measurements.sourceManual");
|
||||
if (source === "APPLE_HEALTH") return t("measurements.sourceAppleHealth");
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-source badge colour. `APPLE_HEALTH` gets the Dracula pink that
|
||||
* matches the iOS app's accent; everything else falls back to the
|
||||
* shadcn outline default (no class override).
|
||||
*/
|
||||
function sourceBadgeClass(source: string): string {
|
||||
if (source === "APPLE_HEALTH") {
|
||||
return "border-dracula-pink/50 bg-dracula-pink/15 text-dracula-pink";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
@@ -431,7 +445,10 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{m.source !== "MANUAL" && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${sourceBadgeClass(m.source)}`.trim()}
|
||||
>
|
||||
{formatMeasurementSource(m.source, t)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user