mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
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.
146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
|
||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||
|
||
/**
|
||
* v1.4.18 — per-chart overlay-controls popover.
|
||
*
|
||
* Marc reverted v1.4.16's always-on chart overlays (gradient fill,
|
||
* personal-baseline line, target-zone shading) and asked for per-chart
|
||
* switches that the user can flip on or off. This spec exercises the
|
||
* end-to-end behaviour against the seeded test user:
|
||
*
|
||
* 1. Dashboard renders with a chart.
|
||
* 2. The chart card has a settings cog in its header.
|
||
* 3. Clicking the cog opens a popover with three switches.
|
||
* 4. Toggling "Target range" on triggers a PUT /api/dashboard/chart-overlay-prefs.
|
||
*/
|
||
test.describe("chart overlay controls", () => {
|
||
test.use({ storageState: STORAGE_STATE_PATH });
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
// Mock analytics + measurements so the dashboard paints at least
|
||
// one chart card. Same seed shape the dashboard.spec.ts uses.
|
||
//
|
||
// 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",
|
||
body: JSON.stringify({
|
||
data: {
|
||
summaries: {
|
||
WEIGHT: {
|
||
latest: 78.5,
|
||
avg7: 78.2,
|
||
avg30: 77.9,
|
||
slope30: { slope: -0.05, direction: "down" },
|
||
count: 30,
|
||
},
|
||
},
|
||
bpInTargetPct: 0,
|
||
glucoseByContext: {},
|
||
},
|
||
error: null,
|
||
}),
|
||
}),
|
||
);
|
||
await page.route("**/api/mood/analytics", (route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: { entries: [], summary: { count: 0 } },
|
||
error: null,
|
||
}),
|
||
}),
|
||
);
|
||
await page.route("**/api/measurements*", (route) => {
|
||
const measurements = Array.from({ length: 10 }, (_, i) => ({
|
||
id: `m_${i}`,
|
||
type: "WEIGHT",
|
||
value: 78 + (i % 3) - 1,
|
||
measuredAt: new Date(Date.now() - i * 86_400_000).toISOString(),
|
||
notes: null,
|
||
}));
|
||
return route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: { measurements, meta: { total: measurements.length } },
|
||
error: null,
|
||
}),
|
||
});
|
||
});
|
||
});
|
||
|
||
test("opens the overlay-controls popover and saves a toggle change", async ({
|
||
page,
|
||
}) => {
|
||
let putRequestCount = 0;
|
||
await page.route("**/api/dashboard/chart-overlay-prefs", async (route) => {
|
||
if (route.request().method() === "PUT") {
|
||
putRequestCount += 1;
|
||
}
|
||
return route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ data: { saved: true }, error: null }),
|
||
});
|
||
});
|
||
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
// Wait for the dashboard tile-strip to settle so we know the user
|
||
// is past initial auth + layout-fetch.
|
||
await expect(
|
||
page.locator('[data-slot="dashboard-tile-strip"]'),
|
||
).toBeVisible({ timeout: 10_000 });
|
||
|
||
// The weight chart is one of the always-visible default charts and
|
||
// it ships an overlay-controls trigger because the dashboard plumbs
|
||
// a chartKey into it.
|
||
const trigger = page
|
||
.locator('[data-slot="chart-overlay-controls-trigger"]')
|
||
.first();
|
||
await expect(trigger).toBeVisible({ timeout: 10_000 });
|
||
|
||
// Scroll the trigger into view before opening the dropdown so the
|
||
// Radix Popper places its portalled content inside the viewport.
|
||
// On the seeded test user the dashboard renders multiple stacked
|
||
// chart cards, and the first cog can sit near the bottom edge of
|
||
// the desktop 1280×720 viewport — opening the popover then drops
|
||
// the toggle below the fold and Playwright refuses to click an
|
||
// off-viewport element after auto-scrolling.
|
||
await trigger.scrollIntoViewIfNeeded();
|
||
await trigger.click();
|
||
|
||
// The popover content paints three switches.
|
||
const targetRangeToggle = page.locator(
|
||
'[data-slot="chart-overlay-toggle-target-range"]',
|
||
);
|
||
await expect(targetRangeToggle).toBeVisible({ timeout: 5_000 });
|
||
|
||
// Toggle target-range on. Radix Switch is a button[role=switch].
|
||
// The Radix dropdown content portals to the body root and can land
|
||
// below the viewport on a 393×851 Pixel 5 (or near the bottom edge
|
||
// on the 1280×720 desktop profile). Playwright's `click({force:true})`
|
||
// still computes the element's bbox against the viewport and refuses
|
||
// to dispatch the click when it falls outside, so neither
|
||
// `scrollIntoViewIfNeeded()` on the portalled content nor `force`
|
||
// can rescue the action. `dispatchEvent('click')` synthesises a DOM
|
||
// click on the element without any viewport / actionability check,
|
||
// which is exactly the semantic we want for an off-screen portalled
|
||
// Radix Switch — and it still fires the onCheckedChange handler that
|
||
// the surface listens for.
|
||
await targetRangeToggle.dispatchEvent("click");
|
||
|
||
// The PUT fires once for the toggle change.
|
||
await expect.poll(() => putRequestCount).toBeGreaterThan(0);
|
||
});
|
||
});
|