/** * Base page object with shared helpers. * * All page objects extend this class to inherit common navigation, * waiting, and assertion patterns. */ import { type Page, type Locator, expect } from '@playwright/test'; export abstract class BasePage { constructor(protected readonly page: Page) {} /** The path segment for this page (e.g. '/dashboard', '/accounts') */ abstract readonly path: string; /** Navigate to this page */ async goto(): Promise { await this.page.goto(this.path); await this.waitForReady(); } /** * Override in subclasses to wait for page-specific readiness signals. * Default: waits for network idle. */ async waitForReady(): Promise { await this.page.waitForLoadState('networkidle'); } /** Assert the page URL contains the expected path */ async assertOnPage(): Promise { await expect(this.page).toHaveURL(new RegExp(this.path)); } /** Get the page title text (Mantine AppShell header or h1) */ async getPageHeading(): Promise { const heading = this.page.getByRole('heading', { level: 1 }).first(); return heading.innerText(); } /** Wait for a Mantine notification to appear with the given text */ async waitForNotification(text: string | RegExp): Promise { await expect( this.page.locator('.mantine-Notification-root').filter({ hasText: text }), ).toBeVisible({ timeout: 10_000 }); } /** Click a navigation link in the sidebar */ async navigateTo(linkText: string): Promise { await this.page.getByRole('link', { name: linkText }).click(); await this.page.waitForLoadState('networkidle'); } /** Get a Mantine table body locator */ get tableBody(): Locator { return this.page.locator('tbody'); } /** Get all table rows */ get tableRows(): Locator { return this.tableBody.locator('tr'); } /** Wait for API response on a specific endpoint pattern */ async waitForApi(urlPattern: string | RegExp): Promise { await this.page.waitForResponse( (response) => (typeof urlPattern === 'string' ? response.url().includes(urlPattern) : urlPattern.test(response.url())) && response.status() < 400, ); } }