diff --git a/frontend/e2e/channel.spec.ts b/frontend/e2e/channel.spec.ts index 7106bd1f..a2136228 100644 --- a/frontend/e2e/channel.spec.ts +++ b/frontend/e2e/channel.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test"; -import { openApp } from "./helpers"; +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 @@ -44,7 +44,11 @@ test.describe("channel view", () => { const saved = await main.evaluate((el) => el.scrollTop); expect(saved).toBeGreaterThan(1500); - await page.getByTestId("video-card-channel").first().click(); + // 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(); diff --git a/frontend/e2e/helpers.ts b/frontend/e2e/helpers.ts index e69ae936..de3c4693 100644 --- a/frontend/e2e/helpers.ts +++ b/frontend/e2e/helpers.ts @@ -12,6 +12,27 @@ export async function gotoPage(page: Page, id: string): Promise { await expect(page.getByTestId(`nav-${id}`)).toHaveAttribute("aria-current", "page"); } +/** + * Click a channel link that is currently WITHIN the viewport — what a real user does. Never use + * `.first()` for this after scrolling: the first rendered card lives in the virtualizer's overscan + * ABOVE the viewport, so Playwright would scroll it into view to click it, moving the scroll + * position — fatal to any test that then asserts on where the feed was. Returns false if none found. + */ +export async function clickVisibleChannelLink(page: Page): Promise { + const links = page.getByTestId("video-card-channel").filter({ visible: true }); + const viewport = page.viewportSize(); + const height = viewport?.height ?? 800; + const count = await links.count(); + for (let i = 0; i < count; i++) { + const box = await links.nth(i).boundingBox(); + if (box && box.y >= 0 && box.y + box.height <= height) { + await links.nth(i).click(); + return true; + } + } + return false; +} + /** Open the in-app player from the first feed card (clicks its thumbnail link). */ export async function openFirstVideo(page: Page): Promise { await expect(page.getByTestId("video-card").first()).toBeVisible();