ee044d03e0
- Add /health endpoint with anonymous access for monitoring - Add FileUploadResult<T> model and PostMultipartForFileResultAsync for proper upload response handling - Add ApiResult.Success() factory method for interface types - Refactor Login.razor for cleaner code - Add comprehensive Playwright E2E test suite with fixtures and helpers
260 lines
8.4 KiB
TypeScript
260 lines
8.4 KiB
TypeScript
import { Page, expect, Locator } from '@playwright/test';
|
|
|
|
/**
|
|
* Notification types in Radzen
|
|
*/
|
|
export type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
|
|
|
/**
|
|
* Badge style types matching Radzen badge styles
|
|
*/
|
|
export type BadgeStyle = 'danger' | 'success' | 'info' | 'warning' | 'primary' | 'secondary';
|
|
|
|
/**
|
|
* Wait for a notification of a specific type to appear
|
|
* @param page - Playwright page object
|
|
* @param type - The notification type to wait for
|
|
* @param timeout - Maximum time to wait (default: 5000ms)
|
|
*/
|
|
export async function waitForNotification(
|
|
page: Page,
|
|
type: NotificationType,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
const notificationClass = `.rz-notification-${type}`;
|
|
await page.locator(notificationClass).waitFor({ state: 'visible', timeout });
|
|
}
|
|
|
|
/**
|
|
* Wait for any notification to appear
|
|
* @param page - Playwright page object
|
|
* @param timeout - Maximum time to wait (default: 5000ms)
|
|
*/
|
|
export async function waitForAnyNotification(
|
|
page: Page,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
await page.locator('.rz-notification').waitFor({ state: 'visible', timeout });
|
|
}
|
|
|
|
/**
|
|
* Check if an error notification is visible
|
|
* @param page - Playwright page object
|
|
* @returns true if error notification is visible
|
|
*/
|
|
export async function hasErrorNotification(page: Page): Promise<boolean> {
|
|
return await page.locator('.rz-notification-error').isVisible({ timeout: 2000 }).catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Check if a success notification is visible
|
|
* @param page - Playwright page object
|
|
* @returns true if success notification is visible
|
|
*/
|
|
export async function hasSuccessNotification(page: Page): Promise<boolean> {
|
|
return await page.locator('.rz-notification-success').isVisible({ timeout: 2000 }).catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Assert that no error notification is visible
|
|
* @param page - Playwright page object
|
|
* @param timeout - Time to wait before asserting (default: 5000ms)
|
|
*/
|
|
export async function assertNoErrorNotification(page: Page, timeout: number = 5000): Promise<void> {
|
|
const errorNotification = page.locator('.rz-notification-error');
|
|
await expect(errorNotification).not.toBeVisible({ timeout });
|
|
}
|
|
|
|
/**
|
|
* Get the row count from a data grid
|
|
* @param page - Playwright page object
|
|
* @param gridLocator - Locator for the specific grid (or defaults to first grid)
|
|
* @returns Number of rows in the grid body
|
|
*/
|
|
export async function getDataGridRowCount(
|
|
page: Page,
|
|
gridLocator?: Locator
|
|
): Promise<number> {
|
|
const grid = gridLocator ?? page.locator('.rz-data-grid').first();
|
|
|
|
// Check for "No records" message
|
|
const noRecords = grid.locator('text=No records to display');
|
|
if (await noRecords.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
return 0;
|
|
}
|
|
|
|
const rows = grid.locator('tbody tr');
|
|
return await rows.count();
|
|
}
|
|
|
|
/**
|
|
* Double-click a row in a data grid
|
|
* @param page - Playwright page object
|
|
* @param rowIndex - Zero-based index of the row to double-click
|
|
* @param gridLocator - Locator for the specific grid (or defaults to first grid)
|
|
*/
|
|
export async function doubleClickDataGridRow(
|
|
page: Page,
|
|
rowIndex: number,
|
|
gridLocator?: Locator
|
|
): Promise<void> {
|
|
const grid = gridLocator ?? page.locator('.rz-data-grid').first();
|
|
const rows = grid.locator('tbody tr');
|
|
await rows.nth(rowIndex).dblclick();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
/**
|
|
* Click a row in a data grid
|
|
* @param page - Playwright page object
|
|
* @param rowIndex - Zero-based index of the row to click
|
|
* @param gridLocator - Locator for the specific grid (or defaults to first grid)
|
|
*/
|
|
export async function clickDataGridRow(
|
|
page: Page,
|
|
rowIndex: number,
|
|
gridLocator?: Locator
|
|
): Promise<void> {
|
|
const grid = gridLocator ?? page.locator('.rz-data-grid').first();
|
|
const rows = grid.locator('tbody tr');
|
|
await rows.nth(rowIndex).click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Get text content from a specific cell in a data grid
|
|
* @param page - Playwright page object
|
|
* @param rowIndex - Zero-based row index
|
|
* @param columnIndex - Zero-based column index
|
|
* @param gridLocator - Locator for the specific grid (or defaults to first grid)
|
|
* @returns Cell text content
|
|
*/
|
|
export async function getDataGridCellText(
|
|
page: Page,
|
|
rowIndex: number,
|
|
columnIndex: number,
|
|
gridLocator?: Locator
|
|
): Promise<string> {
|
|
const grid = gridLocator ?? page.locator('.rz-data-grid').first();
|
|
const cell = grid.locator(`tbody tr:nth-child(${rowIndex + 1}) td:nth-child(${columnIndex + 1})`);
|
|
return await cell.textContent() ?? '';
|
|
}
|
|
|
|
/**
|
|
* Check if a data grid shows "No records to display"
|
|
* @param page - Playwright page object
|
|
* @param gridLocator - Locator for the specific grid (or defaults to first grid)
|
|
* @returns true if no records message is visible
|
|
*/
|
|
export async function dataGridIsEmpty(
|
|
page: Page,
|
|
gridLocator?: Locator
|
|
): Promise<boolean> {
|
|
const grid = gridLocator ?? page.locator('.rz-data-grid').first();
|
|
const noRecords = grid.locator('text=No records to display');
|
|
return await noRecords.isVisible({ timeout: 2000 }).catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Wait for dialog to appear with specific title
|
|
* @param page - Playwright page object
|
|
* @param title - Dialog title text
|
|
* @param timeout - Maximum time to wait (default: 5000ms)
|
|
*/
|
|
export async function waitForDialog(
|
|
page: Page,
|
|
title: string,
|
|
timeout: number = 5000
|
|
): Promise<void> {
|
|
await page.locator(`.rz-dialog:has-text("${title}")`).waitFor({ state: 'visible', timeout });
|
|
}
|
|
|
|
/**
|
|
* Click OK button in a dialog
|
|
* @param page - Playwright page object
|
|
*/
|
|
export async function clickDialogOk(page: Page): Promise<void> {
|
|
await page.locator('.rz-dialog button:has-text("OK")').click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Click Cancel button in a dialog
|
|
* @param page - Playwright page object
|
|
*/
|
|
export async function clickDialogCancel(page: Page): Promise<void> {
|
|
await page.locator('.rz-dialog button:has-text("Cancel")').click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Confirm a dialog (waits for dialog and clicks OK)
|
|
* @param page - Playwright page object
|
|
* @param title - Dialog title to wait for
|
|
*/
|
|
export async function confirmDialog(page: Page, title: string): Promise<void> {
|
|
await waitForDialog(page, title);
|
|
await clickDialogOk(page);
|
|
}
|
|
|
|
/**
|
|
* Get the badge style from a badge element
|
|
* @param badge - Locator for the badge element
|
|
* @returns The badge style or null if not found
|
|
*/
|
|
export async function getBadgeStyle(badge: Locator): Promise<BadgeStyle | null> {
|
|
const classes = await badge.getAttribute('class') ?? '';
|
|
|
|
if (classes.includes('rz-badge-danger')) return 'danger';
|
|
if (classes.includes('rz-badge-success')) return 'success';
|
|
if (classes.includes('rz-badge-info')) return 'info';
|
|
if (classes.includes('rz-badge-warning')) return 'warning';
|
|
if (classes.includes('rz-badge-primary')) return 'primary';
|
|
if (classes.includes('rz-badge-secondary')) return 'secondary';
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if a button is disabled
|
|
* @param page - Playwright page object
|
|
* @param buttonText - Text content of the button
|
|
* @returns true if button is disabled
|
|
*/
|
|
export async function isButtonDisabled(page: Page, buttonText: string): Promise<boolean> {
|
|
const button = page.locator(`button:has-text("${buttonText}")`);
|
|
return await button.isDisabled();
|
|
}
|
|
|
|
/**
|
|
* Click a button by its text
|
|
* @param page - Playwright page object
|
|
* @param buttonText - Text content of the button
|
|
*/
|
|
export async function clickButton(page: Page, buttonText: string): Promise<void> {
|
|
await page.locator(`button:has-text("${buttonText}")`).click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Get the text content of a badge by its parent context
|
|
* @param page - Playwright page object
|
|
* @param contextText - Text near the badge to identify it
|
|
* @returns Badge text content
|
|
*/
|
|
export async function getBadgeText(page: Page, contextText: string): Promise<string> {
|
|
const badge = page.locator(`:has-text("${contextText}") .rz-badge`).first();
|
|
return await badge.textContent() ?? '';
|
|
}
|
|
|
|
/**
|
|
* Status badge style mapping (from Blazor code)
|
|
*/
|
|
export const StatusBadgeStyles: Record<string, BadgeStyle> = {
|
|
'Error': 'danger',
|
|
'Ended': 'success',
|
|
'Running': 'info',
|
|
'Queued': 'warning',
|
|
'New': 'secondary',
|
|
};
|