test(e2e): cover the admin carrier chip + CSV column and the GLP-1 tile tab/range strip

Mock /api/admin/audit-log so a known DACH carrier row paints the
carrier chip and the CSV export carries the `carrier` column header.
Mock /api/dashboard/glp1 so the secondary tile mounts; assert the
tab strip + range strip render, range toggles flip the active state,
and switching to the Weight tab keeps the chart pane visible.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 22:11:43 +02:00
1 parent 07f6bfa5b7
commit b2361e9ddc
2 files changed
+320

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import { expect, test } from "@playwright/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* v1.4.27 B3 — admin login-overview carrier chip + CSV column.
*
* Two contracts under test:
*
* 1. The carrier chip slot (`data-slot="login-overview-carrier"`)
* renders beneath the auth-provider chip when the audit row
* carries a GeoLite2-ASN organisation string. The component
* branches on `entry.carrier`; null carriers (private/loopback
* IPs, offline-miss rows) keep the original single-chip layout.
*
* 2. The CSV-export button emits a file whose first row carries
* the translated `carrier` column header. The header sits
* between `location` and `provider` (see the v1.4.27 B3 column-
* order comment in `_shared.tsx`).
*
* The seed user has the ADMIN role (`e2e/setup/global-setup.ts` →
* `E2E_USER.role = "ADMIN"`), so the admin route is reachable
* directly.
*/
test.describe("v1.4.27 — admin login-overview", () => {
test.use({ storageState: STORAGE_STATE_PATH });
const FIXED_ROW = {
id: "audit_e2e_1",
action: "auth.login.success",
ipAddress: "85.214.0.1",
location: "Berlin, DE",
asn: 3320,
// The carrier short-label collapses Deutsche Telekom → "Telekom"
// (see `carrierShortLabel` in `_shared.tsx`). The chip renders
// the SHORT label; the CSV emits the raw value.
carrier: "Deutsche Telekom AG",
details: null,
createdAt: new Date("2026-05-15T10:00:00Z").toISOString(),
user: { id: "u_e2e", username: "e2e-tester" },
};
test.beforeEach(async ({ page }) => {
await page.route("**/api/admin/audit-log?*", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
entries: [FIXED_ROW],
meta: { total: 1, limit: 50, offset: 0, page: 1, perPage: 50 },
},
error: null,
}),
}),
);
await page.route("**/api/admin/audit-log/actions", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { actions: ["auth.login.success", "auth.login.failed"] },
error: null,
}),
}),
);
});
test("the carrier chip renders beneath the provider chip", async ({
page,
}) => {
await page.goto("/admin/login-overview", { waitUntil: "domcontentloaded" });
// The table mounts the carrier slot for rows where `entry.carrier`
// is non-null. We mocked one row that carries the Deutsche-Telekom
// organisation string.
const chip = page.locator('[data-slot="login-overview-carrier"]').first();
await expect(chip).toBeVisible({ timeout: 10_000 });
// The chip carries the short DACH label for known carriers.
await expect(chip).toHaveText(/Telekom/);
});
test("the CSV export emits a file with the carrier column header", async ({
page,
}) => {
await page.goto("/admin/login-overview", { waitUntil: "domcontentloaded" });
// Wait for the table to render so the export button is enabled.
await expect(
page.locator('[data-slot="login-overview-carrier"]').first(),
).toBeVisible({ timeout: 10_000 });
// The CSV is generated client-side via a Blob + anchor click —
// intercept the download event Playwright surfaces for that path.
const exportButton = page.getByRole("button", { name: /export|csv/i });
const downloadPromise = page.waitForEvent("download", { timeout: 10_000 });
await exportButton.click();
const download = await downloadPromise;
const stream = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
const csv = Buffer.concat(chunks).toString("utf-8");
const headerLine = csv.split(/\r?\n/, 1)[0] ?? "";
// The CSV header is locale-aware (admin.carrier). Test users get
// the English admin locale by default — but if the key falls
// through, the raw `admin.carrier` is still emitted. Accept both.
expect(headerLine.toLowerCase()).toMatch(/carrier|admin\.carrier/);
});
});
+205
View File
@@ -0,0 +1,205 @@
import { expect, test } from "@playwright/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* v1.4.27 B1 — GLP-1 secondary tile.
*
* The tile mounts on the dashboard when `/api/dashboard/glp1` returns
* a payload with at least one active GLP-1 medication. The tile
* carries:
*
* - Drug-line caption ("Mounjaro 7.5mg") + weight-delta caption
* - Schedule pills (last + next injection)
* - Chart pane with a two-tab segmented control (Drug-Level
* default / Weight) and a 7d / 30d / 90d / All range strip
*
* The tab + range state lives on the tile so a parent re-render
* doesn't reset the user's pick. The spec exercises:
*
* 1. Tile mounts with the documented data-slot.
* 2. The Drug-Level tab is active on first paint.
* 3. Each range button toggles the `data-active` attribute to
* reflect the current selection.
* 4. Switching to the Weight tab swaps the chart pane.
*/
test.describe("v1.4.27 — GLP-1 secondary tile", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test.beforeEach(async ({ page }) => {
// Empty dashboard payload so the page renders fast — only the
// GLP-1 tile actually exercises behaviour in this spec.
await page.route("**/api/analytics", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { summaries: {}, bpInTargetPct: null, 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) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { measurements: [], meta: { total: 0 } },
error: null,
}),
}),
);
// The load-bearing fixture. Shape mirrors `Glp1Payload` /
// `Glp1MedicationPayload` in `src/components/dashboard/glp1-tile.tsx`.
await page.route("**/api/dashboard/glp1", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
active: true,
medications: [
{
name: "Mounjaro",
genericName: "tirzepatide",
medicationId: "med_glp1_e2e_1",
currentDose: {
value: 7.5,
unit: "mg",
since: "2026-04-15",
weeksOnDose: 4,
},
doseHistory: [],
lastInjection: {
date: "2026-05-12",
site: "abdomen",
weeksAgo: 0,
},
nextInjection: { date: "2026-05-19", daysAway: 4 },
startWeight: 92.0,
currentWeight: 87.8,
weightDeltaKg: -4.2,
weightSeries: Array.from({ length: 30 }, (_, i) => ({
date: new Date(Date.now() - i * 86_400_000)
.toISOString()
.slice(0, 10),
weight: 88 - i * 0.05,
})),
injectionDates: [
"2026-04-21",
"2026-04-28",
"2026-05-05",
"2026-05-12",
],
},
],
},
error: null,
}),
}),
);
});
test("tile mounts with the tab strip + range strip on Pixel 5", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-mobile",
"GLP-1 tile is dashboard-only; Pixel 5 is the load-bearing viewport",
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const tile = page.locator('[data-slot="glp1-tile"]');
await expect(tile).toBeVisible({ timeout: 15_000 });
// Tab strip carries the two tabs.
await expect(
tile.locator('[data-slot="glp1-tile-tab-level"]'),
).toBeVisible();
await expect(
tile.locator('[data-slot="glp1-tile-tab-weight"]'),
).toBeVisible();
// Range strip carries four range buttons.
const rangeButtons = tile.locator(
'[data-slot="glp1-tile-range-button"]',
);
await expect(rangeButtons).toHaveCount(4);
});
test("clicking a range button updates the active state", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-mobile",
"Pixel 5 is the load-bearing viewport for this tile",
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const tile = page.locator('[data-slot="glp1-tile"]');
await expect(tile).toBeVisible({ timeout: 15_000 });
// Default range = 30 points. Pin on data-active so we don't
// need to read CSS class soup.
const default30 = tile.locator(
'[data-slot="glp1-tile-range-button"][data-points="30"]',
);
await expect(default30).toHaveAttribute("data-active", "true");
// Click the 7d range. After click the 7d button must be active
// and the 30d button must drop back to inactive.
const range7 = tile.locator(
'[data-slot="glp1-tile-range-button"][data-points="7"]',
);
await range7.click();
await expect(range7).toHaveAttribute("data-active", "true");
await expect(default30).toHaveAttribute("data-active", "false");
// Click the 90d range. Same toggle behaviour.
const range90 = tile.locator(
'[data-slot="glp1-tile-range-button"][data-points="90"]',
);
await range90.click();
await expect(range90).toHaveAttribute("data-active", "true");
await expect(range7).toHaveAttribute("data-active", "false");
});
test("switching to the Weight tab swaps the chart pane", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-mobile",
"Pixel 5 is the load-bearing viewport for this tile",
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const tile = page.locator('[data-slot="glp1-tile"]');
await expect(tile).toBeVisible({ timeout: 15_000 });
// Click the Weight tab. The level-unavailable / weight-unavailable
// fallback slots only render when the corresponding pane has no
// data; with the seeded weight series the chart should mount and
// the unavailable fallback should NOT be visible.
await tile.locator('[data-slot="glp1-tile-tab-weight"]').click();
// The drug-level chart pane is no longer the active branch.
// Either the weight chart paints OR the fallback "no data" slot
// appears — both prove the tab switch reached the chart pane.
const chartPane = tile.locator('[data-slot="glp1-tile-chart"]');
await expect(chartPane).toBeVisible();
});
});