chore(release): v1.4.39.3 — dashboard slim/thick merge robustness and list-page precision

The post-deploy CI for v1.4.39.2 surfaced eight e2e failures across
chromium-desktop and chromium-mobile (chart-overlay-controls,
dashboard, charts-mobile, measurement-flow) that all routed through
two narrow regressions and one durable test-fixture gap.

### Fixed

- Dashboard slim/thick merge no longer blanks the tile strip when the
  slim slice resolves with an empty summaries record. The v1.4.39.2
  inline `slim?.summaries ?? thick?.summaries` short-circuited on
  JavaScript's truthy-empty-object even when thick carried the full
  payload. The merge moved to a pure `mergeSlimAndThickAnalytics`
  helper that uses object emptiness as the discriminator: populated
  slim still wins on overlapping fields (progressive-paint contract
  preserved), empty slim falls through to thick, both empty leaves
  the dashboard's data-floor gates to render the appropriate empty
  state. Eight new behaviour-level unit tests cover the edge cases.
- `MeasurementList` non-grouped rows now preserve decimal precision
  on both the desktop table and the mobile card grid. The v1.4.37 W7c
  collapsed-list view passed every value through `fmt.integer`, which
  kept the per-day step / activity aggregates correct but silently
  truncated single weight / body-fat / temperature readings — 78.4 kg
  rendered as "78 kg". Grouped rows (cumulative HK type day
  aggregates) stay on `fmt.integer` because their readings are
  integer-only; non-grouped rows now use `fmt.number` which honours
  up to three fraction digits without forcing a minimum.

### Changed

- Playwright route mocks for `/api/analytics` migrate from the
  literal `**/api/analytics` string glob to the regex form
  `/\/api\/analytics(\?|$)/` across every authenticated spec. The
  string glob does not match the sliced URL form
  `/api/analytics?slice=summaries` the v1.4.39.2 dashboard split
  fires alongside the thick request, so the slim request fell
  through to the real route on the seeded test user that returns
  empty summaries. Ten specs migrate: dashboard,
  chart-overlay-controls, measurement-flow, charts-mobile, a11y,
  v1427-responsive-sheet, v1427-coach-launch,
  v1427-measurements-add-param, v1427-insights-empty-state,
  onboarding-tour-passthrough. The regex form is the durable
  alignment with the v1.4.37 W-CI lesson that already swept
  `onboarding-flicker.spec.ts` and `mobile-viewport.spec.ts`.

### Notes

- No schema migration. Runtime + test-fixture-only changes.
- `pnpm test --run` green at 4662 / 4663 (1 long-standing skip).
- Full Playwright suite green at 116 passing / 36 skipped / 152 total
  on both `chromium-desktop` and `chromium-mobile` profiles, against
  the production Next.js build. Closes every failure from CI run
  26213226723.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 10:31:18 +02:00
1 parent d665a5db42
commit dbb5979a97
16 files changed
+426 -34

No files matched your search

