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>
237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
/**
|
|
* E2E Test Helpers for WellNuo
|
|
* Common utilities for Playwright tests
|
|
*/
|
|
|
|
import { Page, expect } from '@playwright/test';
|
|
|
|
// E2E tests run against local Expo web server
|
|
// Start with: npm run web (expo start --web)
|
|
export const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8081';
|
|
|
|
// Test credentials (use bypass OTP for testing)
|
|
export const TEST_CREDENTIALS = {
|
|
// Test user with existing beneficiary
|
|
existingUser: {
|
|
email: 'e2e.test@wellnuo.com',
|
|
bypassOtp: '000000',
|
|
},
|
|
// Test user for new registration
|
|
newUser: {
|
|
email: `e2e.new.${Date.now()}@test.com`,
|
|
bypassOtp: '000000',
|
|
},
|
|
// Invalid test cases
|
|
invalid: {
|
|
email: 'invalid-email',
|
|
wrongOtp: '999999',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Wait for app to fully load
|
|
*/
|
|
export async function waitForAppLoad(page: Page, timeout = 15000): Promise<void> {
|
|
await page.waitForLoadState('networkidle', { timeout });
|
|
}
|
|
|
|
/**
|
|
* Navigate to login page and verify it loaded
|
|
*/
|
|
export async function goToLogin(page: Page): Promise<void> {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppLoad(page);
|
|
await expect(page.getByText('Welcome to WellNuo')).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Enter email and proceed to OTP screen
|
|
*/
|
|
export async function enterEmailAndSubmit(page: Page, email: string): Promise<void> {
|
|
await page.getByPlaceholder('Enter your email').fill(email);
|
|
await page.getByText('Continue').click();
|
|
}
|
|
|
|
/**
|
|
* Enter OTP code using keyboard (handles hidden input)
|
|
*/
|
|
export async function enterOtpCode(page: Page, code: string): Promise<void> {
|
|
// Click on the OTP container to focus
|
|
await page.click('body');
|
|
await page.keyboard.type(code, { delay: 50 });
|
|
}
|
|
|
|
/**
|
|
* Wait for OTP screen to appear
|
|
*/
|
|
export async function waitForOtpScreen(page: Page): Promise<void> {
|
|
await expect(page.getByText('Check your email')).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
/**
|
|
* Complete login flow (email + OTP)
|
|
*/
|
|
export async function loginWithOtp(
|
|
page: Page,
|
|
email: string,
|
|
otp: string = '000000'
|
|
): Promise<void> {
|
|
await goToLogin(page);
|
|
await enterEmailAndSubmit(page, email);
|
|
await waitForOtpScreen(page);
|
|
await enterOtpCode(page, otp);
|
|
// Wait for navigation after OTP
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
/**
|
|
* Wait for dashboard/main screen
|
|
*/
|
|
export async function waitForDashboard(page: Page): Promise<void> {
|
|
// Could be beneficiary list or dashboard depending on user state
|
|
await expect(
|
|
page.getByText('My Loved Ones').or(page.getByText('Dashboard'))
|
|
).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
/**
|
|
* Wait for enter-name screen (new user onboarding)
|
|
*/
|
|
export async function waitForEnterNameScreen(page: Page): Promise<void> {
|
|
await expect(
|
|
page.getByText('What should we call you?').or(page.getByPlaceholder('Your name'))
|
|
).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Wait for add-loved-one screen
|
|
*/
|
|
export async function waitForAddLovedOneScreen(page: Page): Promise<void> {
|
|
await expect(
|
|
page.getByText('Add a Loved One').or(page.getByText('loved one'))
|
|
).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Check if still on login page
|
|
*/
|
|
export async function isOnLoginPage(page: Page): Promise<boolean> {
|
|
return await page.getByText('Welcome to WellNuo').isVisible({ timeout: 1000 }).catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Check if still on OTP page
|
|
*/
|
|
export async function isOnOtpPage(page: Page): Promise<boolean> {
|
|
return await page.getByText('Check your email').isVisible({ timeout: 1000 }).catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Navigate to beneficiary detail
|
|
*/
|
|
export async function goToBeneficiaryDetail(page: Page, beneficiaryName?: string): Promise<void> {
|
|
if (beneficiaryName) {
|
|
await page.getByText(beneficiaryName).click();
|
|
} else {
|
|
// Click first beneficiary card
|
|
await page.locator('[data-testid="beneficiary-card"]').first().click();
|
|
}
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
/**
|
|
* Navigate to subscription screen for a beneficiary
|
|
*/
|
|
export async function goToSubscription(page: Page): Promise<void> {
|
|
await page.getByText('Subscription').click();
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
/**
|
|
* Navigate to equipment screen for a beneficiary
|
|
*/
|
|
export async function goToEquipment(page: Page): Promise<void> {
|
|
await page.getByText('Equipment').or(page.getByText('Sensors')).click();
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
/**
|
|
* Navigate to profile
|
|
*/
|
|
export async function goToProfile(page: Page): Promise<void> {
|
|
await page.getByText('Profile').click();
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
/**
|
|
* Click back button
|
|
*/
|
|
export async function goBack(page: Page): Promise<void> {
|
|
const backButton = page.locator('[data-testid="back-button"]').or(
|
|
page.getByText('Use a different email')
|
|
);
|
|
if (await backButton.isVisible()) {
|
|
await backButton.click();
|
|
await page.waitForTimeout(500);
|
|
} else {
|
|
// Try browser back
|
|
await page.goBack();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log browser console messages (for debugging)
|
|
*/
|
|
export function enableConsoleLogging(page: Page): void {
|
|
page.on('console', msg => {
|
|
if (msg.type() !== 'log') return;
|
|
console.log(`BROWSER: ${msg.text()}`);
|
|
});
|
|
page.on('pageerror', err => console.log(`ERROR: ${err.message}`));
|
|
}
|
|
|
|
/**
|
|
* Take named screenshot
|
|
*/
|
|
export async function screenshot(page: Page, name: string): Promise<void> {
|
|
await page.screenshot({
|
|
path: `screenshots/${name}.png`,
|
|
quality: 50,
|
|
scale: 'css',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify error message is displayed
|
|
*/
|
|
export async function expectErrorMessage(page: Page, pattern: RegExp | string): Promise<void> {
|
|
if (typeof pattern === 'string') {
|
|
await expect(page.getByText(pattern)).toBeVisible({ timeout: 5000 });
|
|
} else {
|
|
await expect(page.getByText(pattern)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for toast/notification
|
|
*/
|
|
export async function waitForToast(page: Page, text: string | RegExp): Promise<void> {
|
|
await expect(page.getByText(text)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Check API response in network
|
|
*/
|
|
export async function interceptApiCall(
|
|
page: Page,
|
|
urlPattern: string | RegExp,
|
|
callback: (response: { status: number; body: unknown }) => void
|
|
): Promise<void> {
|
|
page.on('response', async response => {
|
|
if (response.url().match(urlPattern)) {
|
|
const body = await response.json().catch(() => null);
|
|
callback({ status: response.status(), body });
|
|
}
|
|
});
|
|
}
|