diff --git a/frontend/e2e/utils/helpers.ts b/frontend/e2e/utils/helpers.ts new file mode 100644 index 00000000..88cbed63 --- /dev/null +++ b/frontend/e2e/utils/helpers.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + const locator = page.locator(selector); + return locator.textContent() || ''; +} + +/** + * Get input value + */ +export async function getInputValue(page: Page, selector: string): Promise { + 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 { + const locator = page.locator(selector); + return locator.isVisible(); +} + +/** + * Check if element is enabled + */ +export async function isElementEnabled(page: Page, selector: string): Promise { + const locator = page.locator(selector); + return locator.isEnabled(); +} + +/** + * Select dropdown option by text + */ +export async function selectOption( + page: Page, + selector: string, + optionText: string +): Promise { + 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 { + 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> { + const cells = page.locator(`${rowSelector} [data-testid*="cell"]`); + const count = await cells.count(); + const data: Record = {}; + + 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 { + const locator = page.locator(selector); + await locator.hover(); +} + +/** + * Double-click element + */ +export async function doubleClickElement(page: Page, selector: string): Promise { + const locator = page.locator(selector); + await locator.dblclick(); +} + +/** + * Right-click element + */ +export async function rightClickElement(page: Page, selector: string): Promise { + const locator = page.locator(selector); + await locator.click({ button: 'right' }); +} + +/** + * Scroll element into view + */ +export async function scrollIntoView(page: Page, selector: string): Promise { + const locator = page.locator(selector); + await locator.scrollIntoViewIfNeeded(); +} + +/** + * Get number of elements matching selector + */ +export async function getElementCount(page: Page, selector: string): Promise { + const locator = page.locator(selector); + return locator.count(); +} + +/** + * Wait for specific network condition + */ +export async function waitForNetworkIdle( + page: Page, + timeout: number = 5000 +): Promise { + await page.waitForLoadState('networkidle', { timeout }); +} + +/** + * Extract all text from selector and split by newlines + */ +export async function getAllText(page: Page, selector: string): Promise { + 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 { + return isElementVisible(page, '[data-testid="toast-error"]'); +} + +/** + * Check for success message + */ +export async function hasSuccessMessage(page: Page): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + return page.evaluate((k) => localStorage.getItem(k), key); +} + +/** + * Set localStorage data + */ +export async function setLocalStorage(page: Page, key: string, value: string): Promise { + await page.evaluate((k, v) => localStorage.setItem(k, v), key, value); +} + +/** + * Clear localStorage + */ +export async function clearLocalStorage(page: Page): Promise { + await page.evaluate(() => localStorage.clear()); +} + +/** + * Get URL pathname + */ +export async function getCurrentPath(page: Page): Promise { + 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 { + 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 { + 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), + }); + } + }); +}