Files
siftlode/frontend/e2e/helpers.ts
T
peter 5e1cc4d981 fix(a11y,test): deep-review round 2 — e2e helper attr, trigger names, observer test
Second gate round over the full branch surfaced 4 verified findings (1 was a
regression the round-1 role change introduced); a 5th (--sbw load jump) was
dropped as refuted (the header only mounts after auth+data, so --sbw is set
before it ever paints).

- e2e (medium): the round-1 switch of FilterSelect rows from role=option to
  menuitemcheckbox moved their state from aria-selected to aria-checked, but
  the Playwright helper expectContentType still asserted aria-selected, so the
  channel specs would fail whenever the e2e lane runs (not part of the fast
  gate, so it stayed green). Assert aria-checked.
- a11y (low): the FilterSelect and SortControl pill triggers set
  aria-label={label}, which overrides the visible "Label: value" text, so a
  screen reader announced only the group name, never the current selection.
  Carry the value (and, for sort, the direction) in the accessible name,
  matching ViewSwitcher. Updated the unit-test trigger name queries to match.
- tests (low): cover the PageShell font-scale MutationObserver recompute path
  (a post-mount root font-size change → --rail-pinned-w re-computes once, no
  loop) — previously only the pre-render and resize paths were exercised.

Verified live: the triggers now announce e.g. "Sort: Date, Descending" /
"Watch state: All". Gate green: tsc (app+node+e2e), 373 vitest, prettier,
eslint (0 err).
2026-08-22 01:06:22 +02:00

66 lines
3.0 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-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<void> {
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<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();
}