344 lines
8.0 KiB
TypeScript
344 lines
8.0 KiB
TypeScript
import { Page, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Helper utilities for E2E tests
|
|
* Navigation, wait conditions, and common actions
|
|
*/
|
|
|
|
/**
|
|
* Navigate to page and wait for load
|
|
*/
|
|
export async function navigateTo(page: Page, url: string, waitUntil: 'load' | 'domcontentloaded' | 'networkidle' = 'networkidle'): Promise<void> {
|
|
await page.goto(url, { waitUntil });
|
|
}
|
|
|
|
/**
|
|
* Wait for element to be ready (visible and stable)
|
|
*/
|
|
export async function waitForElement(
|
|
page: Page,
|
|
selector: string,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
await expect(locator).toBeVisible({ timeout });
|
|
// Give DOM a moment to settle
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
/**
|
|
* Fill form field with clearing first
|
|
*/
|
|
export async function fillField(
|
|
page: Page,
|
|
selector: string,
|
|
value: string,
|
|
clear: boolean = true
|
|
): Promise<void> {
|
|
const field = page.locator(selector);
|
|
|
|
if (clear) {
|
|
await field.fill(''); // Clear
|
|
}
|
|
|
|
await field.fill(value);
|
|
// Trigger change event if not triggered automatically
|
|
await field.dispatchEvent('change');
|
|
}
|
|
|
|
/**
|
|
* Click element and wait for navigation
|
|
*/
|
|
export async function clickAndWait(
|
|
page: Page,
|
|
selector: string,
|
|
waitFor: 'navigation' | 'loadstate' = 'loadstate'
|
|
): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
|
|
if (waitFor === 'navigation') {
|
|
await Promise.all([
|
|
page.waitForNavigation(),
|
|
locator.click(),
|
|
]);
|
|
} else {
|
|
await locator.click();
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get text content of element
|
|
*/
|
|
export async function getText(page: Page, selector: string): Promise<string> {
|
|
const locator = page.locator(selector);
|
|
return locator.textContent() || '';
|
|
}
|
|
|
|
/**
|
|
* Get input value
|
|
*/
|
|
export async function getInputValue(page: Page, selector: string): Promise<string> {
|
|
const locator = page.locator(selector);
|
|
const value = await locator.inputValue();
|
|
return value || '';
|
|
}
|
|
|
|
/**
|
|
* Check if element is visible
|
|
*/
|
|
export async function isElementVisible(page: Page, selector: string): Promise<boolean> {
|
|
const locator = page.locator(selector);
|
|
return locator.isVisible();
|
|
}
|
|
|
|
/**
|
|
* Check if element is enabled
|
|
*/
|
|
export async function isElementEnabled(page: Page, selector: string): Promise<boolean> {
|
|
const locator = page.locator(selector);
|
|
return locator.isEnabled();
|
|
}
|
|
|
|
/**
|
|
* Select dropdown option by text
|
|
*/
|
|
export async function selectOption(
|
|
page: Page,
|
|
selector: string,
|
|
optionText: string
|
|
): Promise<void> {
|
|
const dropdown = page.locator(selector);
|
|
await dropdown.selectOption({ label: optionText });
|
|
}
|
|
|
|
/**
|
|
* Wait for table row to contain text
|
|
*/
|
|
export async function waitForTableRow(
|
|
page: Page,
|
|
tableSelector: string,
|
|
cellText: string,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
const row = page.locator(`${tableSelector} >> text=${cellText}`);
|
|
await expect(row).toBeVisible({ timeout });
|
|
}
|
|
|
|
/**
|
|
* Get row data from table
|
|
*/
|
|
export async function getTableRowData(
|
|
page: Page,
|
|
rowSelector: string
|
|
): Promise<Record<string, string>> {
|
|
const cells = page.locator(`${rowSelector} [data-testid*="cell"]`);
|
|
const count = await cells.count();
|
|
const data: Record<string, string> = {};
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const cellText = await cells.nth(i).textContent();
|
|
data[`cell_${i}`] = cellText || '';
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Hover over element
|
|
*/
|
|
export async function hoverElement(page: Page, selector: string): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
await locator.hover();
|
|
}
|
|
|
|
/**
|
|
* Double-click element
|
|
*/
|
|
export async function doubleClickElement(page: Page, selector: string): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
await locator.dblclick();
|
|
}
|
|
|
|
/**
|
|
* Right-click element
|
|
*/
|
|
export async function rightClickElement(page: Page, selector: string): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
await locator.click({ button: 'right' });
|
|
}
|
|
|
|
/**
|
|
* Scroll element into view
|
|
*/
|
|
export async function scrollIntoView(page: Page, selector: string): Promise<void> {
|
|
const locator = page.locator(selector);
|
|
await locator.scrollIntoViewIfNeeded();
|
|
}
|
|
|
|
/**
|
|
* Get number of elements matching selector
|
|
*/
|
|
export async function getElementCount(page: Page, selector: string): Promise<number> {
|
|
const locator = page.locator(selector);
|
|
return locator.count();
|
|
}
|
|
|
|
/**
|
|
* Wait for specific network condition
|
|
*/
|
|
export async function waitForNetworkIdle(
|
|
page: Page,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
await page.waitForLoadState('networkidle', { timeout });
|
|
}
|
|
|
|
/**
|
|
* Extract all text from selector and split by newlines
|
|
*/
|
|
export async function getAllText(page: Page, selector: string): Promise<string[]> {
|
|
const locator = page.locator(selector);
|
|
const count = await locator.count();
|
|
const texts: string[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const text = await locator.nth(i).textContent();
|
|
if (text) {
|
|
texts.push(text.trim());
|
|
}
|
|
}
|
|
|
|
return texts;
|
|
}
|
|
|
|
/**
|
|
* Check for error/success messages
|
|
*/
|
|
export async function hasErrorMessage(page: Page): Promise<boolean> {
|
|
return isElementVisible(page, '[data-testid="toast-error"]');
|
|
}
|
|
|
|
/**
|
|
* Check for success message
|
|
*/
|
|
export async function hasSuccessMessage(page: Page): Promise<boolean> {
|
|
return isElementVisible(page, '[data-testid="toast-success"]');
|
|
}
|
|
|
|
/**
|
|
* Wait for success message to appear and disappear
|
|
*/
|
|
export async function waitForSuccessAndDismiss(
|
|
page: Page,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
const successToast = page.locator('[data-testid="toast-success"]');
|
|
await expect(successToast).toBeVisible({ timeout });
|
|
await expect(successToast).not.toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Clear input field completely
|
|
*/
|
|
export async function clearField(page: Page, selector: string): Promise<void> {
|
|
const field = page.locator(selector);
|
|
await field.focus();
|
|
await field.press('Control+A');
|
|
await field.press('Backspace');
|
|
}
|
|
|
|
/**
|
|
* Type text slowly (for OCR testing)
|
|
*/
|
|
export async function typeSlowly(
|
|
page: Page,
|
|
selector: string,
|
|
text: string,
|
|
delayMs: number = 50
|
|
): Promise<void> {
|
|
const field = page.locator(selector);
|
|
await field.focus();
|
|
|
|
for (const char of text) {
|
|
await field.type(char);
|
|
await page.waitForTimeout(delayMs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Take screenshot with timestamp
|
|
*/
|
|
export async function takeScreenshot(page: Page, name: string): Promise<void> {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
await page.screenshot({ path: `screenshots/${name}-${timestamp}.png` });
|
|
}
|
|
|
|
/**
|
|
* Get localStorage data
|
|
*/
|
|
export async function getLocalStorage(page: Page, key: string): Promise<string | null> {
|
|
return page.evaluate((k) => localStorage.getItem(k), key);
|
|
}
|
|
|
|
/**
|
|
* Set localStorage data
|
|
*/
|
|
export async function setLocalStorage(page: Page, key: string, value: string): Promise<void> {
|
|
await page.evaluate((k, v) => localStorage.setItem(k, v), key, value);
|
|
}
|
|
|
|
/**
|
|
* Clear localStorage
|
|
*/
|
|
export async function clearLocalStorage(page: Page): Promise<void> {
|
|
await page.evaluate(() => localStorage.clear());
|
|
}
|
|
|
|
/**
|
|
* Get URL pathname
|
|
*/
|
|
export async function getCurrentPath(page: Page): Promise<string> {
|
|
return page.evaluate(() => window.location.pathname);
|
|
}
|
|
|
|
/**
|
|
* Wait for URL to match pattern
|
|
*/
|
|
export async function waitForUrl(
|
|
page: Page,
|
|
urlPattern: RegExp | string,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
if (typeof urlPattern === 'string') {
|
|
await expect(page).toHaveURL(new RegExp(urlPattern), { timeout });
|
|
} else {
|
|
await expect(page).toHaveURL(urlPattern, { timeout });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock API response
|
|
*/
|
|
export async function mockApiResponse(
|
|
page: Page,
|
|
urlPattern: RegExp | string,
|
|
responseData: any,
|
|
status: number = 200
|
|
): Promise<void> {
|
|
await page.route(urlPattern, (route) => {
|
|
route.abort('blockedbyresponse');
|
|
route.continue();
|
|
});
|
|
|
|
await page.on('route', (route) => {
|
|
if (route.request().url().match(urlPattern)) {
|
|
route.respond({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(responseData),
|
|
});
|
|
}
|
|
});
|
|
}
|