Files
HealthLog/e2e/insights-scroll-restoration.spec.ts
T
Marc-André Bombeck 2321ce29db fix(insights): route alert-card compliance through the cadence-aware engine + deep data cleanups
The insights alert cards computed medication compliance with the naive
`schedules.length × days` denominator, so a fully-adherent weekly
injectable read ~14% and fired a false low-compliance alert (and a paused
medication mis-fired the same way). Route the alert path through the
canonical `calculateCompliance` engine like every other compliance
surface — pause eras, schedule revisions and dose windows honoured.

Document confirm: committing a numeric observation against an existing
biomarker silently replaced the stated unit with the catalog's, with no
conversion — a stated "6.1 mmol/L" against a mg/dL marker committed
"6.1 mg/dL". Reject on a unit mismatch (fail closed) so the user
reconciles on the review screen, the same posture as the unit-required
guard.

Wire grip strength, waist (with waist-to-height) and pain into the
briefing feature set and the grounding allow-set so the clinical-depth
signals reach the daily narrative; the mental-health screeners stay
excluded by construction.

Biomarker detail: fall back to a generic description for catalog-less
markers, and keep the heading + description with a retry on the error
state. Collapse the medication External-API and Lifecycle groups by
default. Re-enable the insights scroll-to-top regression and assert the
shipped main-content reset.

Align the EN haemoglobin/haematocrit display names to British spelling.

Drop the redundant `(userId, deletedAt)` index on inbound documents — a
strict prefix of the new three-column index. Trim the dead
MedicalDisclaimer variants to the one live `dataPosture`. Move the
crisis-resources card off the retired amber tint to a calm
destructive-subtle border. Add a registry invariant asserting every
biomarker signal key resolves to a real catalog slug.
2026-06-29 00:37:39 +02:00

110 lines
4.2 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* v1.4.28 R3a FB-D3 — `/insights` scroll restoration + tab-strip
* gesture handling.
*
* Two regressions the maintainer hit walking through v1.4.27:
*
* (a) Returning from a sub-page to `/insights` landed the viewport
* partway down the page because the mother page had no scroll
* reset on mount and the sub-page's `SubPageShell` had already
* reset it for the sub-page only.
*
* (b) Vertical swipes that started on the sticky `<InsightsTabStrip>`
* were eaten by the strip's `overflow-x-auto` scroll container,
* so the page "felt stuck" until the gesture lifted.
*
* Both fixes land in the same commit. This spec is the regression
* lock — failure here means a future refactor re-introduced one of
* the two roots.
*/
test.describe("v1.4.28 — insights scroll restoration", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test("returning to /insights from a sub-page lands at the top", async ({
page,
}) => {
await page.goto("/insights", { waitUntil: "domcontentloaded" });
// v1.25.1 — the app-wide scroll-to-top resets the `#main-content`
// scroll container on every route change (auth-shell), with a
// `window.scrollTo(0,0)` fallback for the body-scrolled shells. The
// authenticated insights surface scrolls `#main-content`, so the
// regression lock reads that container, not `window`.
const readScrollTop = () =>
page.evaluate(() => {
const el = document.getElementById("main-content");
return el && el.scrollHeight > el.clientHeight
? el.scrollTop
: window.scrollY;
});
// Force the mother page tall enough that scrolling has somewhere
// to go even on a sparse seed; the rendered Insights overview
// already runs ~1500 px tall on the demo seed.
await page.evaluate(() => {
const el = document.getElementById("main-content");
if (el && el.scrollHeight > el.clientHeight) {
el.scrollTop = 600;
} else {
window.scrollTo({ top: 600, behavior: "auto" });
}
});
// Click the first available sub-page pill.
const subPagePill = page
.locator("[data-slot='insights-tab-strip-pill']")
.nth(1);
await expect(subPagePill).toBeVisible();
await subPagePill.click();
await page.waitForURL(/\/insights\//);
// Back to the mother page via the overview pill (pill index 0).
await page.locator("[data-slot='insights-tab-strip-pill']").first().click();
await page.waitForURL(/\/insights\/?$/);
// The shell's route-change effect resets the scroll on the next tick;
// `useScrollResetOnRoute` defers to `requestAnimationFrame`. Wait for it
// to settle, then assert the container is back at the top.
await expect.poll(readScrollTop, { timeout: 2000 }).toBeLessThan(50);
});
test("the sticky tab strip declares touch-action pan-y so vertical swipes pass through", async ({
page,
}) => {
await page.goto("/insights", { waitUntil: "domcontentloaded" });
const nav = page.locator("[data-slot='insights-tab-strip']");
await expect(nav).toBeVisible();
const touchAction = await nav.evaluate(
(el) =>
(el as HTMLElement).style.touchAction ||
getComputedStyle(el).touchAction,
);
expect(touchAction).toContain("pan-y");
});
test("the tab strip's horizontal scroll lives on an inner row, not on the outer nav", async ({
page,
}) => {
await page.goto("/insights", { waitUntil: "domcontentloaded" });
const nav = page.locator("[data-slot='insights-tab-strip']");
const navOverflowX = await nav.evaluate(
(el) => getComputedStyle(el).overflowX,
);
// The outer `<nav>` no longer scrolls horizontally — the inner row
// owns that responsibility now so vertical swipes outside the row
// reach the document.
expect(navOverflowX).not.toBe("auto");
expect(navOverflowX).not.toBe("scroll");
const innerScroller = nav.locator("> div > div").first();
const innerOverflowX = await innerScroller.evaluate(
(el) => getComputedStyle(el).overflowX,
);
expect(innerOverflowX).toMatch(/auto|scroll/);
});
});