mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
test(e2e): mock the dashboard snapshot endpoint for tile specs
The v1.7.2 default-ON snapshot flag makes the dashboard read every tile from GET /api/dashboard/snapshot; the e2e build sets no opt-out var, so the specs that only mocked the legacy cells hit the real route, got empty summaries for the zero-measurement seed user, and timed out the tile/chart assertions. Add a shared, correctly-typed mock helper and wire it into the five tile-asserting specs alongside the existing legacy mocks (kept to protect the reversible path).
This commit is contained in:
1 parent
d6ce53a318
commit
a3e1d369e6
6 files changed
+224
No files matched your search
@@ -1,6 +1,10 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import {
|
||||
mockDashboardSnapshot,
|
||||
WEIGHT_ONLY_SUMMARIES,
|
||||
} from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-controls popover.
|
||||
@@ -19,6 +23,11 @@ test.describe("chart overlay controls", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// v1.7.2 — snapshot flag default-ON; mock the snapshot cell with a
|
||||
// WEIGHT-only populated summary so the weight chart card paints.
|
||||
// Legacy mocks below stay for the reversible `=false` path.
|
||||
await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES });
|
||||
|
||||
// Mock analytics + measurements so the dashboard paints at least
|
||||
// one chart card. Same seed shape the dashboard.spec.ts uses.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { mockDashboardSnapshot } from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.19 A2 — mobile-viewport regression spec for chart cards.
|
||||
@@ -31,6 +32,11 @@ test.describe("charts — mobile-viewport regression", () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== "chromium-mobile", "mobile-only spec");
|
||||
|
||||
// v1.7.2 — snapshot flag default-ON; mock the snapshot cell with the
|
||||
// same populated four-metric set so the charts paint. Legacy mocks
|
||||
// below stay for the reversible `=false` path.
|
||||
await mockDashboardSnapshot(page);
|
||||
|
||||
// Mock analytics + measurements so the chart wrappers all clear
|
||||
// their data-floor gates. Same pattern as `dashboard.spec.ts`.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { mockDashboardSnapshot } from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* Authenticated dashboard render — proves the seed user can reach the
|
||||
@@ -20,6 +21,14 @@ test.describe("authenticated dashboard render", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// v1.7.2 — the dashboard snapshot flag is default-ON, so the page
|
||||
// reads every tile from `/api/dashboard/snapshot` and ignores the
|
||||
// legacy cells below. Mock the snapshot with the same populated
|
||||
// four-metric set so the tile strip + charts paint. The legacy
|
||||
// `/api/analytics` + `/api/mood/analytics` mocks stay for the
|
||||
// reversible `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` path.
|
||||
await mockDashboardSnapshot(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`.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import {
|
||||
mockDashboardSnapshot,
|
||||
WEIGHT_ONLY_SUMMARIES,
|
||||
} from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* Mobile-viewport smoke (Pixel 5 profile only). Asserts:
|
||||
@@ -31,6 +35,11 @@ test.describe("mobile-viewport smoke", () => {
|
||||
test("dashboard has no horizontal scroll, bottom-nav respects content, all CTAs ≥ 44 px tall", async ({
|
||||
page,
|
||||
}) => {
|
||||
// v1.7.2 — snapshot flag default-ON; mock the snapshot cell with a
|
||||
// WEIGHT-only populated summary so the weight tile paints. Legacy
|
||||
// mocks below stay for the reversible `=false` path.
|
||||
await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES });
|
||||
|
||||
// v1.4.37 W-CI — match `/api/analytics` AND any sliced variant
|
||||
// (`?slice=summaries`). The previous string glob `**/api/analytics`
|
||||
// didn't match the query-string form so the IW1 slim-slice fetch
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import {
|
||||
mockDashboardSnapshot,
|
||||
WEIGHT_ONLY_SUMMARIES,
|
||||
} from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-A3 fix #3 — onboarding card flicker on dashboard load.
|
||||
@@ -102,6 +106,17 @@ test.describe("dashboard onboarding card flicker guard", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
// v1.7.2 — snapshot flag default-ON. The dashboard tile strip reads
|
||||
// the snapshot cell; mock it with the populated WEIGHT summary
|
||||
// (count 30 >= the 5-reading setup gate) and the same ~250 ms delay
|
||||
// so the page's snapshot-driven tiles resolve on the same race
|
||||
// window as the slowed analytics query the checklist reads. (The
|
||||
// `GettingStartedChecklist` itself reads `/api/analytics?slice=...`
|
||||
// unconditionally, so its flicker gate is unchanged.)
|
||||
await mockDashboardSnapshot(page, {
|
||||
summaries: WEIGHT_ONLY_SUMMARIES,
|
||||
delayMs: 250,
|
||||
});
|
||||
// Same `/api/dashboard/widgets` stub as the incomplete-onboarding
|
||||
// test below — keeps the dashboard from error-boundary-bailing
|
||||
// when this query 500s in CI without a real session.
|
||||
@@ -192,6 +207,10 @@ test.describe("dashboard onboarding card flicker guard", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
// v1.7.2 — snapshot flag default-ON. Empty snapshot summaries keep
|
||||
// the tile strip blank (the user is still in setup) so the page
|
||||
// renders the onboarding card path deterministically.
|
||||
await mockDashboardSnapshot(page, { summaries: {} });
|
||||
await page.route("**/api/mood/analytics", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import {
|
||||
DASHBOARD_WIDGET_CATALOGUE_IDS,
|
||||
DEFAULT_DASHBOARD_LAYOUT,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
import type {
|
||||
DashboardSnapshot,
|
||||
DashboardLayoutCatalogueEntry,
|
||||
} from "@/lib/dashboard/snapshot";
|
||||
|
||||
/**
|
||||
* v1.7.2 — shared mock for `GET /api/dashboard/snapshot`.
|
||||
*
|
||||
* The dashboard snapshot flag flipped default-ON in v1.7.2, so
|
||||
* `src/app/page.tsx` reads every above-the-fold tile from the single
|
||||
* `/api/dashboard/snapshot` cell unless `NEXT_PUBLIC_DASHBOARD_SNAPSHOT`
|
||||
* is `"false"`. The e2e build sets no such var, so the snapshot path is
|
||||
* live in CI. Specs that previously mocked only the legacy cells
|
||||
* (`/api/analytics` + `/api/mood/analytics` + `/api/dashboard/widgets`)
|
||||
* now hit the REAL snapshot route, which returns empty `summaries` for
|
||||
* the seed user (zero measurements) and blanks the tile strip.
|
||||
*
|
||||
* `mockDashboardSnapshot` registers a `page.route` for the snapshot
|
||||
* endpoint returning a correctly-typed `DashboardSnapshot` so the
|
||||
* default-ON path paints deterministic tiles. Callers keep their legacy
|
||||
* mocks alongside it — harmless, and they protect the reversible
|
||||
* `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` path.
|
||||
*/
|
||||
|
||||
/** Fill a `DataSummary` from a sparse partial, defaulting the rest. */
|
||||
function summary(partial: Partial<DataSummary>): DataSummary {
|
||||
return {
|
||||
count: 0,
|
||||
latest: null,
|
||||
min: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
avg7: null,
|
||||
avg30: null,
|
||||
slope7: null,
|
||||
slope30: null,
|
||||
slope90: null,
|
||||
anomalyCount: 0,
|
||||
avg30LastMonth: null,
|
||||
avg30LastYear: null,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Four-metric populated tile set mirroring the legacy `/api/analytics`
|
||||
* mock shared by `dashboard.spec.ts` / `charts-mobile.spec.ts` /
|
||||
* `chart-overlay-controls.spec.ts`. Every summary has `count > 0` so the
|
||||
* tile clears the data-floor gate in `src/app/page.tsx`.
|
||||
*/
|
||||
export const POPULATED_SUMMARIES: Record<string, DataSummary> = {
|
||||
WEIGHT: summary({
|
||||
latest: 78.5,
|
||||
avg7: 78.2,
|
||||
avg30: 77.9,
|
||||
slope30: { slope: -0.05, direction: "down", confidence: 0.8 },
|
||||
count: 30,
|
||||
}),
|
||||
BLOOD_PRESSURE_SYS: summary({
|
||||
latest: 124,
|
||||
avg7: 122,
|
||||
avg30: 121,
|
||||
slope30: { slope: 0.1, direction: "stable", confidence: 0.3 },
|
||||
count: 30,
|
||||
}),
|
||||
BLOOD_PRESSURE_DIA: summary({
|
||||
latest: 80,
|
||||
avg7: 79,
|
||||
avg30: 78,
|
||||
slope30: { slope: 0.05, direction: "stable", confidence: 0.2 },
|
||||
count: 30,
|
||||
}),
|
||||
PULSE: summary({
|
||||
latest: 68,
|
||||
avg7: 70,
|
||||
avg30: 71,
|
||||
slope30: { slope: -0.2, direction: "down", confidence: 0.6 },
|
||||
count: 30,
|
||||
}),
|
||||
};
|
||||
|
||||
/** WEIGHT-only populated set for specs that only need one tile + chart. */
|
||||
export const WEIGHT_ONLY_SUMMARIES: Record<string, DataSummary> = {
|
||||
WEIGHT: POPULATED_SUMMARIES.WEIGHT,
|
||||
};
|
||||
|
||||
function buildLayoutCatalogue(): DashboardLayoutCatalogueEntry[] {
|
||||
return DASHBOARD_WIDGET_CATALOGUE_IDS.map((id, order) => ({
|
||||
id,
|
||||
visible: true,
|
||||
order,
|
||||
}));
|
||||
}
|
||||
|
||||
export interface MockSnapshotOptions {
|
||||
/**
|
||||
* Tile summaries keyed by `MeasurementType`. Defaults to the
|
||||
* four-metric populated set. Pass `{}` for an empty strip (e.g. the
|
||||
* incomplete-onboarding branch).
|
||||
*/
|
||||
summaries?: Record<string, DataSummary>;
|
||||
/** Artificial latency in ms before the route fulfils (race specs). */
|
||||
delayMs?: number;
|
||||
}
|
||||
|
||||
/** Assemble a fully-typed snapshot body from the requested tile data. */
|
||||
export function buildMockSnapshot(
|
||||
options: MockSnapshotOptions = {},
|
||||
): DashboardSnapshot {
|
||||
const summaries = options.summaries ?? POPULATED_SUMMARIES;
|
||||
const now = new Date().toISOString();
|
||||
const lastSeenByType: Record<
|
||||
string,
|
||||
{ lastSeenAt: string; daysAgo: number } | null
|
||||
> = {};
|
||||
for (const type of Object.keys(summaries)) {
|
||||
lastSeenByType[type] = { lastSeenAt: now, daysAgo: 0 };
|
||||
}
|
||||
return {
|
||||
user: {
|
||||
username: "e2e-tester",
|
||||
timezone: "Europe/Berlin",
|
||||
heightCm: 180,
|
||||
dateOfBirth: "1990-01-01T00:00:00.000Z",
|
||||
gender: "MALE",
|
||||
glucoseUnit: "mg/dL",
|
||||
onboardingTourCompleted: true,
|
||||
greetingHour: 9,
|
||||
},
|
||||
layout: DEFAULT_DASHBOARD_LAYOUT,
|
||||
layoutCatalogue: buildLayoutCatalogue(),
|
||||
metricStates: {},
|
||||
tiles: {
|
||||
summaries,
|
||||
lastSeenByType,
|
||||
mood: { summary: null, entries: [] },
|
||||
},
|
||||
extras: null,
|
||||
briefing: null,
|
||||
briefingState: "preparing",
|
||||
briefingUpdatedAt: null,
|
||||
generatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a `page.route` interceptor for `GET /api/dashboard/snapshot`
|
||||
* returning the API envelope `{ data: DashboardSnapshot, error: null }`.
|
||||
*/
|
||||
export async function mockDashboardSnapshot(
|
||||
page: Page,
|
||||
options: MockSnapshotOptions = {},
|
||||
): Promise<void> {
|
||||
const body = buildMockSnapshot(options);
|
||||
await page.route(/\/api\/dashboard\/snapshot(\?|$)/, async (route) => {
|
||||
if (options.delayMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, options.delayMs));
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: body, error: null }),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user