Revert "test(e2e): walk the Topics preview and sweep its pages at 390 px and with axe"

This reverts commit 957c2f279e.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 10:46:43 +02:00
1 parent 9de64cae3e
commit ce0c2a3f3f
6 files changed
+5 -471

No files matched your search

+1 -47
View File
@@ -2,10 +2,7 @@ import AxeBuilder from "@axe-core/playwright";
import type { Locator, Page, Route } from "@playwright/test";
import { expect, test } from "./setup/test";
import {
MOBILE_ROUTES_STORAGE_STATE_PATH,
STORAGE_STATE_PATH,
} from "./setup/global-setup";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
import { openMenu } from "./open-menu";
import { POPULATED_SUMMARIES } from "./utils/mock-dashboard-snapshot";
@@ -982,49 +979,6 @@ const ADMIN_AND_BASELINE_ROUTES: readonly RouteCase[] = [
},
];
/**
* The topic pages (preview), on the phone-sweep account: its preview is on
* and never switched, and its record holds a little of every kind, so the
* overview draws tiles and the pages draw both their data and their
* below-threshold states. Gated on the grid and the page body, which live in
* other components than the `<h1>` the scan checks.
*/
const TOPIC_ROUTES: readonly RouteCase[] = [
{
name: "/topics overview",
path: "/topics",
painted: (page) => page.locator('[data-slot="topics-grid"]:visible'),
},
{
name: "/topics/heart",
path: "/topics/heart",
painted: (page) => page.locator('[data-slot="topic-detail-body"]:visible'),
},
{
name: "/topics/metabolism",
path: "/topics/metabolism",
painted: (page) => page.locator('[data-slot="topic-detail-body"]:visible'),
},
];
test.describe("axe-core topic pages (preview)", () => {
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
for (const theme of THEMES) {
test(`theme=${theme} routes: /topics, /topics/heart, /topics/metabolism`, async ({
page,
}) => {
test.setTimeout(120_000);
await useExplicitTheme(page, theme);
const blocking: AxeViolation[] = [];
for (const routeCase of TOPIC_ROUTES) {
blocking.push(...(await visitAndScan(page, theme, routeCase)));
}
expectNoViolations(theme, "topic route matrix", blocking);
});
}
});
test.describe("axe-core public surfaces", () => {
for (const theme of THEMES) {
test(`theme=${theme} /auth/login`, async ({ page }) => {
-4
View File
@@ -6,7 +6,6 @@ import type { APIRequestContext } from "@playwright/test";
import { ADMIN_SECTION_SLUGS } from "@/components/admin/section-slugs";
import { MODULE_SETTINGS_PAGE_IDS } from "@/components/settings/module-settings-page-ids";
import { SETTINGS_SECTION_SLUGS } from "@/lib/settings/section-slugs";
import { TOPIC_KEYS } from "@/lib/topics/registry";
import { expect, test } from "./setup/test";
import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
@@ -173,9 +172,6 @@ const DYNAMIC_ROUTES: Record<string, Resolver> = {
)}`,
];
},
// Every topic (preview). The sweep account has the preview on; a topic its
// modules do not offer still renders its frame and the refusal card.
"/topics/[topic]": async () => TOPIC_KEYS.map((topic) => `/topics/${topic}`),
// An ECG strip only arrives through a device sync; no route writes one. The
// detail page is swept in its not-found state, which is still the frame,
// header and back link a phone renders.
-1
View File
@@ -58,7 +58,6 @@ const SETTINGS_ROUTES = [
"/settings/api",
"/settings/mcp",
"/settings/advanced",
"/settings/preview",
"/settings/about",
] as const;
+4 -62
View File
@@ -35,7 +35,6 @@ import pg from "pg";
import { seedDayDoorsRecord } from "./day-doors-fixture";
import { seedMobileRoutesRecord } from "./mobile-routes-fixture";
import { seedTopicsRecord } from "./topics-fixture";
// Resolve relative to the project root rather than __dirname/import.meta.url —
// Playwright's TS loader runs files as CJS without `"type":"module"`, and
@@ -290,27 +289,6 @@ export const SCORE_HISTORY_STORAGE_STATE_PATH = resolve(
"e2e/setup/storageStateScoreHistory.json",
);
/**
* The topic pages' own account (`topics-preview.spec.ts`).
*
* The journey switches the Topics preview on and off, which adds and removes
* a navigation entry for the whole account; on the shared account a parallel
* spec would see its More sheet change underneath it. The preview starts off,
* the way every account starts, and the record is written after the login
* capture (`topics-fixture.ts`).
*/
export const E2E_TOPICS = {
email: "e2e-topics@healthlog.test",
username: "e2e-topics",
password: "Tp8!Hq4nVw7zKc3M",
role: "USER",
} as const;
export const TOPICS_STORAGE_STATE_PATH = resolve(
process.cwd(),
"e2e/setup/storageStateTopics.json",
);
export const AI_OPTIONAL_STORAGE_STATE_PATH = resolve(
process.cwd(),
"e2e/setup/storageStateAiOptional.json",
@@ -930,17 +908,15 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
// The phone-width route sweep's account. Every opt-in module is on, so
// the sweep reaches the glucose, documents, wellbeing, nutrients and
// environment surfaces, and the Topics preview is on, so the topic pages
// render rather than leave for Insights; its record is written after the
// login capture.
// environment surfaces; its record is written after the login capture.
await pool.query(
`INSERT INTO users
(id, username, email, password_hash, role, created_at, updated_at,
height_cm, date_of_birth, gender,
onboarding_completed_at, onboarding_tour_completed,
module_preferences_json, preview_features_json)
module_preferences_json)
VALUES ($1, $2, $3, $4, $5, $6, $6, 168, $7, 'FEMALE', $6, true,
$8::jsonb, '{"topics": true}'::jsonb)
$8::jsonb)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
@@ -948,8 +924,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
updated_at = EXCLUDED.updated_at,
onboarding_completed_at = EXCLUDED.onboarding_completed_at,
onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
module_preferences_json = EXCLUDED.module_preferences_json,
preview_features_json = EXCLUDED.preview_features_json`,
module_preferences_json = EXCLUDED.module_preferences_json`,
[
cuid(),
E2E_MOBILE_ROUTES.username,
@@ -1015,31 +990,6 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
],
);
// The topic pages' account. Default modules, the preview off; its record
// is written after the login capture.
await pool.query(
`INSERT INTO users
(id, username, email, password_hash, role, created_at, updated_at,
onboarding_completed_at, onboarding_tour_completed,
module_preferences_json, preview_features_json)
VALUES ($1, $2, $3, $4, 'USER', $5, $5, $5, true, NULL, NULL)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
updated_at = EXCLUDED.updated_at,
onboarding_completed_at = EXCLUDED.onboarding_completed_at,
onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
module_preferences_json = NULL,
preview_features_json = NULL`,
[
cuid(),
E2E_TOPICS.username,
E2E_TOPICS.email,
await hashPassword(E2E_TOPICS.password),
now,
],
);
// The day-doors journey's account. Every module the journey reaches is
// on; its record is written after the login capture.
await pool.query(
@@ -1700,14 +1650,6 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
await capture(E2E_DAY_DOORS, DAY_DOORS_STORAGE_STATE_PATH);
await seedDayDoorsRecord(baseURL, DAY_DOORS_STORAGE_STATE_PATH);
// The topic pages' jar, then its record — see `E2E_TOPICS`.
await capture(E2E_TOPICS, TOPICS_STORAGE_STATE_PATH);
await seedTopicsRecord(
baseURL,
TOPICS_STORAGE_STATE_PATH,
E2E_TOPICS.username,
);
// The score-history journey's jar — see `E2E_SCORE_HISTORY`.
await capture(E2E_SCORE_HISTORY, SCORE_HISTORY_STORAGE_STATE_PATH);
-138
View File
@@ -1,138 +0,0 @@
/**
* The record `topics-preview.spec.ts` reads its topics from.
*
* Enough of each kind that every topic the account is offered has something
* to show, and some of it deliberately short of the thresholds, so the page
* states what is missing rather than a figure:
*
* - 40 days of resting heart rate, heart-rate variability and sleep, with
* every fourth night under seven hours and the morning after it higher:
* the heart pairs reach 14 paired days and say something;
* - 30 days of weight, 25 of blood pressure;
* - 12 days of steps: the steps pairs stay below 14 and say so;
* - 10 mood entries: the mind pairs stay below 14 and say so;
* - LDL three times over two years (a trend line) and vitamin D once
* (below its range, no trend yet).
*
* The readings go straight into Postgres: the topics read the live table, and
* a few hundred route calls would only slow `globalSetup` down. Mood entries
* and lab results go through the app's routes, which own their encryption and
* the marker catalogue. An account that already holds the LDL marker is taken
* as seeded, so a local re-run against a reused database does not double it.
*/
import { randomUUID } from "node:crypto";
import { request as playwrightRequest } from "@playwright/test";
import pg from "pg";
export const TOPICS_LAB_MARKER = "LDL";
const DAY_MS = 86_400_000;
type Row = [type: string, unit: string, value: number, measuredAt: Date];
/** The readings, one per local morning, ending yesterday. */
function readings(now: number): Row[] {
const at = (daysAgo: number, hourUtc: number) => {
const d = new Date(now - daysAgo * DAY_MS);
d.setUTCHours(hourUtc, 30, 0, 0);
return d;
};
const rows: Row[] = [];
for (let i = 1; i <= 40; i++) {
const shortNight = i % 4 === 0;
rows.push(["SLEEP_DURATION", "min", shortNight ? 360 : 480, at(i, 5)]);
rows.push([
"RESTING_HEART_RATE",
"bpm",
55 + (i % 10) + (shortNight ? 5 : 0),
at(i, 6),
]);
rows.push(["HEART_RATE_VARIABILITY", "ms", 80 - (i % 10) * 3, at(i, 6)]);
}
for (let i = 1; i <= 30; i++) {
rows.push(["WEIGHT", "kg", 82 - (30 - i) * 0.03, at(i, 6)]);
}
for (let i = 1; i <= 25; i++) {
rows.push(["BLOOD_PRESSURE_SYS", "mmHg", 118 + (i % 5), at(i, 7)]);
rows.push(["BLOOD_PRESSURE_DIA", "mmHg", 76 + (i % 3), at(i, 7)]);
rows.push(["PULSE", "bpm", 64 + (i % 4), at(i, 7)]);
}
for (let i = 1; i <= 12; i++) {
rows.push(["ACTIVITY_STEPS", "steps", 7000 + (i % 5) * 900, at(i, 18)]);
}
return rows;
}
export async function seedTopicsRecord(
baseURL: string,
storageStatePath: string,
username: string,
): Promise<void> {
const ctx = await playwrightRequest.newContext({
baseURL,
storageState: storageStatePath,
});
const url = process.env.DATABASE_URL;
if (!url) throw new Error("[topics-fixture] DATABASE_URL is not set");
const pool = new pg.Pool({ connectionString: url });
try {
const labs = await ctx.get("/api/labs");
const body = (await labs.json()) as { data: unknown };
if (JSON.stringify(body.data ?? null).includes(`"${TOPICS_LAB_MARKER}"`)) {
return;
}
const user = await pool.query<{ id: string }>(
"SELECT id FROM users WHERE username = $1",
[username],
);
const userId = user.rows[0]?.id;
if (!userId) throw new Error("[topics-fixture] topics account not seeded");
const now = Date.now();
for (const [type, unit, value, measuredAt] of readings(now)) {
await pool.query(
`INSERT INTO measurements
(id, user_id, type, value, unit, source, measured_at, updated_at)
VALUES ($1, $2, $3::"measurement_type", $4, $5, 'MANUAL', $6, now())`,
[randomUUID(), userId, type, value, unit, measuredAt],
);
}
const post = async (path: string, data: unknown): Promise<void> => {
const res = await ctx.post(path, { data });
if (res.status() === 409 || res.ok()) return;
throw new Error(
`[topics-fixture] ${path} answered ${res.status()}: ${(await res.text()).slice(0, 200)}`,
);
};
for (let i = 1; i <= 10; i++) {
const at = new Date(now - i * DAY_MS);
at.setUTCHours(19, 0, 0, 0);
await post("/api/mood-entries", {
mood: i % 3 === 0 ? "OKAY" : "GUT",
moodLoggedAt: at.toISOString(),
});
}
for (const [analyte, unit, value, daysAgo, low, high] of [
[TOPICS_LAB_MARKER, "mg/dL", 140, 760, 0, 130],
[TOPICS_LAB_MARKER, "mg/dL", 121, 390, 0, 130],
[TOPICS_LAB_MARKER, "mg/dL", 98, 20, 0, 130],
["Vitamin D", "ng/mL", 24, 25, 30, 100],
] as const) {
await post("/api/labs", {
analyte,
panel: "Topics",
value,
unit,
referenceLow: low,
referenceHigh: high,
takenAt: new Date(now - daysAgo * DAY_MS).toISOString(),
});
}
} finally {
await pool.end();
await ctx.dispose();
}
}
-219
View File
@@ -1,219 +0,0 @@
import type { Page } from "@playwright/test";
import { expect, test } from "./setup/test";
import { TOPICS_STORAGE_STATE_PATH } from "./setup/global-setup";
import { expectNoHorizontalOverflow } from "./utils/horizontal-overflow";
/**
* The Topics preview end to end, on its own account (`E2E_TOPICS`, seeded by
* `topics-fixture.ts`):
*
* - switched off (every account's start): no More tile, no sidebar entry,
* the pages leave for Insights and the routes answer 404;
* - switched on in Settings > System > Preview features: the More sheet
* leads with a Topics tile carrying the Beta badge, the desktop sidebar
* has the entry directly below Insights;
* - every offered topic renders from the seeded record, with the relationship
* that has fourteen paired days stated and the ones short of it saying how
* far they are, the lab trend drawn for the marker with three readings over
* a year and named as missing for the one with a single reading.
*
* Phone project only, in one serial run: the switch is account-wide, so two
* projects toggling it at once would read each other's state.
*/
test.describe.configure({ mode: "serial" });
async function setPreview(page: Page, topics: boolean) {
const res = await page.request.patch("/api/settings/preview-features", {
data: { topics },
});
expect(res.ok(), `preview PATCH answered ${res.status()}`).toBe(true);
}
async function openMoreSheet(page: Page) {
await page.locator('[data-testid="bottom-nav-more"]').click();
const sheet = page.locator('[data-testid="bottom-nav-more-sheet"]');
await expect(sheet).toBeVisible();
return sheet;
}
test.describe("Topics (preview)", () => {
test.use({
storageState: TOPICS_STORAGE_STATE_PATH,
viewport: { width: 390, height: 844 },
});
test.beforeEach(async ({}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile", "phone journey");
});
test.afterAll(async ({ browser }, testInfo) => {
if (testInfo.project.name !== "chromium-mobile") return;
const context = await browser.newContext({
storageState: TOPICS_STORAGE_STATE_PATH,
baseURL: testInfo.project.use.baseURL,
});
// Leave the account as every account starts: the preview off.
await context.request.patch("/api/settings/preview-features", {
data: { topics: false },
});
await context.close();
});
test("switched off: no tile, no entry, the pages leave and the routes refuse", async ({
page,
}) => {
await setPreview(page, false);
await page.goto("/");
const sheet = await openMoreSheet(page);
await expect(sheet.locator('a[href="/topics"]')).toHaveCount(0);
await page.keyboard.press("Escape");
await page.goto("/topics");
await page.waitForURL(/\/insights$/);
await page.goto("/topics/heart");
await page.waitForURL(/\/insights$/);
const api = await page.request.get("/api/topics");
expect(api.status()).toBe(404);
expect(
((await api.json()) as { meta?: { errorCode?: string } }).meta,
).toMatchObject({ errorCode: "topics.preview_off" });
});
test("switched on in Settings: the More sheet leads with the Topics tile", async ({
page,
}) => {
await setPreview(page, false);
await page.goto("/settings/preview");
const toggle = page.locator('[data-slot="settings-preview-topics-switch"]');
await expect(toggle).toHaveAttribute("aria-checked", "false");
await toggle.click();
await expect(toggle).toHaveAttribute("aria-checked", "true");
await expect(
page.locator('[data-slot="settings-preview-topics-open"]'),
).toBeVisible();
const sheet = await openMoreSheet(page);
const tiles = sheet.locator("a");
await expect(tiles.first()).toHaveAttribute("href", "/topics");
await expect(
tiles.first().locator('[data-slot="preview-badge"]'),
).toBeVisible();
await tiles.first().click();
await page.waitForURL(/\/topics$/);
await expect(page.locator('[data-slot="topics-grid"]')).toBeVisible();
});
test("every offered topic renders from the record, below-threshold states included", async ({
page,
}) => {
await setPreview(page, true);
await page.goto("/topics");
const grid = page.locator('[data-slot="topics-grid"]');
await expect(grid).toBeVisible();
for (const key of [
"heart",
"body",
"blood-pressure",
"sleep",
"activity",
"metabolism",
"mind",
]) {
await expect(
grid.locator(`[data-slot="topic-tile"][data-topic="${key}"]`),
).toBeVisible();
}
// No cycle module for this account: named once, never a tile.
await expect(
grid.locator('[data-slot="topic-tile"][data-topic="cycle"]'),
).toHaveCount(0);
await expect(
page.locator('[data-slot="topics-hidden-list"]'),
).toBeVisible();
await expectNoHorizontalOverflow(page, "/topics");
// The heart: the pair with 40 paired days speaks, the steps pair cannot.
await grid
.locator('[data-topic="heart"] [data-slot="topic-tile-link"]')
.click();
await page.waitForURL(/\/topics\/heart$/);
const body = page.locator('[data-slot="topic-detail-body"]');
await expect(body).toBeVisible();
await expect(
body.locator(
'[data-slot="topic-relationship"][data-relationship="hrv-resting-hr"]',
),
).toHaveAttribute("data-status", "linked");
await expect(
body.locator(
'[data-slot="topic-signal"][data-signal="RESTING_HEART_RATE"]',
),
).toBeVisible();
await expect(
body.locator('[data-slot="topic-chart-row"]').first(),
).toBeVisible();
await expect(body.locator('[data-slot="topic-missing"]')).toBeVisible();
await expectNoHorizontalOverflow(page, "/topics/heart");
// The range switch re-reads the window.
await page
.locator('[data-slot="topic-range"]')
.getByRole("radio", { name: /90/ })
.click();
await expect(body).toBeVisible();
await page.goto("/topics/activity");
await expect(
page.locator(
'[data-slot="topic-relationship"][data-relationship="steps-sleep"]',
),
).toHaveAttribute("data-status", "insufficient");
await page.goto("/topics/mind");
await expect(
page.locator(
'[data-slot="topic-relationship"][data-relationship="short-nights-mood"]',
),
).toHaveAttribute("data-status", "insufficient");
await page.goto("/topics/metabolism");
const labs = page.locator('[data-slot="topic-labs"]');
await expect(labs).toBeVisible();
await expect(labs.locator('[data-slot="topic-lab-trend"]')).toHaveCount(1);
await expect(
labs.locator('[data-slot="topic-lab-trend-short"]'),
).toBeVisible();
await expect(
labs.locator('[data-slot="topic-lab-marker"][data-status="below"]'),
).toHaveCount(1);
await expectNoHorizontalOverflow(page, "/topics/metabolism");
for (const key of ["body", "blood-pressure", "sleep"]) {
await page.goto(`/topics/${key}`);
await expect(
page.locator('[data-slot="topic-detail-body"]'),
).toBeVisible();
await expectNoHorizontalOverflow(page, `/topics/${key}`);
}
});
test("the desktop sidebar carries the entry directly below Insights", async ({
page,
}) => {
await setPreview(page, true);
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/topics");
const nav = page.locator('[data-slot="sidebar-column"] nav');
const links = nav.locator("a");
await expect(nav.locator('a[href="/topics"]')).toBeVisible();
const hrefs = await links.evaluateAll((els) =>
els.map((el) => el.getAttribute("href")),
);
expect(hrefs[hrefs.indexOf("/insights") + 1]).toBe("/topics");
await expect(
nav.locator('a[href="/topics"] [data-slot="preview-badge"]'),
).toBeVisible();
});
});