The "Back restores the feed scroll position" spec failed against the production
build while the app was in fact correct. Root cause, found by instrumenting
PageScroller and comparing the baked :8080 build to live :5173:
`getByTestId("video-card-channel").first()` grabs the first card in the DOM, which
after scrolling 3000px is in the virtualizer's overscan ABOVE the viewport.
Playwright scrolls it into view to click it, moving the feed from 2879 to ~1017–1253
BEFORE navigating; PageScroller's save-on-scroll records that moved position, so Back
faithfully restores the moved position — under the test's own tolerance. A real user
clicks a card they can see, which does not move the feed.
New helper clickVisibleChannelLink() clicks a card whose bounding box is inside the
viewport. Restore now lands at ~2804 (was ~1017). 17/17 pass; stable over 3 reruns.
No app change — the scroll-restore logic was already correct.
150 lines
7.3 KiB
TypeScript
150 lines
7.3 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { clickVisibleChannelLink, 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);
|
|
|
|
// Click a channel card that is actually ON SCREEN — a real user clicks what they see. Clicking
|
|
// `.first()` here would grab an overscan card ABOVE the viewport, and Playwright's scroll-into-
|
|
// view would move the feed before navigating, so Back would restore that moved position, not
|
|
// the one under test. (This is what made the test fail while the app was in fact correct.)
|
|
expect(await clickVisibleChannelLink(page)).toBe(true);
|
|
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);
|
|
});
|
|
|
|
// Bug 2 (Platform S5): opening a channel no longer FORCES Live/Upcoming on — the channel view
|
|
// seeds its content-type prefs (normal / shorts / live) from the global feed. Locked both ways so
|
|
// a future refactor can't silently re-hardcode it.
|
|
test("the channel view inherits the feed's content filters (no forced Live/Upcoming)", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
const feedLive = page.getByTestId("feed-content-includeLive");
|
|
await expect(feedLive).toHaveAttribute("aria-pressed", "false");
|
|
|
|
// Inherit OFF: the channel toolbar's Live/Upcoming stays off, not forced on.
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
await expect(page.getByTestId("channel-title")).toBeVisible();
|
|
await expect(page.getByTestId("feed-content-includeLive")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"false",
|
|
);
|
|
await page.getByTestId("channel-back").click();
|
|
await expect(page.getByTestId("channel-title")).toHaveCount(0);
|
|
|
|
// Inherit ON: flip the feed's Live/Upcoming on, reopen the channel → it inherits the on state.
|
|
await feedLive.click();
|
|
await expect(feedLive).toHaveAttribute("aria-pressed", "true");
|
|
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("feed-content-includeLive")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
// Bug 2 (Platform S5): the channel view is its OWN filter scope — a content toggle made there
|
|
// must NOT leak back into the global feed. (FeedFiltersProvider owns the feed's filters; the
|
|
// channel page keeps a separate local cell.)
|
|
test("a channel's content toggle does not leak into the feed", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
await expect(page.getByTestId("feed-content-includeLive")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"false",
|
|
);
|
|
|
|
// Turn Live/Upcoming ON inside the channel view.
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
await expect(page.getByTestId("channel-title")).toBeVisible();
|
|
await page.getByTestId("feed-content-includeLive").click();
|
|
await expect(page.getByTestId("feed-content-includeLive")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"true",
|
|
);
|
|
|
|
// Back on the feed, its Live/Upcoming is still off — the channel's change stayed local.
|
|
await page.getByTestId("channel-back").click();
|
|
await expect(page.getByTestId("channel-title")).toHaveCount(0);
|
|
await expect(page.getByTestId("feed-content-includeLive")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"false",
|
|
);
|
|
});
|
|
|
|
// Platform S4b: the whole channel chrome (banner, identity, tabs, toolbar) is fixed — it stays
|
|
// put while only the video grid scrolls.
|
|
test("the channel chrome stays fixed while the video grid scrolls", async ({ page }) => {
|
|
await openApp(page);
|
|
await page.getByTestId("video-card-channel").first().click();
|
|
const count = page.getByTestId("feed-result-count");
|
|
const title = page.getByTestId("channel-title");
|
|
await expect(count).toBeVisible();
|
|
await expect(page.getByTestId("video-card").first()).toBeVisible();
|
|
const titleTop = await title.evaluate((el) => el.getBoundingClientRect().top);
|
|
const countTop = await count.evaluate((el) => el.getBoundingClientRect().top);
|
|
|
|
// Keep nudging until the grid is tall enough to actually scroll (it may still be filling in).
|
|
await expect
|
|
.poll(async () => {
|
|
await page.getByRole("main").evaluate((el) => el.scrollBy(0, 1500));
|
|
return page.getByRole("main").evaluate((el) => el.scrollTop);
|
|
})
|
|
.toBeGreaterThan(400);
|
|
|
|
// The chrome didn't move — banner title and toolbar are in exactly the same place.
|
|
expect(await title.evaluate((el) => el.getBoundingClientRect().top)).toBe(titleTop);
|
|
expect(await count.evaluate((el) => el.getBoundingClientRect().top)).toBe(countTop);
|
|
});
|
|
});
|