The shell owns the scroller, so it owns the scroll policy. PageScroller now takes a scrollKey identifying the logical view and keeps a per-view offset in a module-level map, recorded live on scroll: - same key returning (feed -> channel -> Back, filters unchanged) restores where you were (bug 1) -- the feed's scroller unmounts, but the saved offset survives and is re-applied, retried over a few frames so the virtualized list has time to reach its full height; - a new key (feed filters changed) starts at the top of the fresh result set (bug 3). q is normalised out of the feed key so typing doesn't reset. Recording on scroll rather than at unmount matters: by cleanup time the scroller's scrollTop is already zeroed, so a cleanup-read saved 0. Also fixes a Maximum-update-depth loop this introduced: the combined element/fade ref was an inline callback, so React re-attached it every commit and re-ran useScrollFade's state setter. Bound it with useCallback. Adds E2E locks for bugs 1, 3 and 4 (restore on Back, reset on filter, toolbar stays pinned). Full suite 14/14 green on :5173; tsc + knip clean.
59 lines
2.8 KiB
TypeScript
59 lines
2.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { openApp } from "./helpers";
|
|
|
|
// Locks channel-view entry + exit: opening a channel from a feed card replaces the view, and
|
|
// both the in-page Back and the browser Back return to the feed. (This orthogonal channel branch
|
|
// is exactly what the Platform refactor folds into the PageShell contract.)
|
|
test.describe("channel view", () => {
|
|
test("open from a card, then in-page Back returns to the feed", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
await expect(page.getByTestId("channel-title")).toBeVisible();
|
|
await expect(page.getByTestId("channel-back")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-back").click();
|
|
await expect(page.getByTestId("channel-title")).toHaveCount(0);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
});
|
|
|
|
test("browser Back also closes the channel page", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
await expect(page.getByTestId("channel-title")).toBeVisible();
|
|
|
|
await page.goBack();
|
|
await expect(page.getByTestId("channel-title")).toHaveCount(0);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
});
|
|
|
|
// Bug 1 (Platform S4): opening a channel and coming Back restores the feed's scroll position
|
|
// rather than dumping you at the top. The shell owns the scroller and remembers each view's offset.
|
|
test("Back restores the feed scroll position", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
|
|
const main = page.getByRole("main");
|
|
await main.evaluate((el) => el.scrollBy(0, 3000));
|
|
// Let any page-fetch triggered by the scroll settle, so the same content is cached for the
|
|
// return trip (a fetch still in flight at navigation would leave a shorter list to restore into).
|
|
await page.waitForTimeout(600);
|
|
const saved = await main.evaluate((el) => el.scrollTop);
|
|
expect(saved).toBeGreaterThan(1500);
|
|
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
await expect(page.getByTestId("channel-title")).toBeVisible();
|
|
await page.getByTestId("channel-back").click();
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
|
|
// Restored to roughly where we were, not reset to the top. Tolerance absorbs the virtualizer's
|
|
// estimate-vs-measured row heights differing slightly between the two mounts.
|
|
await expect
|
|
.poll(() => page.getByRole("main").evaluate((el) => el.scrollTop), { timeout: 5000 })
|
|
.toBeGreaterThan(saved - 500);
|
|
});
|
|
});
|