mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* add error allowlist file for error collector * add error collector for console + page + request errors * wire error collector into frigateApp fixture * add self-tests for error collector fixture * gate strict error mode on E2E_STRICT_ERRORS=1 * triage pre-existing errors and seed allowlist * add mockEmpty/mockError/mockDelay helpers for state-driven tests * add self-tests for mock override helpers * add mobile affordance helpers to BasePage * add lint script for banned spec patterns and @mobile rule * apply prettier fixes to new e2e files * rewrite export.spec.ts * clean up * move export spec rewrite and bugfix to separate branch
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
/**
|
|
* Base page object with viewport-aware navigation helpers.
|
|
*
|
|
* Desktop: clicks sidebar NavLink elements.
|
|
* Mobile: clicks bottombar NavLink elements.
|
|
*/
|
|
|
|
import type { Page, Locator } from "@playwright/test";
|
|
|
|
export class BasePage {
|
|
constructor(
|
|
protected page: Page,
|
|
public isDesktop: boolean,
|
|
) {}
|
|
|
|
get isMobile() {
|
|
return !this.isDesktop;
|
|
}
|
|
|
|
/** The sidebar (desktop only) */
|
|
get sidebar(): Locator {
|
|
return this.page.locator("aside");
|
|
}
|
|
|
|
/** The bottombar (mobile only) */
|
|
get bottombar(): Locator {
|
|
return this.page
|
|
.locator('[data-bottombar="true"]')
|
|
.or(this.page.locator(".absolute.inset-x-4.bottom-0").first());
|
|
}
|
|
|
|
/** The main page content area */
|
|
get pageRoot(): Locator {
|
|
return this.page.locator("#pageRoot");
|
|
}
|
|
|
|
/** Navigate using a NavLink by its href */
|
|
async navigateTo(path: string) {
|
|
// Wait for any in-progress React renders to settle before clicking
|
|
await this.page.waitForLoadState("domcontentloaded");
|
|
// Use page.click with a CSS selector to avoid stale element issues
|
|
// when React re-renders the nav during route transitions.
|
|
// force: true bypasses actionability checks that fail when React
|
|
// detaches and reattaches nav elements during re-renders.
|
|
const selector = this.isDesktop
|
|
? `aside a[href="${path}"]`
|
|
: `a[href="${path}"]`;
|
|
// Use dispatchEvent to bypass actionability checks that fail when
|
|
// React tooltip wrappers detach/reattach nav elements during re-renders
|
|
await this.page.locator(selector).first().dispatchEvent("click");
|
|
// React Router navigates client-side, wait for URL update
|
|
if (path !== "/") {
|
|
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
await this.page.waitForURL(new RegExp(escaped), { timeout: 10_000 });
|
|
}
|
|
}
|
|
|
|
/** Navigate to Live page */
|
|
async goToLive() {
|
|
await this.navigateTo("/");
|
|
}
|
|
|
|
/** Navigate to Review page */
|
|
async goToReview() {
|
|
await this.navigateTo("/review");
|
|
}
|
|
|
|
/** Navigate to Explore page */
|
|
async goToExplore() {
|
|
await this.navigateTo("/explore");
|
|
}
|
|
|
|
/** Navigate to Export page */
|
|
async goToExport() {
|
|
await this.navigateTo("/export");
|
|
}
|
|
|
|
/** Check if the page has loaded */
|
|
async waitForPageLoad() {
|
|
await this.page.waitForSelector("#pageRoot", { timeout: 10_000 });
|
|
}
|
|
|
|
/**
|
|
* Open the mobile-only export pane / sheet that slides up from the
|
|
* bottom on the export page. No-op on desktop. Returns the pane locator
|
|
* so the caller can assert against its contents.
|
|
*/
|
|
async openMobilePane(): Promise<Locator> {
|
|
if (this.isDesktop) {
|
|
// Return the desktop equivalent (the main content area itself)
|
|
return this.pageRoot;
|
|
}
|
|
// Look for any element that opens a sheet/dialog on tap.
|
|
// Specific views override this with their own selector.
|
|
const pane = this.page.locator('[role="dialog"]').first();
|
|
return pane;
|
|
}
|
|
|
|
/**
|
|
* Open a side drawer (e.g. mobile filter drawer). View-specific page
|
|
* objects should override this with their actual trigger selector.
|
|
* The default implementation looks for a button labelled "Open menu"
|
|
* or "Filters" and clicks it, then returns the drawer locator.
|
|
*/
|
|
async openDrawer(): Promise<Locator> {
|
|
if (this.isDesktop) {
|
|
return this.pageRoot;
|
|
}
|
|
const trigger = this.page
|
|
.getByRole("button", { name: /menu|filter/i })
|
|
.first();
|
|
if (await trigger.count()) {
|
|
await trigger.click();
|
|
}
|
|
return this.page.locator('[role="dialog"], [data-state="open"]').first();
|
|
}
|
|
|
|
/**
|
|
* Open a bottom sheet (vaul). View-specific page objects should
|
|
* override this with their actual trigger selector.
|
|
*/
|
|
async openBottomSheet(): Promise<Locator> {
|
|
if (this.isDesktop) {
|
|
return this.pageRoot;
|
|
}
|
|
return this.page.locator("[vaul-drawer]").first();
|
|
}
|
|
|
|
/** Close any currently-open mobile overlay (drawer, sheet, dialog). */
|
|
async closeMobileOverlay(): Promise<void> {
|
|
if (this.isDesktop) return;
|
|
// Press Escape — Radix dialogs and vaul both close on Escape
|
|
await this.page.keyboard.press("Escape");
|
|
}
|
|
}
|