+5 -1
View File
@@ -70,7 +70,11 @@ test.describe("axe-core authenticated surfaces", () => {
// need fully-painted DOM, and a flaky API would otherwise stall the
// analyse() call until the 30s test timeout.
test.beforeEach(async ({ page }) => {
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — match `/api/analytics` AND any sliced variant
// (`?slice=summaries`); the v1.4.39.2 dashboard split fires both
// in parallel and the literal string glob misses the query-
// string form.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+7 -1
View File
@@ -21,7 +21,13 @@ test.describe("chart overlay controls", () => {
test.beforeEach(async ({ page }) => {
// Mock analytics + measurements so the dashboard paints at least
// one chart card. Same seed shape the dashboard.spec.ts uses.
await page.route("**/api/analytics", (route) =>
//
// v1.4.39.3 — match `/api/analytics` AND any sliced variant
// (`?slice=summaries`). See the comment on dashboard.spec.ts for
// the underlying minimatch-vs-query-string gap; the regex form
// catches both the thick and slim slice URLs the dashboard now
// mounts in parallel.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+7 -1
View File
@@ -33,7 +33,13 @@ test.describe("charts — mobile-viewport regression", () => {
// Mock analytics + measurements so the chart wrappers all clear
// their data-floor gates. Same pattern as `dashboard.spec.ts`.
await page.route("**/api/analytics", (route) =>
//
// v1.4.39.3 — match `/api/analytics` AND any sliced variant
// (`?slice=summaries`). The v1.4.39.2 dashboard split mounts the
// slim slice in parallel; the literal `**/api/analytics` glob
// misses the query-string form and dropped the request through to
// the real route, blanking the tile strip + chart cards.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+12 -1
View File
@@ -23,7 +23,18 @@ test.describe("authenticated dashboard render", () => {
// Mock analytics with a populated weight summary so the weight tile
// and chart render. `count` must be >0 for the tile to clear the
// data-floor gate in `src/app/page.tsx`.
await page.route("**/api/analytics", (route) =>
//
// v1.4.39.3 — match `/api/analytics` AND any sliced variant
// (`?slice=summaries`). The v1.4.39.2 dashboard split mounts two
// parallel `useAnalyticsQuery` calls; the slim variant carries a
// `?slice=summaries` query string that the literal string glob
// `**/api/analytics` (Playwright minimatch) refuses to match,
// dropping the request to the real route. The seeded test user has
// no measurements so the real route returns empty summaries, and
// the dashboard's slim-first merge then blanked the tile strip.
// The same trap bit `onboarding-flicker` and `mobile-viewport` in
// v1.4.37 W-CI; the regex form is the durable fix.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+8 -1
View File
@@ -74,7 +74,14 @@ test.describe("add measurement flow", () => {
// Dashboard's analytics fetch — empty summaries are fine; we only
// need the page to render so we can click "Add".
await page.route("**/api/analytics", (route) =>
//
// v1.4.39.3 — match `/api/analytics` AND any sliced variant
// (`?slice=summaries`). The v1.4.39.2 dashboard split fires both
// requests in parallel; the previous string glob missed the slim
// variant which then fell through to the real route and slowed
// the page paint enough that the post-mutation list query never
// surfaced inside the 10 s window.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+3 -1
View File
@@ -69,7 +69,9 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
}),
);
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — regex form matches the slim slice the v1.4.39.2
// dashboard split fires alongside the thick request.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+3 -1
View File
@@ -31,7 +31,9 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => {
// Populated analytics so each sub-page paints the data branch
// (`hasMetricData` true) — the Coach button is mounted at the
// bottom of the data tree.
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — regex form matches the slim slice the v1.4.39.2
// dashboard split fires alongside the thick request.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+3 -1
View File
@@ -24,7 +24,9 @@ test.describe("v1.4.27 — insights empty-state with metric gating", () => {
test.beforeEach(async ({ page }) => {
// Zero-count summaries — every gated sub-page short-circuits to
// its EmptyState branch.
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — regex form matches the slim slice the v1.4.39.2
// dashboard split fires alongside the thick request.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+3 -1
View File
@@ -25,7 +25,9 @@ test.describe("v1.4.27 — /measurements?add= consumer", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test.beforeEach(async ({ page }) => {
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — regex form matches the slim slice the v1.4.39.2
// dashboard split fires alongside the thick request.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
+3 -1
View File
@@ -22,7 +22,9 @@ test.describe("v1.4.27 — ResponsiveSheet branch", () => {
test.beforeEach(async ({ page }) => {
// Empty analytics + list keeps the page fast and deterministic.
await page.route("**/api/analytics", (route) =>
// v1.4.39.3 — regex form matches `/api/analytics` AND any sliced
// variant (`?slice=summaries`) the v1.4.39.2 dashboard split fires.
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",