lotr-sut/tests/e2e/pages/BasePage.ts
Fellowship Scholar f6a5823439 init commit
2026-03-29 20:07:56 +00:00

252 lines
7.0 KiB
TypeScript

import { Page, expect, Locator } from '@playwright/test';
/**
* Base Page Object
* All page objects inherit from this to ensure consistent locator strategy
* and navigation patterns
*/
export class BasePage {
protected page: Page;
protected baseUrl: string;
protected defaultPollInterval = 500; // Poll every 500ms
protected defaultPollTimeout = 30000; // Poll for up to 30s
constructor(page: Page, baseUrl: string = process.env.SUT_URL || 'http://localhost') {
this.page = page;
this.baseUrl = baseUrl;
}
/**
* Navigate to a specific path
*/
async goto(path: string): Promise<void> {
const url = `${this.baseUrl}${path}`;
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
// Don't wait for networkidle - gives time for async data loading
// Instead wait a bit for React to mount and initial renders
await this.page.waitForTimeout(500);
}
/**
* Get current URL path
*/
async getUrlPath(): Promise<string> {
return new URL(this.page.url()).pathname;
}
/**
* Wait for URL to match pattern
*/
async waitForUrl(urlPattern: string | RegExp, timeout: number = 30000): Promise<void> {
await this.page.waitForURL(urlPattern, { timeout });
}
/**
* Check if page is loaded by verifying load event
* Uses 'load' instead of 'networkidle' to avoid timeouts with async data loading
*/
async isLoaded(): Promise<boolean> {
try {
// Wait for 'load' event which fires after initial page and resources are loaded
// This is faster than 'networkidle' and more reliable with continuously-loading apps
await this.page.waitForLoadState('load', { timeout: 5000 });
return true;
} catch {
// If load event doesn't fire, assume page is loaded since goto() already waited for domcontentloaded
return true;
}
}
/**
* Wait for element with auto-waiting
*/
async waitForElement(selector: string, timeout: number = 10000): Promise<void> {
await this.page.locator(selector).first().waitFor({ state: 'visible', timeout });
}
/**
* Click element by role (preferred method)
*/
async clickByRole(
role: string,
options?: { name?: string | RegExp; exact?: boolean },
): Promise<void> {
await this.page.getByRole(role as any, options).first().click();
}
/**
* Click element by test id
*/
async clickByTestId(testId: string): Promise<void> {
await this.page.getByTestId(testId).first().click();
}
/**
* Fill text input
*/
async fillInput(selector: string, value: string): Promise<void> {
await this.page.locator(selector).first().fill(value);
}
/**
* Get element text content
*/
async getElementText(selector: string): Promise<string> {
const text = await this.page.locator(selector).first().textContent();
return text || '';
}
/**
* Check if element is visible
*/
async isElementVisible(selector: string, timeout: number = 5000): Promise<boolean> {
try {
await this.page.locator(selector).first().isVisible();
return true;
} catch {
return false;
}
}
/**
* Wait and verify element text
*/
async expectElementToHaveText(selector: string, text: string | RegExp): Promise<void> {
await expect(this.page.locator(selector).first()).toContainText(text);
}
/**
* Dismiss alert via keyboard
*/
async dismissAlert(): Promise<void> {
try {
await this.page.keyboard.press('Escape');
} catch {
// Alert may not be present
}
}
/**
* Actively poll for element visibility with retries
* Useful for elements that appear after async operations
*/
async pollForElementVisible(
locator: Locator | string,
timeout: number = this.defaultPollTimeout,
pollInterval: number = this.defaultPollInterval,
): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const isVisible = await element.isVisible().catch(() => false);
if (isVisible) {
return;
}
} catch {
// Element doesn't exist yet, continue polling
}
await this.page.waitForTimeout(pollInterval);
}
throw new Error(
`Element did not become visible within ${timeout}ms. Polling interval: ${pollInterval}ms`,
);
}
/**
* Actively poll for element to contain text
* Useful for dynamic content that updates after async operations
*/
async pollForElementText(
locator: Locator | string,
expectedText: string | RegExp,
timeout: number = this.defaultPollTimeout,
pollInterval: number = this.defaultPollInterval,
): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const text = await element.textContent().catch(() => '');
if (text) {
if (typeof expectedText === 'string') {
if (text.includes(expectedText)) {
return;
}
} else {
if (expectedText.test(text)) {
return;
}
}
}
} catch {
// Element doesn't exist yet, continue polling
}
await this.page.waitForTimeout(pollInterval);
}
throw new Error(
`Element text "${expectedText}" not found within ${timeout}ms. Polling interval: ${pollInterval}ms`,
);
}
/**
* Actively poll for any condition
* Generic polling helper for custom wait conditions
*/
async pollUntil(
condition: () => Promise<boolean>,
timeout: number = this.defaultPollTimeout,
pollInterval: number = this.defaultPollInterval,
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const result = await condition();
if (result) {
return;
}
} catch {
// Condition threw error, continue polling
}
await this.page.waitForTimeout(pollInterval);
}
throw new Error(`Condition not met within ${timeout}ms. Polling interval: ${pollInterval}ms`);
}
/**
* Wait for elements count to match expected count
* Useful for waiting for lists to populate
*/
async pollForElementCount(
locator: Locator | string,
expectedCount: number,
timeout: number = this.defaultPollTimeout,
pollInterval: number = this.defaultPollInterval,
): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const count = await element.count();
if (count === expectedCount) {
return;
}
} catch {
// Locator error, continue polling
}
await this.page.waitForTimeout(pollInterval);
}
throw new Error(
`Expected element count ${expectedCount} not reached within ${timeout}ms`,
);
}
}