/** * Page Object Models for WellNuo E2E Tests * Encapsulates page-specific selectors and actions */ import { Page, expect, Locator } from '@playwright/test'; import { BASE_URL } from './test-helpers'; /** * Login Page Object */ export class LoginPage { readonly page: Page; readonly welcomeText: Locator; readonly emailInput: Locator; readonly continueButton: Locator; readonly inviteCodeLink: Locator; constructor(page: Page) { this.page = page; this.welcomeText = page.getByText('Welcome to WellNuo'); this.emailInput = page.getByPlaceholder('Enter your email'); this.continueButton = page.getByText('Continue'); this.inviteCodeLink = page.getByText('I have an invite code'); } async goto() { await this.page.goto(BASE_URL); await this.page.waitForLoadState('networkidle'); } async expectLoaded() { await expect(this.welcomeText).toBeVisible({ timeout: 10000 }); await expect(this.emailInput).toBeVisible(); await expect(this.continueButton).toBeVisible(); } async enterEmail(email: string) { await this.emailInput.fill(email); } async submit() { await this.continueButton.click(); } async loginWithEmail(email: string) { await this.enterEmail(email); await this.submit(); } } /** * OTP Verification Page Object */ export class OtpPage { readonly page: Page; readonly headerText: Locator; readonly verifyButton: Locator; readonly resendButton: Locator; readonly useDifferentEmailLink: Locator; constructor(page: Page) { this.page = page; this.headerText = page.getByText('Check your email'); this.verifyButton = page.getByText('Verify'); this.resendButton = page.getByText('Resend'); this.useDifferentEmailLink = page.getByText('Use a different email'); } async expectLoaded() { await expect(this.headerText).toBeVisible({ timeout: 15000 }); await expect(this.verifyButton).toBeVisible(); await expect(this.resendButton).toBeVisible(); } async enterCode(code: string) { // OTP input is hidden, type using keyboard await this.page.click('body'); await this.page.keyboard.type(code, { delay: 50 }); } async verify() { await this.verifyButton.click(); } async resend() { await this.resendButton.click(); } async goBackToLogin() { await this.useDifferentEmailLink.click(); } async expectResendCooldown() { await expect(this.page.getByText(/Resend in \d+s/)).toBeVisible({ timeout: 5000 }); } async expectErrorMessage() { await expect( this.page.getByText(/Invalid|incorrect|error/i) ).toBeVisible({ timeout: 5000 }); } } /** * Enter Name Page Object (new user onboarding) */ export class EnterNamePage { readonly page: Page; readonly headerText: Locator; readonly nameInput: Locator; readonly continueButton: Locator; constructor(page: Page) { this.page = page; this.headerText = page.getByText('What should we call you?'); this.nameInput = page.getByPlaceholder(/name/i); this.continueButton = page.getByText('Continue'); } async expectLoaded() { await expect(this.headerText).toBeVisible({ timeout: 10000 }); await expect(this.nameInput).toBeVisible(); } async enterName(name: string) { await this.nameInput.fill(name); } async submit() { await this.continueButton.click(); } } /** * Add Loved One Page Object */ export class AddLovedOnePage { readonly page: Page; readonly headerText: Locator; readonly firstNameInput: Locator; readonly lastNameInput: Locator; readonly addButton: Locator; constructor(page: Page) { this.page = page; this.headerText = page.getByText(/Add.*Loved One/i); this.firstNameInput = page.getByPlaceholder(/first.*name/i); this.lastNameInput = page.getByPlaceholder(/last.*name/i); this.addButton = page.getByText(/Add|Continue/i); } async expectLoaded() { await expect(this.headerText).toBeVisible({ timeout: 10000 }); } async enterBeneficiaryDetails(firstName: string, lastName?: string) { await this.firstNameInput.fill(firstName); if (lastName && await this.lastNameInput.isVisible()) { await this.lastNameInput.fill(lastName); } } async submit() { await this.addButton.click(); } } /** * Beneficiaries List Page Object */ export class BeneficiariesPage { readonly page: Page; readonly headerText: Locator; readonly addButton: Locator; constructor(page: Page) { this.page = page; this.headerText = page.getByText('My Loved Ones'); this.addButton = page.getByText(/Add|Plus/i); } async expectLoaded() { await expect(this.headerText).toBeVisible({ timeout: 15000 }); } async getBeneficiaryCards(): Promise { return this.page.locator('[data-testid="beneficiary-card"]'); } async clickBeneficiary(name: string) { await this.page.getByText(name).click(); } async clickFirstBeneficiary() { const cards = await this.getBeneficiaryCards(); await cards.first().click(); } } /** * Beneficiary Detail Page Object */ export class BeneficiaryDetailPage { readonly page: Page; readonly subscriptionButton: Locator; readonly equipmentButton: Locator; readonly shareButton: Locator; readonly menuButton: Locator; constructor(page: Page) { this.page = page; this.subscriptionButton = page.getByText('Subscription'); this.equipmentButton = page.getByText(/Equipment|Sensors/i); this.shareButton = page.getByText('Share'); this.menuButton = page.locator('[data-testid="menu-button"]'); } async expectLoaded() { await expect(this.subscriptionButton.or(this.equipmentButton)).toBeVisible({ timeout: 10000 }); } async goToSubscription() { await this.subscriptionButton.click(); } async goToEquipment() { await this.equipmentButton.click(); } async goToShare() { await this.shareButton.click(); } async openMenu() { await this.menuButton.click(); } } /** * Subscription Page Object */ export class SubscriptionPage { readonly page: Page; readonly subscribeButton: Locator; readonly activeStatus: Locator; readonly demoStatus: Locator; readonly cancelButton: Locator; constructor(page: Page) { this.page = page; this.subscribeButton = page.getByText('Subscribe'); this.activeStatus = page.getByText('Subscription Active'); this.demoStatus = page.getByText(/Demo/i); this.cancelButton = page.getByText(/Cancel.*Subscription/i); } async expectLoaded() { await expect( this.subscribeButton.or(this.activeStatus).or(this.demoStatus) ).toBeVisible({ timeout: 10000 }); } async subscribe() { await this.subscribeButton.click(); } async isSubscriptionActive(): Promise { return await this.activeStatus.isVisible({ timeout: 2000 }).catch(() => false); } async isDemoMode(): Promise { return await this.demoStatus.isVisible({ timeout: 2000 }).catch(() => false); } } /** * Equipment/Sensors Page Object */ export class EquipmentPage { readonly page: Page; readonly totalSensors: Locator; readonly onlineSensors: Locator; readonly offlineSensors: Locator; readonly addSensorsButton: Locator; readonly emptySensorState: Locator; constructor(page: Page) { this.page = page; this.totalSensors = page.getByText(/Total/i); this.onlineSensors = page.getByText(/Online/i); this.offlineSensors = page.getByText(/Offline/i); this.addSensorsButton = page.getByText(/Add.*Sensor/i); this.emptySensorState = page.getByText('No Sensors Connected'); } async expectLoaded() { await expect( this.totalSensors.or(this.emptySensorState) ).toBeVisible({ timeout: 10000 }); } async getSensorCount(): Promise { const totalText = await this.totalSensors.textContent(); const match = totalText?.match(/\d+/); return match ? parseInt(match[0], 10) : 0; } async addSensors() { await this.addSensorsButton.click(); } async hasSensors(): Promise { return !(await this.emptySensorState.isVisible({ timeout: 2000 }).catch(() => false)); } } /** * Profile Page Object */ export class ProfilePage { readonly page: Page; readonly editButton: Locator; readonly logoutButton: Locator; readonly notificationsItem: Locator; readonly languageItem: Locator; readonly helpItem: Locator; readonly aboutItem: Locator; constructor(page: Page) { this.page = page; this.editButton = page.getByText('Edit Profile'); this.logoutButton = page.getByText('Log Out'); this.notificationsItem = page.getByText('Notifications'); this.languageItem = page.getByText('Language'); this.helpItem = page.getByText('Help'); this.aboutItem = page.getByText('About'); } async expectLoaded() { await expect(this.logoutButton).toBeVisible({ timeout: 10000 }); } async editProfile() { await this.editButton.click(); } async logout() { await this.logoutButton.click(); } async goToNotifications() { await this.notificationsItem.click(); } async goToLanguage() { await this.languageItem.click(); } async goToHelp() { await this.helpItem.click(); } async goToAbout() { await this.aboutItem.click(); } }