import { expect, type Page } from "@playwright/test"; /** The plain member's saved session (global.setup.ts signs it in); `member.spec.ts` and * `devices.spec.ts` run from it. */ export const MEMBER_STORAGE = "e2e/.auth/member.json"; /** Open the app and wait for the authenticated shell (the nav rail) to be ready. */ export async function openApp(page: Page, path = "/"): Promise { await page.goto(path); await expect(page.getByTestId("nav-feed")).toBeVisible(); } /** Click a top-level module in the nav rail and confirm it becomes the active page. */ export async function gotoPage(page: Page, id: string): Promise { await page.getByTestId(`nav-${id}`).click(); await expect(page.getByTestId(`nav-${id}`)).toHaveAttribute("aria-current", "page"); } /** * Click a channel link a real user can actually SEE and hit. 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. Being inside the viewport is not enough either: a card * can sit under the page chrome above the scroller (the update banner, the toolbar band), and the * covered click makes Playwright scroll and retry — the same move (webkit failed 3 in 24). * So the link must be the element hit at its own centre. Returns false if none found. */ export async function clickVisibleChannelLink(page: Page): Promise { const links = page.getByTestId("video-card-channel").filter({ visible: true }); const count = await links.count(); for (let i = 0; i < count; i++) { const hittable = await links.nth(i).evaluate((el) => { const r = el.getBoundingClientRect(); const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2); return hit !== null && el.contains(hit); }); if (hittable) { await links.nth(i).click(); return true; } } return false; } /** * Assert a content-type (e.g. "includeLive") is on/off via the Content-types FilterSelect dropdown. * The options only exist while the dropdown is open, so this opens it, checks the option's * `aria-checked` (the multi FilterSelect renders its rows as menuitemcheckbox), then closes it * (Escape) so the floating panel can't block a later click. */ export async function expectContentType(page: Page, value: string, on: boolean): Promise { await page.getByTestId("feed-content").click(); await expect(page.getByTestId(`feed-content-${value}`)).toHaveAttribute( "aria-checked", on ? "true" : "false", ); await page.keyboard.press("Escape"); await expect(page.getByTestId(`feed-content-${value}`)).toHaveCount(0); } /** Toggle a content-type via the Content-types FilterSelect dropdown, then close it. */ export async function toggleContentType(page: Page, value: string): Promise { await page.getByTestId("feed-content").click(); await page.getByTestId(`feed-content-${value}`).click(); await page.keyboard.press("Escape"); await expect(page.getByTestId(`feed-content-${value}`)).toHaveCount(0); } /** 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(); await page.getByTestId("video-card").first().getByRole("link").first().click(); await expect(page.getByTestId("player-modal")).toBeVisible(); }