diff --git a/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts b/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts index e65751449..21e0dec33 100644 --- a/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts +++ b/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts @@ -52,6 +52,9 @@ const MINTED_ONLY_BY_THE_BUCKET = [ "measurementReminders.nextDue.none", ]; +/** The hook that yields the zone the surrounding dates are printed in. */ +const DISPLAY_ZONE_HOOK = "useDisplayTimezone"; + const FIX_IT = [ `The due-day bucket lives in src/${HOME} and nowhere else.`, `Import { relativeDueKey } ${HOME_IMPORT} instead of writing a second one.`, @@ -59,6 +62,14 @@ const FIX_IT = [ "and the dashboard card read that body, and they must agree.", ].join(" "); +const FIX_THE_ZONE = [ + "The due phrase has to be worked out in the same zone as the date printed", + `beside it. Read ${DISPLAY_ZONE_HOOK}() and pass it as the third argument to`, + "relativeDueKey. Do not reach for the device clock and do not give the", + "parameter a default: a caller that stays silent is exactly how the phrase", + "and the date drifted a day apart in the first place.", +].join(" "); + function sourceFiles(): string[] { return globSync("**/*.{ts,tsx}", { cwd: SRC }) .map((p) => p.split(sep).join("/")) @@ -121,3 +132,37 @@ describe("the checkup due-day bucket has one home", () => { } }); }); + +describe("the due-day bucket asks the same clock as the date beside it", () => { + it("never floors a day against the device clock", () => { + // `startOfLocalDayInTz(instant, undefined)` means "whatever zone this + // machine is set to". That is the reading the phrase used to take while + // the date next to it took the profile zone. + const home = read(HOME); + + expect(home, FIX_THE_ZONE).not.toMatch( + /startOfLocalDayInTz\([^)]*undefined/, + ); + }); + + it("takes the zone as an argument with no default to fall back on", () => { + const home = read(HOME); + const signature = home.slice( + home.indexOf("export function relativeDueKey"), + home.indexOf("): RelativeDue"), + ); + + expect(signature, FIX_THE_ZONE).toContain("timeZone: string"); + // `timeZone: string = …` or `timeZone = …` would let a caller stay silent. + expect(signature, FIX_THE_ZONE).not.toMatch(/timeZone[^,)]*=/); + }); + + it("is called by surfaces that read the printed date's zone", () => { + const silent = sourceFiles() + .filter((rel) => rel !== HOME) + .filter((rel) => read(rel).includes("relativeDueKey(")) + .filter((rel) => !read(rel).includes(DISPLAY_ZONE_HOOK)); + + expect(silent, FIX_THE_ZONE).toEqual([]); + }); +}); diff --git a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx index 50d820432..3db90c565 100644 --- a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx +++ b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx @@ -19,7 +19,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { CalendarClock, CheckCircle2, Plus } from "lucide-react"; -import { useTranslations } from "@/lib/i18n/context"; +import { useTranslations, useDisplayTimezone } from "@/lib/i18n/context"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -50,6 +50,9 @@ function resolveLabel( export function VorsorgeDashboardCard() { const { t } = useTranslations(); + // Issue #490 — the same profile zone the checkups page derives its due + // phrase in, so the two screens still agree when the device sits elsewhere. + const displayTz = useDisplayTimezone(); const router = useRouter(); const { data: reminders, isLoading } = useMeasurementReminders(); const { satisfy } = useMeasurementReminderMutations(); @@ -112,7 +115,7 @@ export function VorsorgeDashboardCard() { ) : (