/** * Page object for the Login page (/login). * * Maps to: frontend/src/pages/auth/LoginPage.tsx * Auth: POST /api/auth/login (Passport local strategy) */ import { type Page, expect } from '@playwright/test'; import { BasePage } from './BasePage'; export class LoginPage extends BasePage { readonly path = '/login'; // ─── Locators (resilient, role/label based) ────────────────── get emailInput() { return this.page.getByLabel('Email'); } get passwordInput() { return this.page.getByLabel('Password'); } get signInButton() { return this.page.getByRole('button', { name: 'Sign in' }); } get passkeyButton() { return this.page.getByRole('button', { name: /passkey/i }); } get errorAlert() { return this.page.getByRole('alert'); } get registerLink() { return this.page.getByRole('link', { name: /register/i }); } // ─── MFA locators ──────────────────────────────────────────── get mfaHeading() { return this.page.getByText('Two-Factor Authentication'); } get mfaPinInput() { return this.page.locator('.mantine-PinInput-input').first(); } get mfaVerifyButton() { return this.page.getByRole('button', { name: 'Verify' }); } // ─── Actions ───────────────────────────────────────────────── /** Fill and submit login credentials */ async login(email: string, password: string): Promise { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.signInButton.click(); } /** Login and wait for successful redirect */ async loginAndWaitForRedirect( email: string, password: string, ): Promise { await this.login(email, password); await this.page.waitForURL(/\/(select-org|dashboard|admin|onboarding)/, { timeout: 15_000, }); } /** Assert an error message is displayed */ async assertError(message: string | RegExp): Promise { await expect(this.errorAlert).toContainText(message); } /** Assert we're still on the login page */ async assertStillOnLogin(): Promise { await expect(this.page).toHaveURL(/\/login/); } override async waitForReady(): Promise { await expect(this.signInButton).toBeVisible(); } }