import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { expect, type Locator, type Page } from '@playwright/test' const here = dirname(fileURLToPath(import.meta.url)) /** Where `screenshot()` drops PNGs — gitignored, readable for visual review. */ const SCREENS_DIR = join(here, '..', '.artifacts', 'screens') /** * Page Object over the CommiTea app shell. As real screens land (P3), add * per-screen page objects and accessor methods here; keep locators * user-facing (getByRole/getByText), never CSS/DOM structure. */ export class AppPage { constructor(readonly page: Page) {} /** The rail wordmark — present on every in-app view. */ get wordmark(): Locator { return this.page.getByText('CommiTea', { exact: true }) } /** Navigate via a left-rail entry by its label. Scoped to the Primary rail so * it never collides with in-screen breadcrumb buttons of the same name, and * matches by prefix so count badges (e.g. "Inbox 3") still resolve. */ nav(label: string): Locator { return this.page.getByRole('navigation', { name: 'Primary' }).getByRole('button', { name: label }) } /** Assert the shell has rendered. */ async expectLoaded(): Promise { await expect(this.wordmark).toBeVisible() } /** Read the `commitea` preload API surface from the renderer. */ async preloadApi(): Promise | undefined> { return this.page.evaluate( () => (globalThis as unknown as { commitea?: Record }).commitea, ) } /** Capture a full-page screenshot for visual review; returns the path. * `animations: 'disabled'` fast-forwards finite CSS animations to their end * state, so fade-in screens (e.g. Standup) capture settled, not mid-fade. */ async screenshot(name: string): Promise { const path = join(SCREENS_DIR, `${name}.png`) await this.page.screenshot({ path, fullPage: true, animations: 'disabled' }) return path } }