diff --git a/e2e/insights-scroll-restoration.spec.ts b/e2e/insights-scroll-restoration.spec.ts new file mode 100644 index 000000000..9010c7399 --- /dev/null +++ b/e2e/insights-scroll-restoration.spec.ts @@ -0,0 +1,90 @@ +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 `` + * 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" }); + // 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(() => 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 mother page's mount-effect deferred a `scrollTo(0)` to the + // next animation frame. Wait a tick for it to land. + await page.waitForTimeout(50); + const y = await page.evaluate(() => window.scrollY); + expect(y).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 `