mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
test(charts): coverage for revert + overlay-controls + persistence
Adds a Playwright spec that drives the dashboard end-to-end — opens the chart overlay-controls popover, toggles target-range on, and verifies the PUT /api/dashboard/chart-overlay-prefs round-trip fires. Adds a vitest SSR test that pins the medication-compliance chart's prefs-gated rendering: with the trend toggle off the 7-day trend chip stays hidden; flipping it on paints the chip in the header. Mocks the useChartOverlayPrefs hook so each test can dial the persisted state directly without round-tripping the layout API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
75c74f1c49
commit
d6e87c3a12
2 files changed
+217
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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.
|
||||
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 });
|
||||
|
||||
await trigger.click();
|
||||
|
||||
// The popover content paints three switches.
|
||||
await expect(
|
||||
page.locator('[data-slot="chart-overlay-toggle-target-range"]'),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Toggle target-range on. Radix Switch is a button[role=switch].
|
||||
await page
|
||||
.locator('[data-slot="chart-overlay-toggle-target-range"]')
|
||||
.click();
|
||||
|
||||
// The PUT fires once for the toggle change.
|
||||
await expect.poll(() => putRequestCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.4.18 — overlay-toggle integration on the medication chart.
|
||||
*
|
||||
* Asserts the contract that the persisted overlay prefs gate the
|
||||
* threshold / goal reference lines AND the 7-day trend chip:
|
||||
*
|
||||
* - Default state (every flag false): no goal/threshold lines, no
|
||||
* trend chip.
|
||||
* - showTargetRange=true: threshold + goal reference lines paint.
|
||||
* - showTrendIndicator=true: 7-day trend chip paints in the header.
|
||||
*/
|
||||
|
||||
const sampleData = vi.hoisted(() =>
|
||||
Array.from({ length: 14 }, (_, i) => ({
|
||||
date: `2026-05-${String(i + 1).padStart(2, "0")}`,
|
||||
scheduled: 4,
|
||||
taken: 3 + (i % 2),
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: sampleData, isLoading: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ isAuthenticated: true, user: null, isLoading: false }),
|
||||
}));
|
||||
|
||||
// Hoisted mutable prefs so each test pre-sets the values the hook
|
||||
// returns. Recharts runs client-side, so the threshold/goal lines
|
||||
// don't appear in the SSR output as actual `<line>` nodes — but the
|
||||
// trend-chip + the components that paint conditional on the prefs
|
||||
// (rendered as `<span>` and friends) DO show up in the SSR markup.
|
||||
const mockPrefs = vi.hoisted(() => ({
|
||||
current: {
|
||||
showTrendIndicator: false,
|
||||
showTrendArrow: false,
|
||||
showTargetRange: false,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-chart-overlay-prefs", () => ({
|
||||
useChartOverlayPrefs: () => ({
|
||||
prefs: mockPrefs.current,
|
||||
setPrefs: () => undefined,
|
||||
isSaving: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
async function renderChart(): Promise<string> {
|
||||
// Re-import to pick up the freshly-set mock value.
|
||||
const { MedicationComplianceChart } = await import(
|
||||
"../medication-compliance-chart"
|
||||
);
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<MedicationComplianceChart />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("MedicationComplianceChart overlay toggles", () => {
|
||||
it("does NOT paint the trend chip by default", async () => {
|
||||
mockPrefs.current = {
|
||||
showTrendIndicator: false,
|
||||
showTrendArrow: false,
|
||||
showTargetRange: false,
|
||||
};
|
||||
const html = await renderChart();
|
||||
expect(html).not.toContain("medication-trend-chip");
|
||||
});
|
||||
|
||||
it("paints the trend chip when showTrendIndicator is true", async () => {
|
||||
mockPrefs.current = {
|
||||
showTrendIndicator: true,
|
||||
showTrendArrow: false,
|
||||
showTargetRange: false,
|
||||
};
|
||||
const html = await renderChart();
|
||||
expect(html).toContain("medication-trend-chip");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user