A tab names the account it was claimed for on every request, beacon and upload (X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with 401 account_changed and keep the session, so a tab left showing the account signed in before reloads instead of writing to the new one. /auth/login clears a connect marker left by an abandoned connect, and a link completes only on a session that still holds. The prefs merge reads the row under a lock. A tab's sessionStorage records its own owner. A page the account loses is given up, address bar and remembered page included. The panel layouts are preferences; the storage contract also refuses a key handed to the Storage API or to usePref outside its kind. A refused sign-in as someone else and a failed sign-out say so. The chromium-only e2e specs are in the gate register, the member walk waits for its requests, and devices.spec checks the save itself.
75 lines
3.5 KiB
TypeScript
75 lines
3.5 KiB
TypeScript
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<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 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<boolean> {
|
|
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<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();
|
|
}
|