178 lines
4.4 KiB
TypeScript
178 lines
4.4 KiB
TypeScript
|
|
import { Page, expect } from '@playwright/test';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* General utility functions for Playwright tests
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Waits for the loading spinner to disappear
|
||
|
|
*/
|
||
|
|
export async function waitForLoadingToFinish(page: Page) {
|
||
|
|
// Adjust selectors based on your loading indicators
|
||
|
|
await page.waitForSelector('[data-testid="loading"], .loading, .spinner', {
|
||
|
|
state: 'hidden',
|
||
|
|
timeout: 10000,
|
||
|
|
}).catch(() => {
|
||
|
|
// If no loading indicator found, that's fine
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Waits for an API call to complete
|
||
|
|
*/
|
||
|
|
export async function waitForApiCall(page: Page, urlPattern: string | RegExp) {
|
||
|
|
return page.waitForResponse(
|
||
|
|
(response) => {
|
||
|
|
const url = response.url();
|
||
|
|
if (typeof urlPattern === 'string') {
|
||
|
|
return url.includes(urlPattern);
|
||
|
|
}
|
||
|
|
return urlPattern.test(url);
|
||
|
|
},
|
||
|
|
{ timeout: 15000 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Mocks an API endpoint with a custom response
|
||
|
|
*/
|
||
|
|
export async function mockApiResponse(
|
||
|
|
page: Page,
|
||
|
|
urlPattern: string | RegExp,
|
||
|
|
response: any,
|
||
|
|
statusCode: number = 200
|
||
|
|
) {
|
||
|
|
await page.route(urlPattern, async (route) => {
|
||
|
|
await route.fulfill({
|
||
|
|
status: statusCode,
|
||
|
|
contentType: 'application/json',
|
||
|
|
body: JSON.stringify(response),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if a toast/notification is visible
|
||
|
|
*/
|
||
|
|
export async function expectToastMessage(page: Page, message: string | RegExp) {
|
||
|
|
// Adjust selector based on your toast implementation
|
||
|
|
const toast = page.locator('[role="alert"], .toast, .notification');
|
||
|
|
await expect(toast).toContainText(message, { timeout: 5000 });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fills a form field by label
|
||
|
|
*/
|
||
|
|
export async function fillFormField(page: Page, label: string | RegExp, value: string) {
|
||
|
|
await page.getByLabel(label).fill(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Selects an option from a dropdown by label
|
||
|
|
*/
|
||
|
|
export async function selectDropdownOption(page: Page, label: string | RegExp, value: string) {
|
||
|
|
await page.getByLabel(label).selectOption(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Uploads a file to a file input
|
||
|
|
*/
|
||
|
|
export async function uploadFile(page: Page, inputSelector: string, filePath: string) {
|
||
|
|
const fileInput = page.locator(inputSelector);
|
||
|
|
await fileInput.setInputFiles(filePath);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scrolls an element into view
|
||
|
|
*/
|
||
|
|
export async function scrollIntoView(page: Page, selector: string) {
|
||
|
|
await page.locator(selector).scrollIntoViewIfNeeded();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Takes a screenshot with a custom name
|
||
|
|
*/
|
||
|
|
export async function takeScreenshot(page: Page, name: string) {
|
||
|
|
await page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Waits for navigation to complete
|
||
|
|
*/
|
||
|
|
export async function waitForNavigation(page: Page, urlPattern?: string | RegExp) {
|
||
|
|
if (urlPattern) {
|
||
|
|
await page.waitForURL(urlPattern);
|
||
|
|
} else {
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates a unique test identifier
|
||
|
|
*/
|
||
|
|
export function generateTestId(prefix: string = 'test'): string {
|
||
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Waits for a specific amount of time (use sparingly, prefer waitFor* methods)
|
||
|
|
*/
|
||
|
|
export async function wait(ms: number) {
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Retries an action until it succeeds or max attempts reached
|
||
|
|
*/
|
||
|
|
export async function retryAction<T>(
|
||
|
|
action: () => Promise<T>,
|
||
|
|
maxAttempts: number = 3,
|
||
|
|
delayMs: number = 1000
|
||
|
|
): Promise<T> {
|
||
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
|
|
try {
|
||
|
|
return await action();
|
||
|
|
} catch (error) {
|
||
|
|
if (attempt === maxAttempts) {
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
await wait(delayMs);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new Error('Retry action failed');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if an element is visible on the page
|
||
|
|
*/
|
||
|
|
export async function isVisible(page: Page, selector: string): Promise<boolean> {
|
||
|
|
try {
|
||
|
|
await page.locator(selector).waitFor({ state: 'visible', timeout: 2000 });
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Accepts cookie consent dialog if present
|
||
|
|
* Should be called after navigating to a page
|
||
|
|
*/
|
||
|
|
export async function acceptCookieConsent(page: Page) {
|
||
|
|
try {
|
||
|
|
const acceptButton = page.getByRole('button', {
|
||
|
|
name: /aceptar todas|accept all/i,
|
||
|
|
});
|
||
|
|
|
||
|
|
const isVisible = await acceptButton.isVisible({ timeout: 2000 }).catch(() => false);
|
||
|
|
|
||
|
|
if (isVisible) {
|
||
|
|
await acceptButton.click();
|
||
|
|
// Wait a bit for the dialog to close
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Cookie dialog not present, that's fine
|
||
|
|
}
|
||
|
|
}
|