Migrate Playwright suite to .NET UI tests and deprecate TS project
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
// Test credentials - FakeAuthService accepts any credentials in development mode
|
||||
const DEFAULT_USERNAME = 'testuser';
|
||||
const DEFAULT_PASSWORD = 'testpass';
|
||||
|
||||
/**
|
||||
* Login to the application using FakeAuthService credentials
|
||||
* @param page - Playwright page object
|
||||
* @param username - Optional username (defaults to testuser)
|
||||
* @param password - Optional password (defaults to testpass)
|
||||
*/
|
||||
export async function login(page: Page, username?: string, password?: string): Promise<void> {
|
||||
const user = username ?? DEFAULT_USERNAME;
|
||||
const pass = password ?? DEFAULT_PASSWORD;
|
||||
|
||||
// Check if we're on the login page
|
||||
const loginForm = page.locator('text=Authentication Required');
|
||||
if (await loginForm.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
// Wait for form inputs to be ready
|
||||
await page.locator('input[name="Username"]').waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
// Fill credentials
|
||||
await page.locator('input[name="Username"]').fill(user);
|
||||
await page.locator('input[name="Password"]').fill(pass);
|
||||
|
||||
// Click the submit button
|
||||
await page.locator('button[type="submit"]:has-text("LOGIN")').click();
|
||||
|
||||
// Wait for login to process
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// After login, force navigation to refresh state
|
||||
await page.goto('/search');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
|
||||
// Verify login succeeded
|
||||
if (await loginForm.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
throw new Error('Login failed - still on login page after attempt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from the application
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function logout(page: Page): Promise<void> {
|
||||
// Click the logout button in the header
|
||||
const logoutButton = page.locator('button:has-text("Logout")');
|
||||
if (await logoutButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await logoutButton.click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently logged in
|
||||
* @param page - Playwright page object
|
||||
* @returns true if logged in, false otherwise
|
||||
*/
|
||||
export async function isLoggedIn(page: Page): Promise<boolean> {
|
||||
const loginForm = page.locator('text=Authentication Required');
|
||||
const logoutButton = page.locator('button:has-text("Logout")');
|
||||
|
||||
// If logout button is visible, user is logged in
|
||||
if (await logoutButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If login form is visible, user is not logged in
|
||||
if (await loginForm.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default to not logged in
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in, performing login if necessary
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function ensureLoggedIn(page: Page): Promise<void> {
|
||||
if (!(await isLoggedIn(page))) {
|
||||
await login(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Configuration for autocomplete filter panels
|
||||
*/
|
||||
export interface AutocompleteConfig {
|
||||
/** Panel header text to identify the panel */
|
||||
panelHeader: string;
|
||||
/** Minimum characters to trigger search */
|
||||
minSearchLength: number;
|
||||
/** Column to check for duplicates */
|
||||
keyColumn: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profit Center autocomplete configuration
|
||||
*/
|
||||
export const profitCenterConfig: AutocompleteConfig = {
|
||||
panelHeader: 'Filter by Profit Center',
|
||||
minSearchLength: 3,
|
||||
keyColumn: 'Code',
|
||||
};
|
||||
|
||||
/**
|
||||
* Work Center autocomplete configuration
|
||||
*/
|
||||
export const workCenterConfig: AutocompleteConfig = {
|
||||
panelHeader: 'Filter by Work Center',
|
||||
minSearchLength: 3,
|
||||
keyColumn: 'Work Center',
|
||||
};
|
||||
|
||||
/**
|
||||
* Operator autocomplete configuration
|
||||
*/
|
||||
export const operatorConfig: AutocompleteConfig = {
|
||||
panelHeader: 'Filter by Operator',
|
||||
minSearchLength: 3,
|
||||
keyColumn: 'User ID',
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an item via autocomplete search
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
* @param searchText - Text to search for (must be >= minSearchLength)
|
||||
*/
|
||||
export async function addAutocompleteItem(
|
||||
page: Page,
|
||||
config: AutocompleteConfig,
|
||||
searchText: string
|
||||
): Promise<void> {
|
||||
// Find the panel by its header
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
|
||||
// Find the autocomplete input within the panel
|
||||
const autocomplete = panel.locator('.rz-autocomplete input');
|
||||
|
||||
// Clear and type the search text
|
||||
await autocomplete.clear();
|
||||
await autocomplete.fill(searchText);
|
||||
|
||||
// Wait for search results to appear
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Click the first matching result in the dropdown
|
||||
const dropdown = page.locator('.rz-autocomplete-list');
|
||||
if (await dropdown.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await dropdown.locator('.rz-autocomplete-list-item').first().click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
// Click the Add button
|
||||
await panel.locator('button:has-text("Add")').click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple items via autocomplete
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
* @param searchTexts - Array of search texts
|
||||
*/
|
||||
export async function addMultipleAutocompleteItems(
|
||||
page: Page,
|
||||
config: AutocompleteConfig,
|
||||
searchTexts: string[]
|
||||
): Promise<void> {
|
||||
for (const text of searchTexts) {
|
||||
await addAutocompleteItem(page, config, text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the autocomplete list by clicking its delete button
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
* @param rowIndex - The zero-based row index to remove
|
||||
*/
|
||||
export async function removeAutocompleteItem(
|
||||
page: Page,
|
||||
config: AutocompleteConfig,
|
||||
rowIndex: number
|
||||
): Promise<void> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
const grid = panel.locator('.rz-data-grid');
|
||||
const rows = grid.locator('tbody tr');
|
||||
|
||||
// Click the delete button in the specified row
|
||||
await rows.nth(rowIndex).locator('button:has-text("Delete")').click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all items from the autocomplete panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
*/
|
||||
export async function clearAutocompleteItems(
|
||||
page: Page,
|
||||
config: AutocompleteConfig
|
||||
): Promise<void> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
const clearButton = panel.locator('button:has-text("Clear Data")');
|
||||
|
||||
if (await clearButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await clearButton.click();
|
||||
|
||||
// Wait for and confirm the dialog
|
||||
await page.waitForSelector('text=Confirm Clear', { timeout: 5000 });
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of items in the autocomplete panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
* @returns Number of items in the grid
|
||||
*/
|
||||
export async function getAutocompleteItemCount(
|
||||
page: Page,
|
||||
config: AutocompleteConfig
|
||||
): Promise<number> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
const grid = panel.locator('.rz-data-grid');
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the autocomplete panel is visible
|
||||
* @param page - Playwright page object
|
||||
* @param config - Autocomplete configuration
|
||||
* @returns true if panel is visible
|
||||
*/
|
||||
export async function isAutocompletePanelVisible(
|
||||
page: Page,
|
||||
config: AutocompleteConfig
|
||||
): Promise<boolean> {
|
||||
const panel = page.locator(`text=${config.panelHeader}`);
|
||||
return await panel.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a profit center
|
||||
* @param page - Playwright page object
|
||||
* @param code - Profit center code (e.g., "1AM")
|
||||
*/
|
||||
export async function addProfitCenter(page: Page, code: string): Promise<void> {
|
||||
await addAutocompleteItem(page, profitCenterConfig, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple profit centers
|
||||
* @param page - Playwright page object
|
||||
* @param codes - Array of profit center codes
|
||||
*/
|
||||
export async function addProfitCenters(page: Page, codes: string[]): Promise<void> {
|
||||
await addMultipleAutocompleteItems(page, profitCenterConfig, codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a work center
|
||||
* @param page - Playwright page object
|
||||
* @param code - Work center code
|
||||
*/
|
||||
export async function addWorkCenter(page: Page, code: string): Promise<void> {
|
||||
await addAutocompleteItem(page, workCenterConfig, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple work centers
|
||||
* @param page - Playwright page object
|
||||
* @param codes - Array of work center codes
|
||||
*/
|
||||
export async function addWorkCenters(page: Page, codes: string[]): Promise<void> {
|
||||
await addMultipleAutocompleteItems(page, workCenterConfig, codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an operator
|
||||
* @param page - Playwright page object
|
||||
* @param userId - Operator user ID (e.g., "ADAMSSN")
|
||||
*/
|
||||
export async function addOperator(page: Page, userId: string): Promise<void> {
|
||||
await addAutocompleteItem(page, operatorConfig, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple operators
|
||||
* @param page - Playwright page object
|
||||
* @param userIds - Array of operator user IDs
|
||||
*/
|
||||
export async function addOperators(page: Page, userIds: string[]): Promise<void> {
|
||||
await addMultipleAutocompleteItems(page, operatorConfig, userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common test data for autocomplete panels
|
||||
*/
|
||||
export const TestAutocompleteData = {
|
||||
profitCenters: ['1AM', '1BM', '1CM', '1PM', '2DM', '2SM', '3TM', '4IM', '5SM'],
|
||||
workCenters: ['WC001', 'WC002', 'WC003'],
|
||||
operators: [
|
||||
'ADAMSSN', 'AGNEWA', 'AGNEWL', 'ALASMARB', 'ALEXIUCG',
|
||||
'ALLENHY', 'ALLENNI', 'ALURUM', 'ALVESM1', 'APONTEVE',
|
||||
],
|
||||
} as const;
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Date picker panel identifiers
|
||||
*/
|
||||
export const DatePickerPanels = {
|
||||
MIN_DATE: 'MinimumDt',
|
||||
MAX_DATE: 'MaximumDt',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Set the minimum date in the TimeSpan filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param date - Date string in YYYY-MM-DD format or Date object
|
||||
*/
|
||||
export async function setMinDate(page: Page, date: string | Date): Promise<void> {
|
||||
const dateStr = formatDateForInput(date);
|
||||
const dateInput = page.locator('input[name="MinimumDt"]');
|
||||
await dateInput.fill(dateStr);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum date in the TimeSpan filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param date - Date string in YYYY-MM-DD format or Date object
|
||||
*/
|
||||
export async function setMaxDate(page: Page, date: string | Date): Promise<void> {
|
||||
const dateStr = formatDateForInput(date);
|
||||
const dateInput = page.locator('input[name="MaximumDt"]');
|
||||
await dateInput.fill(dateStr);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set both minimum and maximum dates in the TimeSpan filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param minDate - Minimum date string in YYYY-MM-DD format or Date object
|
||||
* @param maxDate - Maximum date string in YYYY-MM-DD format or Date object
|
||||
*/
|
||||
export async function setDateRange(page: Page, minDate: string | Date, maxDate: string | Date): Promise<void> {
|
||||
await setMinDate(page, minDate);
|
||||
await setMaxDate(page, maxDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current minimum date value
|
||||
* @param page - Playwright page object
|
||||
* @returns The date value as a string
|
||||
*/
|
||||
export async function getMinDate(page: Page): Promise<string> {
|
||||
const dateInput = page.locator('input[name="MinimumDt"]');
|
||||
return await dateInput.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current maximum date value
|
||||
* @param page - Playwright page object
|
||||
* @returns The date value as a string
|
||||
*/
|
||||
export async function getMaxDate(page: Page): Promise<string> {
|
||||
const dateInput = page.locator('input[name="MaximumDt"]');
|
||||
return await dateInput.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the minimum date field
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function clearMinDate(page: Page): Promise<void> {
|
||||
const dateInput = page.locator('input[name="MinimumDt"]');
|
||||
await dateInput.clear();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the maximum date field
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function clearMaxDate(page: Page): Promise<void> {
|
||||
const dateInput = page.locator('input[name="MaximumDt"]');
|
||||
await dateInput.clear();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear both date fields
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function clearDateRange(page: Page): Promise<void> {
|
||||
await clearMinDate(page);
|
||||
await clearMaxDate(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the TimeSpan filter panel is visible
|
||||
* @param page - Playwright page object
|
||||
* @returns true if visible
|
||||
*/
|
||||
export async function isTimeSpanPanelVisible(page: Page): Promise<boolean> {
|
||||
const panel = page.locator('text=Filter by Time Span');
|
||||
return await panel.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for input into the date picker
|
||||
* @param date - Date string or Date object
|
||||
* @returns Date string in MM/DD/YYYY format (Radzen date picker format)
|
||||
*/
|
||||
function formatDateForInput(date: string | Date): string {
|
||||
let d: Date;
|
||||
|
||||
if (typeof date === 'string') {
|
||||
// Parse YYYY-MM-DD format
|
||||
const parts = date.split('-');
|
||||
if (parts.length === 3) {
|
||||
d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||
} else {
|
||||
d = new Date(date);
|
||||
}
|
||||
} else {
|
||||
d = date;
|
||||
}
|
||||
|
||||
// Format as MM/DD/YYYY for Radzen DatePicker
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
|
||||
return `${month}/${day}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common date ranges for testing
|
||||
*/
|
||||
export const TestDateRanges = {
|
||||
/** Recent date range: 2020-01-01 to 2020-09-01 */
|
||||
RECENT: {
|
||||
min: '2020-01-01',
|
||||
max: '2020-09-01',
|
||||
},
|
||||
/** Mid-range dates: 2018-01-01 to 2019-12-31 */
|
||||
MID_RANGE: {
|
||||
min: '2018-01-01',
|
||||
max: '2019-12-31',
|
||||
},
|
||||
/** Historical date range: 2016-01-01 to 2017-12-31 */
|
||||
HISTORICAL: {
|
||||
min: '2016-01-01',
|
||||
max: '2017-12-31',
|
||||
},
|
||||
/** Same day range: 2020-06-15 */
|
||||
SAME_DAY: {
|
||||
min: '2020-06-15',
|
||||
max: '2020-06-15',
|
||||
},
|
||||
/** Start boundary: earliest date in database */
|
||||
START_BOUNDARY: {
|
||||
min: '1905-01-20',
|
||||
max: '1905-12-31',
|
||||
},
|
||||
/** End boundary: latest date in database */
|
||||
END_BOUNDARY: {
|
||||
min: '2020-08-01',
|
||||
max: '2020-09-01',
|
||||
},
|
||||
/** Invalid: min > max (for negative testing) */
|
||||
INVALID_REVERSED: {
|
||||
min: '2020-09-01',
|
||||
max: '2020-01-01',
|
||||
},
|
||||
/** Future dates (for negative testing) */
|
||||
FUTURE: {
|
||||
min: '2025-01-01',
|
||||
max: '2025-12-31',
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Page, Download } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Configuration for file upload filter panels
|
||||
*/
|
||||
export interface FileUploadConfig {
|
||||
/** Panel header text to identify the panel */
|
||||
panelHeader: string;
|
||||
/** Template filename (for download verification) */
|
||||
templateFilename: string;
|
||||
/** Expected columns in the template */
|
||||
expectedColumns: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Work Order upload configuration
|
||||
*/
|
||||
export const workOrderConfig: FileUploadConfig = {
|
||||
panelHeader: 'Filter by Work Order',
|
||||
templateFilename: 'WorkOrderTemplate.xlsx',
|
||||
expectedColumns: ['Work Order Number'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Component Lot upload configuration
|
||||
*/
|
||||
export const componentLotConfig: FileUploadConfig = {
|
||||
panelHeader: 'Filter By Component Lot',
|
||||
templateFilename: 'ComponentLotTemplate.xlsx',
|
||||
expectedColumns: ['Lot Number', 'Item Number'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Item Number upload configuration
|
||||
*/
|
||||
export const itemNumberConfig: FileUploadConfig = {
|
||||
panelHeader: 'Filter by Item Number',
|
||||
templateFilename: 'ItemNumberTemplate.xlsx',
|
||||
expectedColumns: ['Item Number'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Part Operation (Item/Operation/MIS) upload configuration
|
||||
*/
|
||||
export const partOperationConfig: FileUploadConfig = {
|
||||
panelHeader: 'Filter By Item/Operation/MIS',
|
||||
templateFilename: 'PartOperationTemplate.xlsx',
|
||||
expectedColumns: ['Item Number', 'Operation Number', 'MIS Number', 'MIS Revision'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file to a filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
* @param filePath - Path to the file to upload
|
||||
*/
|
||||
export async function uploadFile(
|
||||
page: Page,
|
||||
config: FileUploadConfig,
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
// Find the panel by its header
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
|
||||
// Find the file input (RadzenUpload creates a hidden input)
|
||||
const fileInput = panel.locator('input[type="file"]');
|
||||
|
||||
// Set the file
|
||||
await fileInput.setInputFiles(filePath);
|
||||
|
||||
// Wait for upload to complete
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file using the global file input (fallback for panels without specific locators)
|
||||
* @param page - Playwright page object
|
||||
* @param filePath - Path to the file to upload
|
||||
*/
|
||||
export async function uploadFileGlobal(page: Page, filePath: string): Promise<void> {
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
await fileInput.setInputFiles(filePath);
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a template file from a filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
* @returns The Download object
|
||||
*/
|
||||
export async function downloadTemplate(
|
||||
page: Page,
|
||||
config: FileUploadConfig
|
||||
): Promise<Download> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
|
||||
// Start waiting for download before clicking
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// Click the download template button
|
||||
await panel.locator('button:has-text("Download Template")').click();
|
||||
|
||||
// Wait for download to complete
|
||||
const download = await downloadPromise;
|
||||
return download;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear uploaded data from a filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
*/
|
||||
export async function clearUploadedData(
|
||||
page: Page,
|
||||
config: FileUploadConfig
|
||||
): Promise<void> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
const clearButton = panel.locator('button:has-text("Clear Data")');
|
||||
|
||||
if (await clearButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await clearButton.click();
|
||||
|
||||
// Wait for and confirm the dialog
|
||||
await page.waitForSelector('text=Confirm Clear', { timeout: 5000 });
|
||||
// Click the Ok button inside the dialog
|
||||
await page.locator('.rz-dialog-wrapper button').getByText('Ok', { exact: true }).click();
|
||||
|
||||
// Wait for the grid to update - either "No records" appears or grid rows are removed
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Wait for dialog to close
|
||||
await page.waitForSelector('.rz-dialog-wrapper', { state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of uploaded items in a filter panel
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
* @returns Number of items in the grid
|
||||
*/
|
||||
export async function getUploadedItemCount(
|
||||
page: Page,
|
||||
config: FileUploadConfig
|
||||
): Promise<number> {
|
||||
const panel = page.locator(`.rz-card:has-text("${config.panelHeader}")`);
|
||||
const grid = panel.locator('.rz-data-grid');
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file upload panel is visible
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
* @returns true if panel is visible
|
||||
*/
|
||||
export async function isFileUploadPanelVisible(
|
||||
page: Page,
|
||||
config: FileUploadConfig
|
||||
): Promise<boolean> {
|
||||
const panel = page.locator(`text=${config.panelHeader}`);
|
||||
return await panel.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the uploaded file shows items in the grid
|
||||
* @param page - Playwright page object
|
||||
* @param config - File upload configuration
|
||||
* @returns true if grid has items
|
||||
*/
|
||||
export async function hasUploadedItems(
|
||||
page: Page,
|
||||
config: FileUploadConfig
|
||||
): Promise<boolean> {
|
||||
const count = await getUploadedItemCount(page, config);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the test data directory path
|
||||
*/
|
||||
export function getTestDataDir(): string {
|
||||
return path.join(__dirname, '..', 'test-data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to a test data file
|
||||
* @param filename - The filename in the test-data directory
|
||||
*/
|
||||
export function getTestDataPath(filename: string): string {
|
||||
return path.join(getTestDataDir(), filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard test files available in test-data directory
|
||||
*/
|
||||
export const TestFiles = {
|
||||
// Work Order files
|
||||
SINGLE_WORKORDER: 'single_workorder.xlsx',
|
||||
MULTIPLE_WORKORDERS: 'multiple_workorders.xlsx',
|
||||
|
||||
// Component Lot files
|
||||
SINGLE_LOT: 'single_lot.xlsx',
|
||||
MULTIPLE_LOTS: 'multiple_lots.xlsx',
|
||||
|
||||
// Item Number files
|
||||
SINGLE_ITEM: 'single_item.xlsx',
|
||||
MULTIPLE_ITEMS: 'multiple_items.xlsx',
|
||||
|
||||
// Part Operation files
|
||||
SINGLE_OPERATION: 'single_operation.xlsx',
|
||||
MULTIPLE_OPERATIONS: 'multiple_operations.xlsx',
|
||||
|
||||
// Invalid files (for negative testing)
|
||||
INVALID_FORMAT: 'invalid_format.txt',
|
||||
EMPTY_FILE: 'empty_file.xlsx',
|
||||
INVALID_WORKORDERS: 'invalid_workorders.xlsx',
|
||||
SPECIAL_CHARS_WORKORDERS: 'special_chars_workorders.xlsx',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get full path to a standard test file
|
||||
* @param testFile - One of the TestFiles constants
|
||||
*/
|
||||
export function getTestFile(testFile: string): string {
|
||||
return getTestDataPath(testFile);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Playwright Test Helpers for JDE Scoping Tool
|
||||
*
|
||||
* This module exports all helper functions for E2E testing.
|
||||
*/
|
||||
|
||||
// Authentication helpers
|
||||
export * from './auth.helper';
|
||||
|
||||
// Navigation helpers
|
||||
export * from './navigation.helper';
|
||||
|
||||
// Search type helpers
|
||||
export * from './search-type.helper';
|
||||
|
||||
// Date picker helpers
|
||||
export * from './date-picker.helper';
|
||||
|
||||
// Autocomplete panel helpers
|
||||
export * from './autocomplete.helper';
|
||||
|
||||
// File upload panel helpers
|
||||
export * from './file-upload.helper';
|
||||
|
||||
// Radzen component helpers
|
||||
export * from './radzen.helper';
|
||||
|
||||
// Form validation helpers
|
||||
export * from './validation.helper';
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { login } from './auth.helper';
|
||||
|
||||
/**
|
||||
* Wait for Blazor WASM application to be fully loaded
|
||||
* @param page - Playwright page object
|
||||
* @param timeout - Maximum time to wait in milliseconds (default: 120000)
|
||||
*/
|
||||
export async function waitForBlazorReady(page: Page, timeout: number = 120000): Promise<void> {
|
||||
// Wait for network to be idle (all WASM files loaded)
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
|
||||
// Wait for Radzen components or main content to be visible
|
||||
await page.locator('.rz-dropdown').or(page.locator('text=Search Details')).or(page.locator('.rz-data-grid')).first().waitFor({
|
||||
state: 'visible',
|
||||
timeout: timeout
|
||||
});
|
||||
|
||||
// Additional wait for Radzen components to fully initialize
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the search page and wait for it to be ready
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToSearchPage(page: Page): Promise<void> {
|
||||
await page.goto('/search');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific search by ID
|
||||
* @param page - Playwright page object
|
||||
* @param searchId - The ID of the search to view
|
||||
*/
|
||||
export async function navigateToSearch(page: Page, searchId: number): Promise<void> {
|
||||
await page.goto(`/search/${searchId}`);
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the searches dashboard (home page)
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToSearchesDashboard(page: Page): Promise<void> {
|
||||
await page.goto('/searches');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the search queue page
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToSearchQueue(page: Page): Promise<void> {
|
||||
await page.goto('/search/queue');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the refresh status page
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToRefreshStatus(page: Page): Promise<void> {
|
||||
await page.goto('/refresh-status');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the data sync requests page
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToDataSync(page: Page): Promise<void> {
|
||||
await page.goto('/data-sync/requests');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
await login(page);
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the login page
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function navigateToLoginPage(page: Page): Promise<void> {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a navigation link in the header
|
||||
* @param page - Playwright page object
|
||||
* @param linkText - The text of the navigation link to click
|
||||
*/
|
||||
export async function clickNavLink(page: Page, linkText: string): Promise<void> {
|
||||
await page.locator(`a:has-text("${linkText}")`).click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
||||
await waitForBlazorReady(page);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
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',
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Valid search type names as displayed in the dropdown
|
||||
*/
|
||||
export const SearchTypes = {
|
||||
WORK_ORDER: 'Work Order',
|
||||
COMPONENT_LOT: 'Component Lot',
|
||||
TIMESPAN_PROFIT_CENTER: 'Time Span + Profit Center',
|
||||
TIMESPAN_WORK_CENTER: 'Time Span + Work Center',
|
||||
TIMESPAN_OPERATOR: 'Time Span + Operator',
|
||||
TIMESPAN_PC_ITEM: 'Time Span + Profit Center + Item Number',
|
||||
TIMESPAN_PC_PARTOP: 'Time Span + Profit Center + Item/Operation/MIS',
|
||||
TIMESPAN_PC_WO_PARTOP: 'Time Span + Profit Center + Work Order + Item/Operation/MIS',
|
||||
TIMESPAN_PC_EXTRACTMIS: 'Time Span + Profit Center + Extract MIS',
|
||||
TIMESPAN_WC_ITEM: 'Time Span + Work Center + Item Number',
|
||||
TIMESPAN_WC_EXTRACTMIS: 'Time Span + Work Center + Extract MIS',
|
||||
TIMESPAN_WC_PARTOP: 'Time Span + Work Center + Item/Operation/MIS',
|
||||
TIMESPAN_WC_WO_PARTOP: 'Time Span + Work Center + Work Order + Item/Operation/MIS',
|
||||
TIMESPAN_ITEM: 'Time Span + Item Number',
|
||||
TIMESPAN_WC_OPERATOR: 'Time Span + Work Center + Operator',
|
||||
TIMESPAN_PC_OPERATOR: 'Time Span + Profit Center + Operator',
|
||||
} as const;
|
||||
|
||||
export type SearchType = typeof SearchTypes[keyof typeof SearchTypes];
|
||||
|
||||
/**
|
||||
* Search type to numeric code mapping
|
||||
*/
|
||||
export const SearchTypeCodes: Record<SearchType, number> = {
|
||||
[SearchTypes.WORK_ORDER]: 10,
|
||||
[SearchTypes.COMPONENT_LOT]: 20,
|
||||
[SearchTypes.TIMESPAN_PROFIT_CENTER]: 30,
|
||||
[SearchTypes.TIMESPAN_WORK_CENTER]: 40,
|
||||
[SearchTypes.TIMESPAN_OPERATOR]: 50,
|
||||
[SearchTypes.TIMESPAN_PC_ITEM]: 60,
|
||||
[SearchTypes.TIMESPAN_PC_PARTOP]: 70,
|
||||
[SearchTypes.TIMESPAN_PC_WO_PARTOP]: 80,
|
||||
[SearchTypes.TIMESPAN_PC_EXTRACTMIS]: 90,
|
||||
[SearchTypes.TIMESPAN_WC_ITEM]: 100,
|
||||
[SearchTypes.TIMESPAN_WC_EXTRACTMIS]: 110,
|
||||
[SearchTypes.TIMESPAN_WC_PARTOP]: 120,
|
||||
[SearchTypes.TIMESPAN_WC_WO_PARTOP]: 130,
|
||||
[SearchTypes.TIMESPAN_ITEM]: 140,
|
||||
[SearchTypes.TIMESPAN_WC_OPERATOR]: 150,
|
||||
[SearchTypes.TIMESPAN_PC_OPERATOR]: 160,
|
||||
};
|
||||
|
||||
/**
|
||||
* Select a search type from the dropdown
|
||||
* @param page - Playwright page object
|
||||
* @param searchType - The search type to select
|
||||
*/
|
||||
export async function selectSearchType(page: Page, searchType: SearchType): Promise<void> {
|
||||
// Click the search type dropdown (first dropdown on the page)
|
||||
await page.locator('.rz-dropdown').first().click();
|
||||
|
||||
// Wait for dropdown to open
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Select the option by exact text using aria-label for exact matching
|
||||
await page.getByRole('option', { name: searchType, exact: true }).click();
|
||||
|
||||
// Wait for the filter panel to update
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected search type
|
||||
* @param page - Playwright page object
|
||||
* @returns The currently selected search type text
|
||||
*/
|
||||
export async function getSelectedSearchType(page: Page): Promise<string> {
|
||||
const dropdown = page.locator('.rz-dropdown').first();
|
||||
return await dropdown.locator('.rz-dropdown-label').textContent() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the search type dropdown is disabled (for read-only searches)
|
||||
* @param page - Playwright page object
|
||||
* @returns true if dropdown is disabled
|
||||
*/
|
||||
export async function isSearchTypeDropdownDisabled(page: Page): Promise<boolean> {
|
||||
const dropdown = page.locator('.rz-dropdown').first();
|
||||
const isDisabled = await dropdown.getAttribute('aria-disabled');
|
||||
return isDisabled === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the search name
|
||||
* @param page - Playwright page object
|
||||
* @param name - The search name to enter
|
||||
*/
|
||||
export async function enterSearchName(page: Page, name: string): Promise<void> {
|
||||
// The search name input has placeholder " " (single space)
|
||||
await page.locator('input[placeholder=" "]').first().fill(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current search name
|
||||
* @param page - Playwright page object
|
||||
* @returns The current search name value
|
||||
*/
|
||||
export async function getSearchName(page: Page): Promise<string> {
|
||||
return await page.locator('input[placeholder=" "]').first().inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the Submit Search button
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function clickSubmitSearch(page: Page): Promise<void> {
|
||||
// Button contains icon "send" and text "Submit" - use text content
|
||||
await page.locator('button:has-text("Submit")').first().click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the submit search dialog
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function confirmSubmitSearch(page: Page): Promise<void> {
|
||||
// Wait for confirmation dialog
|
||||
await page.waitForSelector('text=Confirm Submit', { timeout: 5000 });
|
||||
// Click the Submit button inside the dialog (use exact match to avoid the main page Submit button)
|
||||
await page.locator('.rz-dialog-wrapper button').getByText('Submit', { exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Page, expect, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Check if a form field has a validation error
|
||||
* @param page - Playwright page object
|
||||
* @param fieldName - The name attribute of the field
|
||||
* @returns true if field has validation error styling
|
||||
*/
|
||||
export async function fieldHasError(page: Page, fieldName: string): Promise<boolean> {
|
||||
const field = page.locator(`[name="${fieldName}"]`);
|
||||
const classes = await field.getAttribute('class') ?? '';
|
||||
|
||||
// Radzen validation error class
|
||||
return classes.includes('rz-state-invalid') || classes.includes('invalid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the validation summary has any errors
|
||||
* @param page - Playwright page object
|
||||
* @returns true if validation errors are present
|
||||
*/
|
||||
export async function hasValidationErrors(page: Page): Promise<boolean> {
|
||||
// Check for Radzen error notifications (Validation Error notifications)
|
||||
const errorNotification = page.locator('.rz-notification-error:has-text("Validation Error")');
|
||||
if (await errorNotification.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Radzen validation summary
|
||||
const validationSummary = page.locator('.rz-validation-summary');
|
||||
if (await validationSummary.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
const errors = validationSummary.locator('.validation-message, .rz-message');
|
||||
const count = await errors.count();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
// Check for standard validation messages
|
||||
const validationMessages = page.locator('.validation-message');
|
||||
return (await validationMessages.count()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all validation error messages
|
||||
* @param page - Playwright page object
|
||||
* @returns Array of error message texts
|
||||
*/
|
||||
export async function getValidationErrors(page: Page): Promise<string[]> {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Get errors from notification toasts (Validation Error notifications)
|
||||
const notificationErrors = page.locator('.rz-notification-error');
|
||||
const notificationCount = await notificationErrors.count();
|
||||
for (let i = 0; i < notificationCount; i++) {
|
||||
const notification = notificationErrors.nth(i);
|
||||
// Check if it's a validation error notification
|
||||
const title = await notification.locator('div').first().textContent() ?? '';
|
||||
if (title.includes('Validation Error')) {
|
||||
// Get the message (second div in the notification)
|
||||
const message = await notification.locator('div').nth(1).textContent();
|
||||
if (message) errors.push(message.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Get errors from validation summary
|
||||
const summaryErrors = page.locator('.rz-validation-summary .validation-message, .rz-validation-summary .rz-message');
|
||||
const summaryCount = await summaryErrors.count();
|
||||
for (let i = 0; i < summaryCount; i++) {
|
||||
const text = await summaryErrors.nth(i).textContent();
|
||||
if (text && !errors.includes(text.trim())) errors.push(text.trim());
|
||||
}
|
||||
|
||||
// Get inline validation messages
|
||||
const inlineErrors = page.locator('.field-validation-error, .validation-message');
|
||||
const inlineCount = await inlineErrors.count();
|
||||
for (let i = 0; i < inlineCount; i++) {
|
||||
const text = await inlineErrors.nth(i).textContent();
|
||||
if (text && !errors.includes(text.trim())) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a specific validation error message is present
|
||||
* @param page - Playwright page object
|
||||
* @param expectedMessage - The expected error message (partial match)
|
||||
*/
|
||||
export async function assertValidationError(page: Page, expectedMessage: string): Promise<void> {
|
||||
const errors = await getValidationErrors(page);
|
||||
const found = errors.some(e => e.toLowerCase().includes(expectedMessage.toLowerCase()));
|
||||
expect(found, `Expected validation error containing "${expectedMessage}" but found: ${errors.join(', ')}`).toBe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert no validation errors are present
|
||||
* @param page - Playwright page object
|
||||
*/
|
||||
export async function assertNoValidationErrors(page: Page): Promise<void> {
|
||||
const hasErrors = await hasValidationErrors(page);
|
||||
if (hasErrors) {
|
||||
const errors = await getValidationErrors(page);
|
||||
expect(hasErrors, `Expected no validation errors but found: ${errors.join(', ')}`).toBe(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific form field is required and shows error when empty
|
||||
* @param page - Playwright page object
|
||||
* @param fieldLocator - Locator for the field
|
||||
* @returns true if field shows required validation
|
||||
*/
|
||||
export async function isFieldRequired(page: Page, fieldLocator: Locator): Promise<boolean> {
|
||||
// Check for required attribute
|
||||
const required = await fieldLocator.getAttribute('required');
|
||||
if (required !== null) return true;
|
||||
|
||||
// Check for aria-required attribute
|
||||
const ariaRequired = await fieldLocator.getAttribute('aria-required');
|
||||
return ariaRequired === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a panel shows a validation message for missing required items
|
||||
* @param page - Playwright page object
|
||||
* @param panelHeader - The header text of the panel
|
||||
* @returns true if panel has validation error
|
||||
*/
|
||||
export async function panelHasValidationError(page: Page, panelHeader: string): Promise<boolean> {
|
||||
const panel = page.locator(`.rz-card:has-text("${panelHeader}")`);
|
||||
const validationMessage = panel.locator('.validation-message, .rz-message-error, .text-danger');
|
||||
return await validationMessage.isVisible({ timeout: 1000 }).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for validation to complete (after form submission attempt)
|
||||
* @param page - Playwright page object
|
||||
* @param timeout - Maximum time to wait (default: 2000ms)
|
||||
*/
|
||||
export async function waitForValidation(page: Page, timeout: number = 2000): Promise<void> {
|
||||
await page.waitForTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Submit button is enabled (form is valid)
|
||||
* @param page - Playwright page object
|
||||
* @returns true if submit button is enabled
|
||||
*/
|
||||
export async function isSubmitEnabled(page: Page): Promise<boolean> {
|
||||
const submitButton = page.locator('button:has-text("Submit Search")');
|
||||
return !(await submitButton.isDisabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to submit and expect validation failure
|
||||
* @param page - Playwright page object
|
||||
* @param expectedError - Expected error message (partial match)
|
||||
*/
|
||||
export async function submitAndExpectError(page: Page, expectedError?: string): Promise<void> {
|
||||
await page.locator('button:has-text("Submit Search")').click();
|
||||
await waitForValidation(page);
|
||||
|
||||
expect(await hasValidationErrors(page), 'Expected validation errors after submit').toBe(true);
|
||||
|
||||
if (expectedError) {
|
||||
await assertValidationError(page, expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error message constants for common scenarios
|
||||
*/
|
||||
export const ValidationMessages = {
|
||||
SEARCH_NAME_REQUIRED: 'Search name is required',
|
||||
SEARCH_TYPE_REQUIRED: 'Search type is required',
|
||||
MIN_DATE_REQUIRED: 'Minimum date is required',
|
||||
MAX_DATE_REQUIRED: 'Maximum date is required',
|
||||
INVALID_DATE_RANGE: 'Minimum date must be before or equal to maximum date',
|
||||
WORK_ORDER_REQUIRED: 'At least one work order is required',
|
||||
PROFIT_CENTER_REQUIRED: 'At least one profit center is required',
|
||||
WORK_CENTER_REQUIRED: 'At least one work center is required',
|
||||
OPERATOR_REQUIRED: 'At least one operator is required',
|
||||
ITEM_NUMBER_REQUIRED: 'At least one item number is required',
|
||||
COMPONENT_LOT_REQUIRED: 'At least one component lot is required',
|
||||
PART_OPERATION_REQUIRED: 'At least one part operation is required',
|
||||
} as const;
|
||||
Reference in New Issue
Block a user