test(meds): green the wizard e2e locale and one-shot timezone fixtures

The medication-wizard e2e specs assert the German step counter, but the
seeded e2e user falls back to the English default locale; pin the
`healthlog-locale=de` cookie for the wizard flow and correct the
storage-state cookie allowlist to the hyphenated cookie name the i18n
layer actually reads.

The one-shot cadence integration fixture derived `startsOn`/`endsOn` from
the UTC calendar date but checked emission against the Europe/Berlin
today-window, so the assertion flipped when the suite ran in the UTC
evening. Derive the fixture dates from the same Berlin calendar day the
window uses.

Also refresh the OpenAPI spec version to 1.6.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-30 00:30:27 +02:00
1 parent 8ee7aaac44
commit ac1d03f8ea
4 files changed
+41 -24

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.5.6
version: 1.6.0
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+17
View File
@@ -124,6 +124,23 @@ export async function stubDashboardAnalytics(page: Page): Promise<void> {
* redirects to `/medications?new=1` and opens the create dialog).
*/
export async function openCreateWizard(page: Page): Promise<void> {
// Pin the German locale for the wizard flow. The app default locale is
// English; these specs assert the "Schritt N von M" counter and other
// German label surfaces, which only render when the `healthlog-locale`
// cookie resolves to "de". Both the server layout and the client i18n
// provider read this exact cookie name, so setting it before the first
// navigation makes the dialog render German on first paint. Scoped here
// rather than in the shared auth state so English-asserting specs keep
// the default locale.
await page.context().addCookies([
{
name: "healthlog-locale",
value: "de",
url: page.url().startsWith("http")
? new URL(page.url()).origin
: "http://localhost:3000",
},
]);
await page.goto("/medications/new", { waitUntil: "domcontentloaded" });
await expect(
page.locator('[data-slot="medication-wizard-dialog"]'),
+1 -1
View File
@@ -181,7 +181,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
// already-onboarded ones, so for our pre-seeded completed e2e user
// the cookie is absent and the proxy passes the dashboard through.
state.cookies = state.cookies.filter((c) =>
["healthlog_session", "healthlog_locale", "hl_onboarding"].includes(c.name),
["healthlog_session", "healthlog-locale", "hl_onboarding"].includes(c.name),
);
await writeFile(STORAGE_STATE_PATH, JSON.stringify(state, null, 2));
await ctx.dispose();
@@ -133,6 +133,26 @@ async function loadCanonical(medicationId: string): Promise<{
* matching `getUserTodayBounds` in `src/lib/timezone.ts` so the test
* window matches what the worker would build on that tick.
*/
// UTC midnight on the local calendar day that `now` falls within for `tz`.
// Mirrors the column convention for `startsOn`/`endsOn` (stored as
// `...T00:00:00.000Z` on the local calendar day). Deriving the day from
// the same timezone `todayBounds` uses keeps a one-shot's emit day inside
// the checked window regardless of wall-clock time — reading the UTC date
// instead drifts a day late in the UTC evening when the local zone has
// already rolled into the next calendar day.
function localCalendarMidnightUtc(now: Date, tz: string): Date {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: tz,
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(now);
const y = Number(parts.find((p) => p.type === "year")?.value);
const m = Number(parts.find((p) => p.type === "month")?.value);
const d = Number(parts.find((p) => p.type === "day")?.value);
return new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0));
}
function todayBounds(now: Date, tz: string): { start: Date; end: Date } {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: tz,
@@ -467,17 +487,7 @@ describe("one-shot lifecycle", () => {
it("mints the slot, the intake POST flips medication.active to false", async () => {
const prisma = getPrismaClient();
const today = new Date();
const todayDate = new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
today.getUTCDate(),
0,
0,
0,
0,
),
);
const todayDate = localCalendarMidnightUtc(today, "Europe/Berlin");
const med = await prisma.medication.create({
data: {
@@ -543,17 +553,7 @@ describe("one-shot lifecycle", () => {
it("does NOT deactivate when the intake is skipped", async () => {
const prisma = getPrismaClient();
const today = new Date();
const todayDate = new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
today.getUTCDate(),
0,
0,
0,
0,
),
);
const todayDate = localCalendarMidnightUtc(today, "Europe/Berlin");
const med = await prisma.medication.create({
data: {