mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +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.
181 lines
6.9 KiB
TypeScript
181 lines
6.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
|
|
/**
|
|
* Authenticated dashboard render — proves the seed user can reach the
|
|
* home dashboard and the core widgets paint without uncaught client
|
|
* errors.
|
|
*
|
|
* The dashboard self-gates each tile on (visible AND has data), so a
|
|
* fresh seed user with zero measurements would render an empty strip
|
|
* and skip all charts. Rather than seed measurements before this spec
|
|
* runs (which couples the spec to a particular signature of the
|
|
* `/api/measurements` shape), we mock `/api/analytics` and
|
|
* `/api/dashboard/widgets` to return a deterministic dataset. The
|
|
* widget layout + chart wrappers are pure enough that asserting on the
|
|
* mocked-data render exercises the same code paths as a real session.
|
|
*/
|
|
test.describe("authenticated dashboard render", () => {
|
|
test.use({ storageState: STORAGE_STATE_PATH });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// 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`.
|
|
//
|
|
// 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",
|
|
body: JSON.stringify({
|
|
data: {
|
|
summaries: {
|
|
WEIGHT: {
|
|
latest: 78.5,
|
|
avg7: 78.2,
|
|
avg30: 77.9,
|
|
slope30: { slope: -0.05, direction: "down" },
|
|
count: 30,
|
|
},
|
|
BLOOD_PRESSURE_SYS: {
|
|
latest: 124,
|
|
avg7: 122,
|
|
avg30: 121,
|
|
slope30: { slope: 0.1, direction: "flat" },
|
|
count: 25,
|
|
},
|
|
BLOOD_PRESSURE_DIA: {
|
|
latest: 80,
|
|
avg7: 79,
|
|
avg30: 78,
|
|
slope30: { slope: 0.05, direction: "flat" },
|
|
count: 25,
|
|
},
|
|
PULSE: {
|
|
latest: 68,
|
|
avg7: 70,
|
|
avg30: 71,
|
|
slope30: { slope: -0.2, direction: "down" },
|
|
count: 25,
|
|
},
|
|
},
|
|
bpInTargetPct: 78,
|
|
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,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
// The chart's per-type fetch reads `json.data.measurements` and
|
|
// expects a `meta.total`. Return a small dataset so the chart has
|
|
// points to plot — otherwise Recharts mounts a placeholder instead
|
|
// of the recharts-wrapper SVG and the assertion below times out.
|
|
await page.route("**/api/measurements*", (route) => {
|
|
const url = new URL(route.request().url());
|
|
const type = url.searchParams.get("type") ?? "WEIGHT";
|
|
const baseValue =
|
|
type === "BLOOD_PRESSURE_SYS"
|
|
? 124
|
|
: type === "BLOOD_PRESSURE_DIA"
|
|
? 80
|
|
: type === "PULSE"
|
|
? 70
|
|
: 78.5;
|
|
const measurements = Array.from({ length: 10 }, (_, i) => ({
|
|
id: `m_${type}_${i}`,
|
|
type,
|
|
value: baseValue + (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,
|
|
}),
|
|
});
|
|
});
|
|
|
|
// v1.4.28 retired the InsightAdvisorCard surface. Coach drawer
|
|
// remains the conversational entry point on /insights, but the
|
|
// bottom-of-page card with its own summary container is gone. The
|
|
// dashboard render assertion below no longer touches /insights, so
|
|
// we don't need to mock /api/insights/comprehensive or
|
|
// /api/insights/generate any more (the iOS-consumed POST endpoint
|
|
// is preserved server-side; only the web mount retired).
|
|
});
|
|
|
|
test("dashboard tile strip + chart + insight card render", async ({
|
|
page,
|
|
}) => {
|
|
const consoleErrors: string[] = [];
|
|
page.on("pageerror", (err) => {
|
|
consoleErrors.push(err.message);
|
|
});
|
|
page.on("console", (msg) => {
|
|
if (msg.type() === "error") consoleErrors.push(msg.text());
|
|
});
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
// Tile strip — `data-slot="dashboard-tile-strip"` is the stable hook
|
|
// the dashboard exposes. The component sets `data-tile-count` to the
|
|
// active tile count; assert it's >0 so we know at least one tile
|
|
// cleared the visibility + data-floor gate.
|
|
const strip = page.locator('[data-slot="dashboard-tile-strip"]');
|
|
await expect(strip).toBeVisible({ timeout: 10_000 });
|
|
const tileCount = Number(await strip.getAttribute("data-tile-count"));
|
|
expect(tileCount).toBeGreaterThan(0);
|
|
|
|
// At least one chart must render. Recharts mounts a `<svg>` with
|
|
// `class="recharts-surface"` (or a `<div class="recharts-wrapper">`
|
|
// around it). Use the recharts class so we don't accidentally match
|
|
// an aria-hidden lucide-react icon.
|
|
await expect(
|
|
page.locator(".recharts-wrapper, .recharts-surface").first(),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
|
|
// v1.4.28 — the bottom-of-page InsightAdvisorCard retired; the
|
|
// dashboard spec no longer asserts on its summary slot. The hero
|
|
// strip and the Coach drawer carry the differentiator on /insights
|
|
// and are covered by their own specs.
|
|
|
|
// Filter out a small set of known-noise messages — we care about
|
|
// real React/JS uncaught errors only. 404s from optional assets
|
|
// (manifest icons, prefetched chunks for unrendered routes) are
|
|
// explicitly tolerated; they don't affect the dashboard render.
|
|
const significant = consoleErrors.filter(
|
|
(msg) =>
|
|
!/ResizeObserver loop|Download the React DevTools|Warning: |\[Fast Refresh\]|Failed to load resource|net::ERR_/i.test(
|
|
msg,
|
|
),
|
|
);
|
|
expect(significant, significant.join("\n")).toEqual([]);
|
|
});
|
|
});
|