diff --git a/e2e/day-doors.spec.ts b/e2e/day-doors.spec.ts index c83cfa352..357e2773c 100644 --- a/e2e/day-doors.spec.ts +++ b/e2e/day-doors.spec.ts @@ -1,5 +1,6 @@ import type { Locator, Page, TestInfo } from "@playwright/test"; +import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities"; import { DAY_DOORS_ANALYTE } from "./setup/day-doors-fixture"; import { mockDay } from "./setup/day-mock"; import { @@ -203,6 +204,11 @@ test.describe("every chart drawn in days opens its days", () => { .locator('[data-slot="chart-overlay-comparison-lastMonth"]') .click(); await page.keyboard.press("Escape"); + // The overlay menu is closed before the chart is touched, so the tap + // lands on the chart and not on the menu's outside-dismiss. + await expect( + page.locator('[data-slot="chart-overlay-controls-content"]'), + ).toHaveCount(0); await expect( page.locator('[data-slot="chart-compare-caption"]').first(), ).toBeAttached(); @@ -265,9 +271,13 @@ test.describe("the dashboard and the list of all values", () => { }, testInfo) => { test.skip(isPhone(testInfo), "docked from 1280 px"); await page.setViewportSize({ width: 1440, height: 900 }); + // The launcher follows the `coach` capability, which needs a provider + // the suite cannot reach, so the capability reads available here; this + // journey is about where the button sits, not what it opens. + await serveAiBlock(page, aiBlockAvailable()); await page.goto("/insights/weight"); const fab = page.locator('[data-slot="coach-fab"]'); - test.skip((await fab.count()) === 0, "no Coach on this account"); + await expect(fab).toBeVisible(); const plot = page .locator('[data-slot="chart-plot"][data-day-links="true"]') .first(); diff --git a/src/components/day/__tests__/use-day-value-format.test.tsx b/src/components/day/__tests__/use-day-value-format.test.tsx new file mode 100644 index 000000000..7fbca9381 --- /dev/null +++ b/src/components/day/__tests__/use-day-value-format.test.tsx @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import deMessages from "../../../../messages/de.json"; +import enMessages from "../../../../messages/en.json"; + +/** + * A day's values read as their charts read them. The day route sends a + * night's length in "min" and a device score in "score"; the first read as a + * bare 412 and the second as an untranslated "score" before both were + * spelled the way the sleep and recovery charts spell them. + */ + +vi.mock("@/hooks/use-unit-display", async () => { + const { unitDisplayFor } = + await import("@/__tests__/helpers/unit-display-mock"); + return { useUnitDisplay: () => unitDisplayFor("metric") }; +}); + +import { I18nProvider } from "@/lib/i18n/context"; + +import { useDayValueFormat } from "../use-day-value-format"; + +function Probe({ + type, + value, + unit, +}: { + type: string; + value: number; + unit: string; +}) { + const { formatTile } = useDayValueFormat(); + const shown = formatTile({ + key: type, + values: [{ type, value, unit, at: "", source: "", band: null }], + }); + return ( + + {shown.value}|{shown.unit} + + ); +} + +function read( + type: string, + value: number, + unit: string, + locale: "en" | "de" = "en", +) { + return renderToStaticMarkup( + + + , + ) + .replace(/<[^>]+>/g, "") + .replace(//g, ""); +} + +describe("a day's values in their charts' spelling", () => { + it("reads a night sent in min as a duration", () => { + const [value, unit] = read("SLEEP_DURATION", 412, "min").split("|"); + expect(value).toMatch(/6\s*h/); + expect(value).toMatch(/52/); + expect(unit).toBe(""); + }); + + it("reads a device score in the reader's word for points", () => { + // German, where the wire's "score" and the chart's "Punkte" differ. + const [value, unit] = read("ANS_CHARGE", 8, "score", "de").split("|"); + expect(value).toBe("8"); + expect(unit).toBe("Punkte"); + }); +}); diff --git a/src/components/day/day-view.tsx b/src/components/day/day-view.tsx index 0b7ff57a1..55e317a23 100644 --- a/src/components/day/day-view.tsx +++ b/src/components/day/day-view.tsx @@ -54,14 +54,16 @@ import { useDayValueFormat } from "./use-day-value-format"; */ /** "Saturday, 3 January 2026", for the header and the spoken name. */ -export function useLongDayLabel(): (date: DateKey) => string { +export function useLongDayLabel( + length: "long" | "short" = "long", +): (date: DateKey) => string { const { locale } = useTranslations(); const intl = resolveIntlLocale(locale); return (date: DateKey) => new Intl.DateTimeFormat(intl, { - weekday: "long", + weekday: length, day: "numeric", - month: "long", + month: length, year: "numeric", timeZone: "UTC", }).format(new Date(`${date}T12:00:00.000Z`)); @@ -118,6 +120,10 @@ export function DayView({ }: DayViewProps) { const { t, tCount } = useTranslations(); const longLabel = useLongDayLabel(); + // "Mo., 5. Okt. 2026" in the phone sheet's header: a September Thursday + // spelled out in full does not fit beside the arrows at 360 px, and a + // date cut off before its year reads as the wrong day. + const shortLabel = useLongDayLabel("short"); const pathname = usePathname(); const day = useDay(date); const { labelFor, formatTile } = useDayValueFormat(); @@ -218,7 +224,14 @@ export function DayView({ tabIndex={-1} className="min-w-0 flex-1 truncate px-1 text-base leading-snug font-semibold focus-visible:outline-none" > - {longLabel(date)} + {compact ? ( + <> + + {longLabel(date)} + + ) : ( + longLabel(date) + )}