Files
siftlode/frontend/e2e/channel.spec.ts
T
peter 0e03e561eb feat(shell): sticky tabs+toolbar on the channel page
Generalise the fixed-chrome pattern to the channel detail page: the
banner + identity now scroll away, while the tab row and the channel
feed's toolbar stick to the top as a sticky sub-header — only the video
grid scrolls.

Mechanism: PageToolbarSlot (the toolbar portal target) is now an exported,
overridable context. ChannelPage provides its own sticky sub-header node
as the target, so the channelScoped Feed portals its toolbar up next to
the tabs instead of into the shell's top band. Feed always portals its
toolbar now (the slot decides where it lands).

Also: the page scroller now fades only its BOTTOM edge — the top is
bounded by the fixed/sticky chrome, so a top fade only bit into it.

E2E: adds a lock that the channel tabs/toolbar stay pinned while the
banner scrolls away; suite 15/15 on :5173, tsc + knip clean.
2026-07-18 04:45:11 +02:00

83 lines
4.0 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);
});
// Platform S4b: the channel page's banner/identity scroll away, but the tabs + feed toolbar stick
// to the top (a sticky sub-header) so only the video grid scrolls.
test("tabs + toolbar stay pinned while the channel videos scroll", async ({ page }) => {
await openApp(page);
await page.getByTestId("video-card-channel").first().click();
const count = page.getByTestId("feed-result-count");
await expect(count).toBeVisible();
await expect(page.getByTestId("video-card").first()).toBeVisible();
await expect(page.getByTestId("channel-title")).toBeInViewport();
// 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);
// Banner/identity scrolled away, but the tabs+toolbar stuck near the top.
await expect(page.getByTestId("channel-title")).not.toBeInViewport();
await expect(count).toBeInViewport();
expect(await count.evaluate((el) => el.getBoundingClientRect().top)).toBeLessThan(130);
});
});