mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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:
1 parent
9de64cae3e
commit
ce0c2a3f3f
6 files changed
+5
-471
No files matched your search
+1
-47
@@ -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 }) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -58,7 +58,6 @@ const SETTINGS_ROUTES = [
|
||||
"/settings/api",
|
||||
"/settings/mcp",
|
||||
"/settings/advanced",
|
||||
"/settings/preview",
|
||||
"/settings/about",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user