WellNuo/e2e/helpers/page-objects.ts
Sergei 67496d6913 Add comprehensive E2E tests for critical flows
Implement Playwright E2E tests covering 43 critical user flows:
- Authentication: login, OTP verification, validation, onboarding
- Beneficiary management: list, detail, equipment, subscription navigation
- Subscription: status display, Stripe integration, demo mode
- Profile: settings, edit, logout flow

Includes:
- Page Object Models for test maintainability
- Test helpers with common utilities
- Updated Playwright config with proper project structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:22:47 -08:00

368 lines
9.1 KiB
TypeScript

/**
* 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<Locator> {
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<boolean> {
return await this.activeStatus.isVisible({ timeout: 2000 }).catch(() => false);
}
async isDemoMode(): Promise<boolean> {
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<number> {
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<boolean> {
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();
}
}