feat: create helper utilities for e2e test navigation and actions
This commit is contained in:
343
frontend/e2e/utils/helpers.ts
Normal file
343
frontend/e2e/utils/helpers.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user