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>
368 lines
9.1 KiB
TypeScript
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();
|
|
}
|
|
}
|