Files
HealthLog/e2e/modules-off.spec.ts
T
Marc-André Bombeck 8eff500e9d test(e2e): prove a switched-off module leaves every surface it owns
A journey on its own account switches mood and medications on, proves
the Mood nav entry, the Mood and Medications pills, the mood trends slot
and both add-menu entries are there, switches both off through the
module route and proves each is gone, the trends row fills from pulse,
/insights/mood shows the module notice at the same URL, and no chart on
the way paints its error state.
2026-09-24 10:06:14 +02:00

170 lines
6.1 KiB
TypeScript

/**
* A module switched off disappears everywhere, and a page of it answers with
* the notice instead of errors.
*
* One journey on its own account (`E2E_MODULES`): mood and medications are
* switched on, the surfaces they own are proven present (the positive control
* every "is gone" assertion below leans on), then both are switched off
* through the real module route and the same surfaces are proven gone:
*
* - the Mood nav entry,
* - the Mood and Medications pills in the Insights tab strip,
* - the mood slot in the Insights trends row (the row fills from the next
* metric instead),
* - Mood and Medication in the add menu,
* - `/insights/mood` renders the module notice, not the page,
*
* and no chart on the overview paints its error state.
*
* Every assertion is addressed to a stable `data-*` attribute, never to copy.
* The journey moves its account's module switches, so it runs in one project
* and serially (see `playwright.config.ts`).
*/
import type { Page } from "@playwright/test";
import { MODULES_STORAGE_STATE_PATH } from "./setup/global-setup";
import { modulesSeedState } from "./setup/modules-fixture";
import { expect, test } from "./setup/test";
test.use({ storageState: MODULES_STORAGE_STATE_PATH });
test.describe.configure({ mode: "serial" });
/** A same-origin JSON write from the page, so the session cookie rides along. */
async function send(
page: Page,
method: "POST" | "PATCH",
path: string,
body: unknown,
): Promise<number> {
return page.evaluate(
async ({ method, path, body }) => {
const res = await fetch(path, {
method,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
return res.status;
},
{ method, path, body },
);
}
async function setModules(page: Page, on: boolean): Promise<void> {
expect(
await send(page, "PATCH", "/api/auth/me/modules", {
mood: on,
medications: on,
}),
).toBe(200);
}
/** One mood entry and one medication, created once, through the app. */
async function seed(page: Page): Promise<void> {
const state = await modulesSeedState();
if (state.moodEntries === 0) {
expect(
await send(page, "POST", "/api/mood-entries", {
mood: "GUT",
moodLoggedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
}),
).toBe(201);
}
if (state.medications === 0) {
expect(
await send(page, "POST", "/api/medications", {
name: "Module journey",
dose: "5 mg",
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
}),
).toBe(201);
}
}
const moodPill = '[data-slot="insights-tab-strip-pill"][href="/insights/mood"]';
const medsPill =
'[data-slot="insights-tab-strip-pill"][href="/insights/medications"]';
const trendCard = (metric: string) =>
`[data-slot="trends-row-card"][data-metric="${metric}"]`;
async function openCapturePicker(page: Page): Promise<void> {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
const capture = page.locator('[data-testid="bottom-nav-capture"]');
await expect(capture).toBeVisible({ timeout: 15_000 });
await capture.click();
await expect(
page.locator('[data-testid="capture-picker-measurement"]'),
).toBeVisible();
}
test("mood and medications, switched off, leave every surface they own", async ({
page,
}) => {
test.setTimeout(120_000);
await page.goto("/");
await setModules(page, true);
await seed(page);
// ── Positive control: with both modules on, each surface is there. ──
await page.goto("/insights");
await expect(page.locator(moodPill)).toBeVisible({ timeout: 20_000 });
await expect(page.locator(medsPill)).toBeVisible();
await expect(page.locator(trendCard("mood"))).toHaveCount(1);
await expect(page.locator('[data-tour-id="nav-mood"]')).toHaveCount(1);
await openCapturePicker(page);
await expect(page.locator('[data-testid="capture-picker-mood"]')).toHaveCount(
1,
);
await expect(
page.locator('[data-testid="capture-picker-medication"]'),
).toHaveCount(1);
await page.setViewportSize({ width: 1280, height: 720 });
// ── Switch both off through the real route. ──
await setModules(page, false);
// Insights overview: no Mood or Medications pill, no mood trend slot, the
// row still fills three slots from the next metric, and nothing errors.
await page.goto("/insights");
await expect(
page.locator('[data-slot="insights-tab-strip-pill"][href="/insights"]'),
).toBeVisible({ timeout: 20_000 });
await expect(page.locator('[data-slot="trends-row"]')).toBeVisible();
await expect(page.locator(moodPill)).toHaveCount(0);
await expect(page.locator(medsPill)).toHaveCount(0);
await expect(page.locator(trendCard("mood"))).toHaveCount(0);
await expect(page.locator(trendCard("pulse"))).toHaveCount(1);
await expect(page.locator('[data-slot="chart-error-state"]')).toHaveCount(0);
// The nav entry is gone; Insights itself is not a module's and stays.
await expect(page.locator('[data-tour-id="nav-mood"]')).toHaveCount(0);
await expect(page.locator('[data-tour-id="nav-insights"]')).toHaveCount(1);
// A direct visit: the notice, named for mood and for the record's own
// switch (so it offers Settings), at the same URL, never the page.
await page.goto("/insights/mood");
await expect(page).toHaveURL(/\/insights\/mood$/);
const notice = page.locator(
'[data-slot="module-disabled-notice"][data-module="mood"]',
);
await expect(notice).toBeVisible({ timeout: 15_000 });
await expect(notice).toHaveAttribute("data-module-access", "disabled");
await expect(
page.locator('[data-slot="module-off-open-settings"]'),
).toBeVisible();
await expect(page.locator('[data-slot="chart-error-state"]')).toHaveCount(0);
// The add menu offers a measurement and nothing the modules owned.
await openCapturePicker(page);
await expect(page.locator('[data-testid="capture-picker-mood"]')).toHaveCount(
0,
);
await expect(
page.locator('[data-testid="capture-picker-medication"]'),
).toHaveCount(0);
// Leave the account as the next run expects to find it.
await setModules(page, true);
});