Files
siftlode/frontend/e2e/channel.spec.ts
T
peter 45d8b47366 feat(toolbar): fixed-width ItemCount pill, lead the toolbar with it, align chrome to the cards
- New ItemCount: a generic, FIXED-WIDTH count pill (glass, non-interactive) — "192,050 items"
  (neutral noun, dynamic plural), min-w-[9rem] so a changing number never shifts the controls
  beside it; a million or more collapses to "1M+" with the exact figure in the title tooltip.
- Feed toolbar: ItemCount is the fixed leading element now; dropped the mx-1 separators so every
  gap is a uniform 1.5 (Content-types↔Source used to be wider than Watch-state↔Content-types).
- Chrome-to-card alignment (app-wide, via the shared Header + PageShell): the floating header and
  the toolbar band now span exactly the card grid's width — left-4, and a right edge of
  1rem + the scrollbar gutter (measured once into --sbw), so the fixed chrome lines up with the
  scrolled cards instead of running ~15px past them. Wraps cleanly to multiple rows on mobile.

Renames the count testid feed-result-count → item-count (E2Es updated).
2026-08-21 20:33:55 +02:00

134 lines
6.9 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { clickVisibleChannelLink, expectContentType, openApp, toggleContentType } 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();
await expectContentType(page, "includeLive", 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 expectContentType(page, "includeLive", 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 toggleContentType(page, "includeLive");
await expectContentType(page, "includeLive", 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 expectContentType(page, "includeLive", 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 expectContentType(page, "includeLive", 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 toggleContentType(page, "includeLive");
await expectContentType(page, "includeLive", 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 expectContentType(page, "includeLive", 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("item-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);
});
});