Adds a reusable FilterSelect: a "Label: value ▾" pill that opens a listbox (a checkmark
row for single-select, a checkbox row for multi), on the AnchoredPopover platform. It
replaces the two feed-toolbar chip walls:
- Watch state (single-select, default All) — picking a row replaces + closes.
- Content types (multi-select, default Normal; requireOne keeps at least one on) — rows
toggle and the panel stays open.
The trigger summarises multi picks as "First +N". The component is source-agnostic, meant
for reuse anywhere a small known option set currently renders as a chip group.
FilterSelect takes a `testId` → the trigger gets it, each option gets `${testId}-${value}`,
so the feed/channel E2Es keep stable selectors. They now open the dropdown and assert the
option's aria-selected instead of the old inline aria-pressed chips (new expectContentType/
toggleContentType helpers).
65 lines
2.9 KiB
TypeScript
65 lines
2.9 KiB
TypeScript
import { expect, type Page } from "@playwright/test";
|
|
|
|
/** Open the app and wait for the authenticated shell (the nav rail) to be ready. */
|
|
export async function openApp(page: Page, path = "/"): Promise<void> {
|
|
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<void> {
|
|
await page.getByTestId(`nav-${id}`).click();
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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-selected`, 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<void> {
|
|
await page.getByTestId("feed-content").click();
|
|
await expect(page.getByTestId(`feed-content-${value}`)).toHaveAttribute(
|
|
"aria-selected",
|
|
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<void> {
|
|
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<void> {
|
|
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();
|
|
}
|