Migrate Playwright suite to .NET UI tests and deprecate TS project

This commit is contained in:
Joseph Doherty
2026-02-06 18:44:40 -05:00
parent 4e56ea3435
commit 562f7e9e37
105 changed files with 1119 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
# JDE Scoping Tool - Playwright E2E Tests
End-to-end tests for the JDE Scoping Tool using Playwright.
## Prerequisites
- Node.js 18+
- The JDE Scoping Tool server running at `http://localhost:5294`
## Setup
```bash
# Install dependencies
npm install
# Install Playwright browsers
npx playwright install chromium
# Create test data files
npm run create-test-data
```
## Running Tests
```bash
# Start the server first (in another terminal)
cd ../../NEW
dotnet run --project src/JdeScoping.Host/JdeScoping.Host.csproj
# Run all tests
npm test
# Run tests with browser visible
npm run test:headed
# Run tests in debug mode
npm run test:debug
# Run tests with Playwright UI
npm run test:ui
```
## Test Structure
```
playwright/
├── package.json # Dependencies
├── playwright.config.ts # Playwright configuration
├── scripts/
│ └── create-test-excel.js # Script to generate test Excel files
├── test-data/ # Generated test Excel files
│ ├── single_workorder.xlsx
│ ├── multiple_workorders.xlsx
│ ├── single_lot.xlsx
│ └── ...
└── tests/
└── search-page.spec.ts # Search page tests
```
## Test Cases
### Work Order Search (TC-010)
- **TC-010-P01**: Upload single work order ✓
- **TC-010-P02**: Upload multiple work orders ✓
- **TC-010-P03**: Download template ✓
- **TC-010-P04**: Clear data ✓
### Component Lot Search (TC-020)
- **TC-020-P01**: Upload component lot file ✓
### Time Span + Profit Center + Item Number (TC-060)
- **TC-060-P01**: Upload item numbers file ✓
### Time Span + Profit Center + Item/Operation/MIS (TC-070)
- **TC-070-P01**: Upload part operations file ✓
## Environment Variables
- `BASE_URL`: Override the default URL (default: `http://localhost:5294`)
```bash
BASE_URL=http://localhost:5000 npm test
```
## Authentication
The tests automatically handle login using the FakeAuthService in development mode:
- Username: `testuser`
- Password: `testpass` (any password works with FakeAuthService)
## File Upload Mechanism
These tests use Playwright's native `setInputFiles()` method which:
1. Finds the hidden file input created by RadzenUpload
2. Sets the file directly on the input element
3. Triggers the component's change event properly
This bypasses the binary encoding issues that occur with JavaScript FormData uploads.
## Blazor WASM Considerations
Blazor WebAssembly applications require additional time to initialize. The tests include:
- Extended timeouts (up to 2 minutes for page load)
- Network idle waits for WASM file downloads
- Post-login navigation refresh to ensure proper state
## Troubleshooting
### Tests timeout waiting for page load
- Ensure the server is running and accessible
- Check that Blazor WASM files are being served (verify `/_framework/dotnet.*.js` returns 200)
- Try running in headed mode to see what's happening: `npm run test:headed`
### Authentication issues
- The app uses FakeAuthService in development mode which accepts any credentials
- If authentication fails, check the server logs for errors
### File upload fails
- Ensure test data files exist: `npm run create-test-data`
- Verify the file input is accessible (RadzenUpload creates a hidden input)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,135 @@
import { test as base, expect, Page } from '@playwright/test';
import { login, ensureLoggedIn } from '../helpers/auth.helper';
import { waitForBlazorReady } from '../helpers/navigation.helper';
/**
* Extended test fixture with authentication state and common utilities
*/
export const test = base.extend<{
/** Page that is already logged in */
authenticatedPage: Page;
}>({
authenticatedPage: async ({ page }, use) => {
// Navigate to the app and login
await page.goto('/');
await page.waitForLoadState('networkidle', { timeout: 60000 });
await login(page);
await waitForBlazorReady(page);
// Provide the authenticated page to the test
await use(page);
},
});
export { expect };
/**
* Test configuration options
*/
export const testConfig = {
/** Default timeout for waiting operations */
defaultTimeout: 30000,
/** Timeout for Blazor WASM initialization */
blazorTimeout: 120000,
/** Timeout for network operations */
networkTimeout: 60000,
/** Short wait for UI updates */
shortWait: 500,
/** Medium wait for async operations */
mediumWait: 2000,
/** Long wait for file operations */
longWait: 5000,
};
/**
* Common test setup function
* @param page - Playwright page object
* @param route - Route to navigate to after login (default: '/search')
*/
export async function setupTest(page: Page, route: string = '/search'): Promise<void> {
await page.goto(route);
await page.waitForLoadState('networkidle', { timeout: testConfig.networkTimeout });
await ensureLoggedIn(page);
await waitForBlazorReady(page);
}
/**
* Test data factory for generating unique test names
*/
export function generateTestName(prefix: string): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
return `${prefix}-${timestamp}`;
}
/**
* Test utilities for common assertions
*/
export const testUtils = {
/**
* Wait for page to stabilize after navigation
*/
async waitForStable(page: Page): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: testConfig.networkTimeout });
await page.waitForTimeout(testConfig.shortWait);
},
/**
* Assert page URL contains expected path
*/
async assertUrlContains(page: Page, expectedPath: string): Promise<void> {
await expect(page).toHaveURL(new RegExp(expectedPath));
},
/**
* Assert element is visible with custom timeout
*/
async assertVisible(page: Page, selector: string, timeout?: number): Promise<void> {
await expect(page.locator(selector)).toBeVisible({
timeout: timeout ?? testConfig.defaultTimeout,
});
},
/**
* Assert element is not visible
*/
async assertNotVisible(page: Page, selector: string, timeout?: number): Promise<void> {
await expect(page.locator(selector)).not.toBeVisible({
timeout: timeout ?? testConfig.defaultTimeout,
});
},
/**
* Assert text is present on page
*/
async assertTextPresent(page: Page, text: string): Promise<void> {
await expect(page.locator(`text=${text}`)).toBeVisible();
},
/**
* Assert text is not present on page
*/
async assertTextNotPresent(page: Page, text: string): Promise<void> {
await expect(page.locator(`text=${text}`)).not.toBeVisible();
},
};
/**
* Skip test if condition is met
*/
export function skipIf(condition: boolean, reason: string): void {
if (condition) {
test.skip(true, reason);
}
}
/**
* Mark test as slow (extends timeout)
*/
export function markSlow(): void {
test.slow();
}
@@ -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;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
{
"name": "jde-scoping-e2e-tests",
"version": "1.0.0",
"description": "End-to-end tests for JDE Scoping Tool using Playwright",
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:ui": "playwright test --ui",
"create-test-data": "node scripts/create-test-excel.js"
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"exceljs": "^4.4.0",
"typescript": "^5.9.3"
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'html',
// Increase timeout for Blazor WASM initialization
timeout: 180000, // 3 minutes per test
expect: {
timeout: 30000, // 30 seconds for assertions
},
use: {
baseURL: process.env.BASE_URL || 'http://localhost:5294',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// Longer timeouts for Blazor WASM
navigationTimeout: 120000, // 2 minutes for page load
actionTimeout: 30000, // 30 seconds for actions
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
/* Run local dev server before starting tests */
// webServer: {
// command: 'dotnet run --project ../NEW/src/JdeScoping.Host/JdeScoping.Host.csproj',
// url: 'http://localhost:5294',
// reuseExistingServer: !process.env.CI,
// },
});
@@ -0,0 +1,147 @@
const ExcelJS = require('exceljs');
const path = require('path');
const fs = require('fs');
const testDataDir = path.join(__dirname, '..', 'test-data');
// Ensure test-data directory exists
if (!fs.existsSync(testDataDir)) {
fs.mkdirSync(testDataDir, { recursive: true });
}
async function createWorkOrderFile(filename, workOrders) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.getCell('A1').value = 'Work Order Number';
workOrders.forEach((wo, index) => {
worksheet.getCell(`A${index + 2}`).value = wo;
});
const filepath = path.join(testDataDir, filename);
await workbook.xlsx.writeFile(filepath);
console.log(`Created: ${filepath}`);
return filepath;
}
async function createComponentLotFile(filename, lots) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.getCell('A1').value = 'Lot Number';
worksheet.getCell('B1').value = 'Item Number';
lots.forEach((lot, index) => {
worksheet.getCell(`A${index + 2}`).value = lot.lotNumber;
worksheet.getCell(`B${index + 2}`).value = lot.itemNumber;
});
const filepath = path.join(testDataDir, filename);
await workbook.xlsx.writeFile(filepath);
console.log(`Created: ${filepath}`);
return filepath;
}
async function createItemNumberFile(filename, items) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.getCell('A1').value = 'Item Number';
items.forEach((item, index) => {
worksheet.getCell(`A${index + 2}`).value = item;
});
const filepath = path.join(testDataDir, filename);
await workbook.xlsx.writeFile(filepath);
console.log(`Created: ${filepath}`);
return filepath;
}
async function createPartOperationFile(filename, operations) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.getCell('A1').value = 'Item Number';
worksheet.getCell('B1').value = 'Operation Number';
worksheet.getCell('C1').value = 'MIS Number';
worksheet.getCell('D1').value = 'MIS Revision';
operations.forEach((op, index) => {
worksheet.getCell(`A${index + 2}`).value = op.itemNumber;
worksheet.getCell(`B${index + 2}`).value = op.operationNumber;
worksheet.getCell(`C${index + 2}`).value = op.misNumber;
worksheet.getCell(`D${index + 2}`).value = op.misRevision;
});
const filepath = path.join(testDataDir, filename);
await workbook.xlsx.writeFile(filepath);
console.log(`Created: ${filepath}`);
return filepath;
}
async function createEmptyExcelFile(filename) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// Just headers, no data
worksheet.getCell('A1').value = 'Work Order Number';
const filepath = path.join(testDataDir, filename);
await workbook.xlsx.writeFile(filepath);
console.log(`Created: ${filepath}`);
return filepath;
}
async function createInvalidTextFile(filename) {
const filepath = path.join(testDataDir, filename);
fs.writeFileSync(filepath, 'This is not a valid Excel file\nJust plain text');
console.log(`Created: ${filepath}`);
return filepath;
}
async function main() {
console.log('Creating test Excel files...\n');
// Work Order test files
await createWorkOrderFile('single_workorder.xlsx', [99059700]);
await createWorkOrderFile('multiple_workorders.xlsx', [99059700, 99059701, 99059702]);
// Maximum work orders from test data
await createWorkOrderFile('max_workorders.xlsx', [
99059700, 99002260, 99002259, 99002258, 99002257,
99002256, 99002255, 99002254, 99002252, 99002251,
99002250, 99002249, 99002248, 99002247, 99002246
]);
// Component Lot test files
await createComponentLotFile('single_lot.xlsx', [
{ lotNumber: 'LOT001', itemNumber: '00598004702' }
]);
await createComponentLotFile('multiple_lots.xlsx', [
{ lotNumber: 'LOT001', itemNumber: '00598004702' },
{ lotNumber: 'LOT002', itemNumber: '00598004703' }
]);
// Item Number test files
await createItemNumberFile('single_item.xlsx', ['00598004702']);
await createItemNumberFile('multiple_items.xlsx', ['00598004702', '00598004703', '00598004704']);
// Part Operation test files
await createPartOperationFile('single_operation.xlsx', [
{ itemNumber: '00598004702', operationNumber: '100', misNumber: 'MIS001', misRevision: 'A' }
]);
await createPartOperationFile('multiple_operations.xlsx', [
{ itemNumber: '00598004702', operationNumber: '100', misNumber: 'MIS001', misRevision: 'A' },
{ itemNumber: '00598004702', operationNumber: '200', misNumber: 'MIS002', misRevision: 'B' }
]);
// Invalid test files for negative testing
console.log('\nCreating invalid test files for negative testing...');
await createEmptyExcelFile('empty_file.xlsx');
await createInvalidTextFile('invalid_format.txt');
// Work orders with invalid data
await createWorkOrderFile('invalid_workorders.xlsx', ['ABC123XYZ', 'INVALID', '']);
await createWorkOrderFile('special_chars_workorders.xlsx', ['99059700!@#', '99002260$%^', '99002259&*']);
console.log('\nAll test files created successfully!');
}
main().catch(console.error);
@@ -0,0 +1,2 @@
This is not a valid Excel file
Just plain text
@@ -0,0 +1,146 @@
{
"profitCenters": [
{ "code": "1AM", "description": "Profit Center 1AM" },
{ "code": "1BM", "description": "Profit Center 1BM" },
{ "code": "1CM", "description": "Profit Center 1CM" },
{ "code": "1PM", "description": "Profit Center 1PM" },
{ "code": "2DM", "description": "Profit Center 2DM" },
{ "code": "2SM", "description": "Profit Center 2SM" },
{ "code": "3TM", "description": "Profit Center 3TM" },
{ "code": "4IM", "description": "Profit Center 4IM" },
{ "code": "5SM", "description": "Profit Center 5SM" }
],
"workCenters": [
{ "code": "WC001", "description": "Work Center 001" },
{ "code": "WC002", "description": "Work Center 002" },
{ "code": "WC003", "description": "Work Center 003" }
],
"operators": [
{ "userId": "ADAMSSN", "fullName": "Adams, S N" },
{ "userId": "AGNEWA", "fullName": "Agnew, A" },
{ "userId": "AGNEWL", "fullName": "Agnew, L" },
{ "userId": "ALASMARB", "fullName": "Alasmar, B" },
{ "userId": "ALEXIUCG", "fullName": "Alexiuc, G" },
{ "userId": "ALLENHY", "fullName": "Allen, H Y" },
{ "userId": "ALLENNI", "fullName": "Allen, N I" },
{ "userId": "ALURUM", "fullName": "Aluru, M" },
{ "userId": "ALVESM1", "fullName": "Alves, M" },
{ "userId": "APONTEVE", "fullName": "Aponte, V E" }
],
"workOrders": [
{ "workOrderNumber": "99059700", "itemNumber": "00598004702" },
{ "workOrderNumber": "99002260", "itemNumber": "82070000028" },
{ "workOrderNumber": "99002259", "itemNumber": "82070000027" },
{ "workOrderNumber": "99002258", "itemNumber": "82070000019" },
{ "workOrderNumber": "99002257", "itemNumber": "82070000018" },
{ "workOrderNumber": "99002256", "itemNumber": "82070000017" },
{ "workOrderNumber": "99002255", "itemNumber": "00855140333" },
{ "workOrderNumber": "99002254", "itemNumber": "00855480834" },
{ "workOrderNumber": "99002252", "itemNumber": "82070000016" },
{ "workOrderNumber": "99002251", "itemNumber": "00855910448" },
{ "workOrderNumber": "99002250", "itemNumber": "82070000015" },
{ "workOrderNumber": "99002249", "itemNumber": "00855480834" },
{ "workOrderNumber": "99002248", "itemNumber": "00855910446" },
{ "workOrderNumber": "99002247", "itemNumber": "00855910447" },
{ "workOrderNumber": "99002246", "itemNumber": "82900171601" }
],
"itemNumbers": [
{ "itemNumber": "00598004702", "description": "Item 598004702" },
{ "itemNumber": "82070000028", "description": "Item 82070000028" },
{ "itemNumber": "82070000027", "description": "Item 82070000027" },
{ "itemNumber": "00855140333", "description": "Item 855140333" },
{ "itemNumber": "00855480834", "description": "Item 855480834" }
],
"componentLots": [
{ "lotNumber": "LOT001", "itemNumber": "00598004702" },
{ "lotNumber": "LOT002", "itemNumber": "82070000028" },
{ "lotNumber": "LOT003", "itemNumber": "82070000027" }
],
"partOperations": [
{
"itemNumber": "00598004702",
"operationNumber": "100",
"misNumber": "MIS001",
"misRevision": "A"
},
{
"itemNumber": "00598004702",
"operationNumber": "200",
"misNumber": "MIS002",
"misRevision": "B"
},
{
"itemNumber": "82070000028",
"operationNumber": "100",
"misNumber": "MIS003",
"misRevision": "A"
}
],
"dateRanges": {
"recent": {
"min": "2020-01-01",
"max": "2020-09-01",
"description": "Recent data range"
},
"midRange": {
"min": "2018-01-01",
"max": "2019-12-31",
"description": "Mid-range data"
},
"historical": {
"min": "2016-01-01",
"max": "2017-12-31",
"description": "Historical data"
},
"sameDay": {
"min": "2020-06-15",
"max": "2020-06-15",
"description": "Single day range"
},
"startBoundary": {
"min": "1905-01-20",
"max": "1905-12-31",
"description": "Start of data range"
},
"endBoundary": {
"min": "2020-08-01",
"max": "2020-09-01",
"description": "End of data range"
}
},
"invalidData": {
"workOrders": {
"invalidFormat": "ABC123XYZ",
"specialChars": "99059700!@#",
"empty": "",
"whitespace": " "
},
"profitCenters": {
"invalid": "INVALID",
"specialChars": "1AM!@#",
"empty": "",
"tooLong": "1AMEXTRALONG"
},
"dates": {
"invalidFormat": "31-12-2020",
"future": {
"min": "2025-01-01",
"max": "2025-12-31"
},
"reversed": {
"min": "2020-09-01",
"max": "2020-01-01"
}
}
},
"testCredentials": {
"validUser": {
"username": "testuser",
"password": "testpass"
},
"invalidUser": {
"username": "invaliduser",
"password": "wrongpassword"
}
}
}
@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}
@@ -0,0 +1,553 @@
import { test, expect } from '@playwright/test';
import path from 'path';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { uploadFile, clearUploadedData, downloadTemplate, getUploadedItemCount, componentLotConfig, getTestFile, TestFiles, getTestDataPath } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, dataGridIsEmpty, confirmDialog, hasErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, assertValidationError, ValidationMessages, getValidationErrors } from '../helpers/validation.helper';
/**
* Component Lot Search (Type 20) - E2E Tests
*
* Based on manual test scripts from TestScripts/SearchPage/20_ComponentLot.md
* Tests the Component Lot search functionality which allows users to search by
* one or more component lot numbers without requiring a time span or other filters.
*/
test.describe('Component Lot Search (Type 20)', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
/**
* POSITIVE TEST CASES
*/
test.describe('Positive Tests', () => {
test('TC-020-P01: Single Component Lot Search', async ({ page }) => {
// Step 1-2: Navigate to Submit Search page (done in beforeEach), enter search name
await enterSearchName(page, 'TC-020-P01 Single Lot Test');
// Step 3: Select "Component Lot" search type (Type 20)
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-6: Upload single lot file and verify it appears in grid
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Verify lot appears in the grid (count should be 1)
const count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBe(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Step 7: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created successfully (no errors)
await assertNoErrorNotification(page);
});
test('TC-020-P02: Multiple Component Lots Search', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P02 Multiple Lots Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-7: Upload multiple lots file
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.MULTIPLE_LOTS);
await uploadFile(page, componentLotConfig, testFile);
// Verify multiple lots appear in the grid
const count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBeGreaterThan(1);
// Verify no error
await assertNoErrorNotification(page);
// Step 8: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with all lots
await assertNoErrorNotification(page);
});
test('TC-020-P03: Component Lot with Multiple Item Associations', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P03 Multi-Item Lot Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload file containing lot 00009106 (associated with multiple items)
// This tests that the system correctly handles lots that map to multiple items
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Use multiple_lots file which may contain lots with multiple item associations
const testFile = getTestFile(TestFiles.MULTIPLE_LOTS);
await uploadFile(page, componentLotConfig, testFile);
// Verify lots appear in the grid
const count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBeGreaterThan(0);
// Verify no error
await assertNoErrorNotification(page);
// Step 6: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created successfully
// Results should include all work orders where these lots were used
await assertNoErrorNotification(page);
});
test('TC-020-P04: Component Lot Search with Maximum Entries', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P04 Max Lots Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload file with many lots
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.MULTIPLE_LOTS);
await uploadFile(page, componentLotConfig, testFile);
// Verify lots appear in the grid
const count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBeGreaterThan(0);
// Verify no error
await assertNoErrorNotification(page);
// Step 6: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with all lots
await assertNoErrorNotification(page);
});
test('TC-020-P05: Component Lot Search - Remove and Re-add (Clear Data)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P05 Remove Re-add Lot Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload multiple lots
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.MULTIPLE_LOTS);
await uploadFile(page, componentLotConfig, testFile);
// Verify lots were added
let count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBeGreaterThan(0);
// Step 6: Clear all data (simulates removing entries)
await clearUploadedData(page, componentLotConfig);
// Step 7: Verify grid is empty
const isEmpty = await dataGridIsEmpty(page);
expect(isEmpty).toBe(true);
// Step 8: Re-add with single lot
const singleFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, singleFile);
// Verify single lot appears
count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBe(1);
// Step 9: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with only the re-added lot
await assertNoErrorNotification(page);
});
test('TC-020-P06: Component Lot Search - Duplicate Prevention', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P06 Lot Duplicate Prevention Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4: Upload single lot
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Get initial count
const initialCount = await getUploadedItemCount(page, componentLotConfig);
expect(initialCount).toBe(1);
// Step 5: Upload the same file again (attempt to add duplicate)
await uploadFile(page, componentLotConfig, testFile);
// Step 6: Observe system behavior
// Expected Results: System prevents duplicate or displays validation message
// Lot should appear only once in the list
const finalCount = await getUploadedItemCount(page, componentLotConfig);
// The count should either:
// 1. Stay the same (duplicates prevented)
// 2. Increase (if duplicates allowed) but system may show warning
// This test verifies the behavior is consistent
expect(finalCount).toBeGreaterThanOrEqual(initialCount);
});
test('TC-020-P07: Component Lot with Leading Zeros Handling', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-P07 Leading Zeros Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-6: Upload file - system should handle lot numbers with leading zeros
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// The test file should contain lot numbers which may have leading zeros
// (e.g., "00000099" should be handled correctly)
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Verify lot was added
const count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBe(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search to verify the data can be processed
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: System handles leading zeros consistently
await assertNoErrorNotification(page);
});
test('TC-020-P08: Download Template contains correct format', async ({ page }) => {
// Select Component Lot search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Verify panel is visible
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Download the template
const download = await downloadTemplate(page, componentLotConfig);
// Verify filename
const filename = download.suggestedFilename();
expect(filename).toContain('.xlsx');
expect(filename.toLowerCase()).toContain('lot');
});
});
/**
* NEGATIVE TEST CASES
*/
test.describe('Negative Tests', () => {
test('TC-020-N01: Missing Search Name', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Leave search name field empty
// (Don't enter any name)
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4: Add lot
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Step 5: Click Submit Search
await clickSubmitSearch(page);
// Expected Results: Validation error for missing search name
// Search is NOT created, user remains on Submit Search page
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
// Verify the specific error message
await assertValidationError(page, 'name');
});
test('TC-020-N02: No Search Type Selected', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Enter search name
await enterSearchName(page, 'TC-020-N02 No Type Test');
// Step 3: Do NOT select any search type
// Step 4: Attempt to add component lots - should not be possible without search type
// The component lot input panel should not be visible without selecting a search type
const isPanelVisible = await page.locator(`text=${componentLotConfig.panelHeader}`).isVisible({ timeout: 2000 }).catch(() => false);
expect(isPanelVisible).toBe(false);
// Step 5: Click Submit Search anyway
await clickSubmitSearch(page);
// Expected Results: Validation error for missing search type
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
});
test('TC-020-N03: Empty Component Lot List', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Enter search name
await enterSearchName(page, 'TC-020-N03 Empty Lots Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Verify panel is visible
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Step 4: Do NOT add any component lots
// Verify grid shows no records
const isEmpty = await dataGridIsEmpty(page);
expect(isEmpty).toBe(true);
// Step 5: Click Submit Search
await clickSubmitSearch(page);
// Expected Results: Validation error indicating at least one component lot is required
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
});
test('TC-020-N04: Invalid Lot Number Format', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-N04 Invalid Lot Format Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload file with invalid lot format (ABCD1234)
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Use invalid workorders file as a proxy for invalid format
// (Both test invalid alphanumeric entries)
const testFile = getTestFile(TestFiles.INVALID_WORKORDERS);
await uploadFile(page, componentLotConfig, testFile);
// Expected Results:
// Either error notification appears OR invalid data is not added to list
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// One of these should be true - either upload failed or no valid records were added
// If items were added despite being invalid, submit should fail
if (!hasError && !isEmpty) {
await clickSubmitSearch(page);
const hasValidationError = await hasValidationErrors(page);
expect(hasValidationError || hasError || isEmpty).toBe(true);
}
});
test('TC-020-N05: Component Lot with Special Characters', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-N05 Lot Special Characters Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload file with special characters (00000099!@#)
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SPECIAL_CHARS_WORKORDERS);
await uploadFile(page, componentLotConfig, testFile);
// Expected Results:
// System should reject invalid characters
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// Either upload should fail or no valid records should be added
// If data was added, submit should fail validation
if (!hasError && !isEmpty) {
await clickSubmitSearch(page);
const hasValidationError = await hasValidationErrors(page);
expect(hasValidationError || hasError || isEmpty).toBe(true);
}
});
test('TC-020-N06: Empty Component Lot Input (Empty File)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-N06 Empty Lot Input Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload empty file
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.EMPTY_FILE);
await uploadFile(page, componentLotConfig, testFile);
// Wait for any processing
await page.waitForTimeout(1000);
// Expected Results: Grid should remain empty or show error
const isEmpty = await dataGridIsEmpty(page);
// Try to submit
await clickSubmitSearch(page);
// Should fail validation since no lots were added
const hasErrors = await hasValidationErrors(page);
expect(hasErrors || isEmpty).toBe(true);
});
test('TC-020-N07: Invalid File Format (Non-Excel)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-020-N07 Invalid File Format Test');
// Step 3: Select "Component Lot" search type
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Step 4-5: Upload invalid format file (.txt instead of .xlsx)
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.INVALID_FORMAT);
await uploadFile(page, componentLotConfig, testFile);
// Wait for any error processing
await page.waitForTimeout(2000);
// Expected Results:
// System should show error notification for invalid file format
// OR grid should remain empty
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// At least one of these should be true
expect(hasError || isEmpty).toBe(true);
});
test('TC-020-N08: Negative Lot Number', async ({ page }) => {
// This test validates that the system handles negative numbers appropriately
// Using the special chars file as proxy for invalid numeric input
await enterSearchName(page, 'TC-020-N08 Negative Lot Number Test');
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Upload file with special/invalid characters
const testFile = getTestFile(TestFiles.SPECIAL_CHARS_WORKORDERS);
await uploadFile(page, componentLotConfig, testFile);
// Expected Results:
// System should reject invalid format (negative numbers would have minus sign)
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
if (!hasError && !isEmpty) {
await clickSubmitSearch(page);
const hasValidationError = await hasValidationErrors(page);
expect(hasValidationError || hasError || isEmpty).toBe(true);
}
});
test('TC-020-N09: Submit Without Confirmation', async ({ page }) => {
// Setup: Create a valid search
await enterSearchName(page, 'TC-020-N09 Confirmation Test');
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Click Submit but do NOT confirm
await clickSubmitSearch(page);
// Wait for dialog
await page.waitForSelector('text=Confirm Submit', { timeout: 5000 });
// Click Cancel instead of OK
await page.locator('button:has-text("Cancel")').click();
await page.waitForTimeout(500);
// Expected Results: Search should NOT be created, user remains on page
// The page should still show the search form
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
});
});
/**
* EDGE CASE TESTS
*/
test.describe('Edge Cases', () => {
test('TC-020-E01: Switch from Component Lot to Different Search Type', async ({ page }) => {
// Select Component Lot first
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Upload some data
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Verify data was added
let count = await getUploadedItemCount(page, componentLotConfig);
expect(count).toBeGreaterThan(0);
// Switch to a different search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// The Component Lot panel should be hidden
const isPanelVisible = await page.locator(`text=${componentLotConfig.panelHeader}`).isVisible({ timeout: 2000 }).catch(() => false);
expect(isPanelVisible).toBe(false);
// Work Order panel should now be visible
await expect(page.locator('text=Filter by Work Order')).toBeVisible();
});
test('TC-020-E02: Switch Back to Component Lot After Changing Type', async ({ page }) => {
// Select Component Lot
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Upload data
const testFile = getTestFile(TestFiles.SINGLE_LOT);
await uploadFile(page, componentLotConfig, testFile);
// Get count
const initialCount = await getUploadedItemCount(page, componentLotConfig);
expect(initialCount).toBeGreaterThan(0);
// Switch to Work Order
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Switch back to Component Lot
await selectSearchType(page, SearchTypes.COMPONENT_LOT);
// Verify panel is visible again
await expect(page.locator(`text=${componentLotConfig.panelHeader}`)).toBeVisible();
// Check if data was preserved (implementation-dependent)
const finalCount = await getUploadedItemCount(page, componentLotConfig);
// Data may or may not be preserved depending on implementation
// Just verify the panel is functional
expect(finalCount).toBeGreaterThanOrEqual(0);
});
});
});
@@ -0,0 +1,419 @@
import { test, expect } from '@playwright/test';
import { login, logout, isLoggedIn } from '../helpers/auth.helper';
import { navigateToDataSync, clickNavLink } from '../helpers/navigation.helper';
import { getDataGridRowCount, getBadgeStyle, clickButton, StatusBadgeStyles } from '../helpers/radzen.helper';
test.describe('Data Sync Requests Page', () => {
test.beforeEach(async ({ page }) => {
await navigateToDataSync(page);
});
test.describe('Page Load and Display', () => {
test('page loads and displays Data Sync Requests heading', async ({ page }) => {
// Verify page title
await expect(page).toHaveTitle(/Data Sync Requests - JDE Scoping Tool/);
// Verify heading is visible
await expect(page.locator('h4:has-text("Data Sync Requests")')).toBeVisible();
});
test('page shows filter card', async ({ page }) => {
// Verify filter card is visible
const filterCard = page.locator('.rz-card');
await expect(filterCard.first()).toBeVisible();
});
test('page shows New Request button', async ({ page }) => {
const newRequestButton = page.locator('button:has-text("New Request")');
await expect(newRequestButton).toBeVisible();
});
test('page shows Reload Pipelines button', async ({ page }) => {
const reloadButton = page.locator('button:has-text("Reload Pipelines")');
await expect(reloadButton).toBeVisible();
});
test('no error notification on page load', async ({ page }) => {
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Filter Controls', () => {
test('show pending only checkbox is visible', async ({ page }) => {
const checkbox = page.locator('.rz-chkbox');
await expect(checkbox).toBeVisible();
});
test('show pending only label is visible', async ({ page }) => {
await expect(page.locator('text=Show pending only')).toBeVisible();
});
test('refresh button is visible', async ({ page }) => {
const refreshButton = page.locator('button:has-text("Refresh")');
await expect(refreshButton).toBeVisible();
});
test('clicking refresh button reloads data', async ({ page }) => {
// Wait for initial load
await page.waitForTimeout(2000);
// Click refresh button
const refreshButton = page.locator('button:has-text("Refresh")');
await refreshButton.click();
// Wait for reload
await page.waitForTimeout(2000);
// Verify no error
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
test('filter checkbox toggles pending only filter', async ({ page }) => {
// Wait for initial load
await page.waitForTimeout(2000);
// Find and click the checkbox
const checkbox = page.locator('.rz-chkbox').first();
await checkbox.click();
// Wait for filter to apply
await page.waitForTimeout(1000);
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Data Grid Display', () => {
test('data grid or empty state message is visible', async ({ page }) => {
// Wait for loading to complete
await page.waitForTimeout(2000);
// Either data grid is visible or info message about no requests
const dataGrid = page.locator('.rz-data-grid');
const noRequestsMessage = page.locator('text=No sync requests found');
const gridVisible = await dataGrid.isVisible({ timeout: 3000 }).catch(() => false);
const messageVisible = await noRequestsMessage.isVisible({ timeout: 3000 }).catch(() => false);
expect(gridVisible || messageVisible).toBe(true);
});
});
test.describe('Data Grid Columns', () => {
test('data grid shows Pipeline column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const pipelineHeader = page.locator('.rz-data-grid th:has-text("Pipeline")');
await expect(pipelineHeader).toBeVisible();
}
});
test('data grid shows Type column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const typeHeader = page.locator('.rz-data-grid th:has-text("Type")');
await expect(typeHeader).toBeVisible();
}
});
test('data grid shows Requested column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const requestedHeader = page.locator('.rz-data-grid th:has-text("Requested")');
await expect(requestedHeader).toBeVisible();
}
});
test('data grid shows By column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const byHeader = page.locator('.rz-data-grid th:has-text("By")');
await expect(byHeader).toBeVisible();
}
});
test('data grid shows Status column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const statusHeader = page.locator('.rz-data-grid th:has-text("Status")');
await expect(statusHeader).toBeVisible();
}
});
test('data grid shows Actions column when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const actionsHeader = page.locator('.rz-data-grid th:has-text("Actions")');
await expect(actionsHeader).toBeVisible();
}
});
test('all expected columns are present when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const headers = page.locator('.rz-data-grid th');
const headerTexts = await headers.allTextContents();
// Verify all expected columns
expect(headerTexts.some(h => h.includes('Pipeline'))).toBe(true);
expect(headerTexts.some(h => h.includes('Type'))).toBe(true);
expect(headerTexts.some(h => h.includes('Requested'))).toBe(true);
expect(headerTexts.some(h => h.includes('By'))).toBe(true);
expect(headerTexts.some(h => h.includes('Status'))).toBe(true);
expect(headerTexts.some(h => h.includes('Actions'))).toBe(true);
}
});
});
test.describe('Status Badges', () => {
test('Pending status displays with appropriate badge style', async ({ page }) => {
await page.waitForTimeout(2000);
const pendingBadge = page.locator('.rz-data-grid .rz-badge:has-text("Pending")').first();
if (await pendingBadge.isVisible({ timeout: 3000 }).catch(() => false)) {
const style = await getBadgeStyle(pendingBadge);
// Pending typically uses warning style
expect(style).toBeTruthy();
}
});
test('Completed status displays with success badge style', async ({ page }) => {
await page.waitForTimeout(2000);
const completedBadge = page.locator('.rz-data-grid .rz-badge:has-text("Completed")').first();
if (await completedBadge.isVisible({ timeout: 3000 }).catch(() => false)) {
const style = await getBadgeStyle(completedBadge);
expect(style).toBe('success');
}
});
test('Failed status displays with danger badge style', async ({ page }) => {
await page.waitForTimeout(2000);
const failedBadge = page.locator('.rz-data-grid .rz-badge:has-text("Failed")').first();
if (await failedBadge.isVisible({ timeout: 3000 }).catch(() => false)) {
const style = await getBadgeStyle(failedBadge);
expect(style).toBe('danger');
}
});
});
test.describe('Action Buttons', () => {
test('Cancel button appears only for Pending requests', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get all rows
const rows = page.locator('.rz-data-grid tbody tr');
const rowCountNum = await rows.count();
for (let i = 0; i < rowCountNum; i++) {
const row = rows.nth(i);
const statusBadge = row.locator('.rz-badge');
const statusText = await statusBadge.textContent();
const cancelButton = row.locator('button:has-text("Cancel")');
if (statusText?.trim() === 'Pending') {
// Pending rows should have Cancel button
await expect(cancelButton).toBeVisible();
} else {
// Non-pending rows should not have Cancel button
await expect(cancelButton).not.toBeVisible();
}
}
}
});
test('New Request button opens dialog', async ({ page }) => {
// Click New Request button
const newRequestButton = page.locator('button:has-text("New Request")');
await newRequestButton.click();
// Wait for dialog to appear
await page.waitForTimeout(1000);
// Should see a dialog (Radzen dialog)
const dialog = page.locator('.rz-dialog');
const dialogVisible = await dialog.isVisible({ timeout: 5000 }).catch(() => false);
// If dialog opened, verify it's there; if not, verify no error
if (!dialogVisible) {
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
}
});
});
test.describe('Data Grid Features', () => {
test('data grid supports sorting when requests exist', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
// Click on Pipeline column header to sort
const pipelineHeader = page.locator('.rz-data-grid th:has-text("Pipeline")');
await pipelineHeader.click();
await page.waitForTimeout(500);
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
}
});
test('data grid supports pagination with 20 items per page', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
// Look for pager component
const pager = page.locator('.rz-pager');
await expect(pager).toBeVisible({ timeout: 5000 });
}
});
test('data grid supports column resize', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
}
});
});
test.describe('Date Formatting', () => {
test('requested date is formatted correctly (MM/dd hh:mm)', async ({ page }) => {
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
if (await dataGrid.isVisible({ timeout: 3000 }).catch(() => false)) {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from Requested column (index 2)
const requestedCell = page.locator('.rz-data-grid tbody tr').first().locator('td').nth(2);
const dateText = await requestedCell.textContent();
if (dateText && dateText.trim()) {
// Verify date format matches MM/dd hh:mm pattern
const datePattern = /^\d{2}\/\d{2} \d{2}:\d{2}$/;
expect(dateText.trim()).toMatch(datePattern);
}
}
}
});
});
test.describe('Empty State', () => {
test('displays info message when no sync requests found', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount === 0) {
// Should show info message
const noRequestsMessage = page.locator('text=No sync requests found');
await expect(noRequestsMessage).toBeVisible();
}
});
test('empty state message suggests creating new request', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount === 0) {
// Message should mention "New Request"
const helpMessage = page.locator('text=Click "New Request" to create one');
await expect(helpMessage).toBeVisible();
}
});
});
test.describe('Loading State', () => {
test('shows loading indicator while fetching data', async ({ page }) => {
// Navigate fresh to catch loading state
await page.goto('/data-sync/requests');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// If there's a loading indicator, it should eventually disappear
const loadingIndicator = page.locator('text=Loading requests');
// Wait for page to settle
await page.waitForTimeout(3000);
// After waiting, loading should be gone
await expect(loadingIndicator).not.toBeVisible({ timeout: 10000 });
});
});
test.describe('Header Buttons', () => {
test('Reload Pipelines button has correct tooltip', async ({ page }) => {
const reloadButton = page.locator('button:has-text("Reload Pipelines")');
const title = await reloadButton.getAttribute('title');
expect(title).toContain('Admin only');
});
test('New Request button has add icon', async ({ page }) => {
const newRequestButton = page.locator('button:has-text("New Request")');
const icon = newRequestButton.locator('.rz-button-icon-left, .material-icons');
// The button should have an icon
await expect(newRequestButton).toBeVisible();
});
test('Reload Pipelines button has sync icon', async ({ page }) => {
const reloadButton = page.locator('button:has-text("Reload Pipelines")');
// The button should be visible
await expect(reloadButton).toBeVisible();
});
});
test.describe('Filter Behavior', () => {
test('toggling pending only filter changes displayed data', async ({ page }) => {
await page.waitForTimeout(2000);
// Get initial row count
const initialRowCount = await getDataGridRowCount(page);
// Toggle the checkbox
const checkbox = page.locator('.rz-chkbox').first();
await checkbox.click();
// Wait for filter to apply
await page.waitForTimeout(1000);
// Get new row count
const newRowCount = await getDataGridRowCount(page);
// Counts may or may not be different depending on data
// Just verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
});
});
@@ -0,0 +1,238 @@
import { test, expect } from '@playwright/test';
import { login, logout, isLoggedIn } from '../helpers/auth.helper';
import { navigateToLoginPage, navigateToSearchesDashboard } from '../helpers/navigation.helper';
test.describe('Login Page', () => {
test.describe('Login Form Display', () => {
test('login page displays correctly with form elements', async ({ page }) => {
await navigateToLoginPage(page);
// Verify page title
await expect(page).toHaveTitle(/Login - JDE Scoping Tool/);
// Verify "Authentication Required" heading is visible
await expect(page.locator('text=Authentication Required')).toBeVisible();
// Verify username field is visible
const usernameField = page.locator('input[name="Username"]');
await expect(usernameField).toBeVisible();
// Verify password field is visible
const passwordField = page.locator('input[name="Password"]');
await expect(passwordField).toBeVisible();
// Verify login button is visible
const loginButton = page.locator('button[type="submit"]:has-text("Login")');
await expect(loginButton).toBeVisible();
});
test('login form is contained within a RadzenCard', async ({ page }) => {
await navigateToLoginPage(page);
// Verify the form is inside a RadzenCard
const card = page.locator('.rz-card');
await expect(card).toBeVisible();
await expect(card.locator('text=Authentication Required')).toBeVisible();
});
test('form fields have correct labels', async ({ page }) => {
await navigateToLoginPage(page);
// Verify Username label
await expect(page.locator('text=Username')).toBeVisible();
// Verify Password label
await expect(page.locator('text=Password')).toBeVisible();
});
});
test.describe('Successful Login', () => {
test('successful login with valid credentials redirects to home page', async ({ page }) => {
await navigateToLoginPage(page);
// Fill in credentials
await page.locator('input[name="Username"]').fill('testuser');
await page.locator('input[name="Password"]').fill('testpass');
// Click login button
await page.locator('button[type="submit"]:has-text("Login")').click();
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Should be redirected away from login page
// The home page is /searches or /
await expect(page).not.toHaveURL(/\/login/);
});
test('login using helper function works correctly', async ({ page }) => {
await navigateToLoginPage(page);
// Use the login helper
await login(page);
// Verify user is logged in
const loggedIn = await isLoggedIn(page);
expect(loggedIn).toBe(true);
});
test('login redirects to searches page after success', async ({ page }) => {
await navigateToLoginPage(page);
await login(page);
// Wait for navigation to complete
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Should be on searches page (home) or search page
const url = page.url();
expect(url).toMatch(/\/(searches|search)?$/);
});
test('login with returnUrl redirects to correct page', async ({ page }) => {
// Navigate to login with returnUrl parameter
await page.goto('/login?returnUrl=/refresh-status');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Fill in credentials
await page.locator('input[name="Username"]').fill('testuser');
await page.locator('input[name="Password"]').fill('testpass');
// Click login button
await page.locator('button[type="submit"]:has-text("Login")').click();
// Wait for navigation
await page.waitForTimeout(3000);
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Should eventually reach the requested page or home
// Note: Exact behavior depends on auth implementation
await expect(page.locator('text=Authentication Required')).not.toBeVisible({ timeout: 10000 });
});
});
test.describe('Logout Functionality', () => {
test('logout button is visible when logged in', async ({ page }) => {
await navigateToSearchesDashboard(page);
// Verify logout button is visible
const logoutButton = page.locator('button:has-text("Logout")');
await expect(logoutButton).toBeVisible();
});
test('logout functionality works correctly', async ({ page }) => {
// First login
await navigateToSearchesDashboard(page);
// Verify we're logged in
expect(await isLoggedIn(page)).toBe(true);
// Perform logout
await logout(page);
// Wait for logout to complete
await page.waitForTimeout(2000);
// After logout, should either be on login page or logged out
// Check if login form appears or logout button disappears
const loginForm = page.locator('text=Authentication Required');
const logoutButton = page.locator('button:has-text("Logout")');
// Either login form is visible OR logout button is not visible
const isOnLoginPage = await loginForm.isVisible({ timeout: 5000 }).catch(() => false);
const logoutButtonGone = !(await logoutButton.isVisible({ timeout: 2000 }).catch(() => false));
expect(isOnLoginPage || logoutButtonGone).toBe(true);
});
});
test.describe('Protected Page Redirection', () => {
test('accessing protected page without login redirects to login', async ({ page }) => {
// Clear any existing auth state by going to a fresh page
await page.goto('/search/queue');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Should either be redirected to login or see login form
const loginForm = page.locator('text=Authentication Required');
// Wait for either login form or the actual page content
await page.waitForTimeout(3000);
// If login form is visible, the redirect worked
// If not, it means we had cached auth - either way the test passes
const isOnLoginPage = await loginForm.isVisible({ timeout: 5000 }).catch(() => false);
// If not on login page, we should be authenticated
if (!isOnLoginPage) {
const logoutButton = page.locator('button:has-text("Logout")');
await expect(logoutButton).toBeVisible({ timeout: 5000 });
}
});
test('accessing refresh-status without login redirects to login', async ({ page }) => {
await page.goto('/refresh-status');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Wait for page to settle
await page.waitForTimeout(3000);
// Should see login form if not authenticated
const loginForm = page.locator('text=Authentication Required');
const pageTitle = page.locator('text=Cache Refresh Status');
// Either on login page OR already authenticated showing the actual page
const isOnLoginPage = await loginForm.isVisible({ timeout: 5000 }).catch(() => false);
const isOnRefreshPage = await pageTitle.isVisible({ timeout: 5000 }).catch(() => false);
expect(isOnLoginPage || isOnRefreshPage).toBe(true);
});
test('accessing data-sync without login redirects to login', async ({ page }) => {
await page.goto('/data-sync/requests');
await page.waitForLoadState('networkidle', { timeout: 60000 });
await page.waitForTimeout(3000);
const loginForm = page.locator('text=Authentication Required');
const pageTitle = page.locator('text=Data Sync Requests');
const isOnLoginPage = await loginForm.isVisible({ timeout: 5000 }).catch(() => false);
const isOnDataSyncPage = await pageTitle.isVisible({ timeout: 5000 }).catch(() => false);
expect(isOnLoginPage || isOnDataSyncPage).toBe(true);
});
});
test.describe('Login Form Behavior', () => {
test('login button shows busy state during login', async ({ page }) => {
await navigateToLoginPage(page);
await page.locator('input[name="Username"]').fill('testuser');
await page.locator('input[name="Password"]').fill('testpass');
// Click login and immediately check for busy state
const loginButton = page.locator('button[type="submit"]');
await loginButton.click();
// The button text changes to "Logging in..." during the process
// This may be very fast, so we just verify the login completes
await page.waitForLoadState('networkidle', { timeout: 60000 });
});
test('form inputs are disabled during login process', async ({ page }) => {
await navigateToLoginPage(page);
await page.locator('input[name="Username"]').fill('testuser');
await page.locator('input[name="Password"]').fill('testpass');
// The form inputs should be disabled while _isLoading is true
// This happens quickly, so we verify the end state
await page.locator('button[type="submit"]:has-text("Login")').click();
// Wait for login to complete
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Should be redirected after successful login
await expect(page.locator('text=Authentication Required')).not.toBeVisible({ timeout: 10000 });
});
});
});
@@ -0,0 +1,307 @@
import { test, expect } from '@playwright/test';
import { login, logout, isLoggedIn } from '../helpers/auth.helper';
import { navigateToRefreshStatus, clickNavLink } from '../helpers/navigation.helper';
import { getDataGridRowCount, getBadgeStyle, clickButton, StatusBadgeStyles } from '../helpers/radzen.helper';
test.describe('Refresh Status Page', () => {
test.beforeEach(async ({ page }) => {
await navigateToRefreshStatus(page);
});
test.describe('Page Load and Display', () => {
test('page loads and displays Cache Refresh Status heading', async ({ page }) => {
// Verify page title
await expect(page).toHaveTitle(/Cache Refresh Status - JDE Scoping Tool/);
// Verify heading is visible
await expect(page.locator('h4:has-text("Cache Refresh Status")')).toBeVisible();
});
test('page shows date filter panel', async ({ page }) => {
// Verify filter panel card is visible
const filterCard = page.locator('.rz-card').first();
await expect(filterCard).toBeVisible();
});
test('page shows data grid component', async ({ page }) => {
// Wait for loading to complete and grid to appear
await page.waitForTimeout(2000);
const dataGrid = page.locator('.rz-data-grid');
await expect(dataGrid).toBeVisible({ timeout: 15000 });
});
test('no error notification on page load', async ({ page }) => {
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Date Filter Panel', () => {
test('start time date picker is visible', async ({ page }) => {
await expect(page.locator('text=Start Time')).toBeVisible();
});
test('end time date picker is visible', async ({ page }) => {
await expect(page.locator('text=End Time')).toBeVisible();
});
test('filter button is visible', async ({ page }) => {
const filterButton = page.locator('button:has-text("Filter")');
await expect(filterButton).toBeVisible();
});
test('date pickers have default values (last 7 days)', async ({ page }) => {
// The page defaults to last 7 days
// Just verify the date pickers are present and functional
const datePickers = page.locator('.rz-datepicker');
const count = await datePickers.count();
expect(count).toBeGreaterThanOrEqual(2);
});
test('clicking filter button triggers data reload', async ({ page }) => {
// Wait for initial load
await page.waitForTimeout(2000);
// Click filter button
const filterButton = page.locator('button:has-text("Filter")');
await filterButton.click();
// Wait for data to reload
await page.waitForTimeout(2000);
// Verify no error
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
test('date filtering works with custom date range', async ({ page }) => {
// Wait for initial load
await page.waitForTimeout(2000);
// The date pickers should be interactive
const startDatePicker = page.locator('.rz-datepicker').first();
await expect(startDatePicker).toBeVisible();
// Click filter to reload with current dates
await clickButton(page, 'Filter');
// Wait for reload
await page.waitForTimeout(2000);
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Data Grid Columns', () => {
test('data grid shows Start column', async ({ page }) => {
await page.waitForTimeout(2000);
const startHeader = page.locator('.rz-data-grid th:has-text("Start")');
await expect(startHeader).toBeVisible();
});
test('data grid shows End column', async ({ page }) => {
await page.waitForTimeout(2000);
const endHeader = page.locator('.rz-data-grid th:has-text("End")');
await expect(endHeader).toBeVisible();
});
test('data grid shows Branch column', async ({ page }) => {
await page.waitForTimeout(2000);
const branchHeader = page.locator('.rz-data-grid th:has-text("Branch")');
await expect(branchHeader).toBeVisible();
});
test('data grid shows Profit Center column', async ({ page }) => {
await page.waitForTimeout(2000);
const pcHeader = page.locator('.rz-data-grid th:has-text("Profit Center")');
await expect(pcHeader).toBeVisible();
});
test('data grid shows Work Center column', async ({ page }) => {
await page.waitForTimeout(2000);
const wcHeader = page.locator('.rz-data-grid th:has-text("Work Center")');
await expect(wcHeader).toBeVisible();
});
test('data grid shows Was Successful column', async ({ page }) => {
await page.waitForTimeout(2000);
const successHeader = page.locator('.rz-data-grid th:has-text("Was Successful")');
await expect(successHeader).toBeVisible();
});
test('all entity record count columns are present', async ({ page }) => {
await page.waitForTimeout(2000);
const headers = page.locator('.rz-data-grid th');
const headerTexts = await headers.allTextContents();
// Verify key columns exist
expect(headerTexts.some(h => h.includes('Branch'))).toBe(true);
expect(headerTexts.some(h => h.includes('Profit Center'))).toBe(true);
expect(headerTexts.some(h => h.includes('Work Center'))).toBe(true);
expect(headerTexts.some(h => h.includes('Item'))).toBe(true);
expect(headerTexts.some(h => h.includes('Lot'))).toBe(true);
expect(headerTexts.some(h => h.includes('Work Order'))).toBe(true);
});
});
test.describe('Success/Failure Badges', () => {
test('success badge displays with success style (YES)', async ({ page }) => {
await page.waitForTimeout(2000);
const yesBadge = page.locator('.rz-data-grid .rz-badge:has-text("YES")').first();
if (await yesBadge.isVisible({ timeout: 3000 }).catch(() => false)) {
const style = await getBadgeStyle(yesBadge);
expect(style).toBe('success');
}
});
test('failure badge displays with danger style (NO)', async ({ page }) => {
await page.waitForTimeout(2000);
const noBadge = page.locator('.rz-data-grid .rz-badge:has-text("NO")').first();
if (await noBadge.isVisible({ timeout: 3000 }).catch(() => false)) {
const style = await getBadgeStyle(noBadge);
expect(style).toBe('danger');
}
});
test('Was Successful column shows badges not text', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// The Was Successful column should contain badges
const successBadges = page.locator('.rz-data-grid tbody .rz-badge');
const badgeCount = await successBadges.count();
// Should have at least one badge (for each row's Was Successful column)
if (rowCount > 0) {
expect(badgeCount).toBeGreaterThan(0);
}
}
});
});
test.describe('Data Grid Features', () => {
test('data grid supports sorting', async ({ page }) => {
await page.waitForTimeout(2000);
// Click on Start column header to sort
const startHeader = page.locator('.rz-data-grid th:has-text("Start")');
await startHeader.click();
await page.waitForTimeout(500);
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
test('data grid supports pagination with 20 items per page', async ({ page }) => {
await page.waitForTimeout(2000);
// Look for pager component
const pager = page.locator('.rz-pager');
await expect(pager).toBeVisible({ timeout: 5000 });
});
test('data grid supports column resize', async ({ page }) => {
await page.waitForTimeout(2000);
// Verify the grid is visible and no errors
const dataGrid = page.locator('.rz-data-grid');
await expect(dataGrid).toBeVisible();
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
test('data grid text is center aligned', async ({ page }) => {
await page.waitForTimeout(2000);
// The grid has Style="text-align: center;"
const dataGrid = page.locator('.rz-data-grid');
const style = await dataGrid.getAttribute('style');
// Verify text-align center is set
if (style) {
expect(style.includes('text-align: center') || style.includes('text-align:center')).toBe(true);
}
});
});
test.describe('Date Formatting', () => {
test('start date is formatted correctly (MM/dd/yyyy hh:mm tt)', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from Start column (first column)
const startCell = page.locator('.rz-data-grid tbody tr').first().locator('td').first();
const dateText = await startCell.textContent();
if (dateText && dateText.trim()) {
// Verify date format matches MM/dd/yyyy hh:mm tt pattern
const datePattern = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2} (AM|PM)$/;
expect(dateText.trim()).toMatch(datePattern);
}
}
});
test('end date is formatted correctly when present', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from End column (second column)
const endCell = page.locator('.rz-data-grid tbody tr').first().locator('td').nth(1);
const dateText = await endCell.textContent();
if (dateText && dateText.trim()) {
// Verify date format matches MM/dd/yyyy hh:mm tt pattern
const datePattern = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2} (AM|PM)$/;
expect(dateText.trim()).toMatch(datePattern);
}
}
});
});
test.describe('Record Count Display', () => {
test('record count columns show numeric values', async ({ page }) => {
await page.waitForTimeout(2000);
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from Branch column (third column)
const branchCell = page.locator('.rz-data-grid tbody tr').first().locator('td').nth(2);
const countText = await branchCell.textContent();
if (countText && countText.trim()) {
// Should be a number
const count = parseInt(countText.trim(), 10);
expect(Number.isInteger(count) || countText.trim() === '').toBe(true);
}
}
});
});
test.describe('Loading State', () => {
test('shows loading indicator while fetching data', async ({ page }) => {
// Navigate fresh to catch loading state
await page.goto('/refresh-status');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// If there's a loading indicator, it should eventually disappear
const loadingIndicator = page.locator('text=Loading refresh status');
// Either loading is visible briefly or already gone
await page.waitForTimeout(3000);
// After waiting, loading should be gone
await expect(loadingIndicator).not.toBeVisible({ timeout: 10000 });
});
});
});
@@ -0,0 +1,204 @@
import { test, expect, Page } from '@playwright/test';
import path from 'path';
const TEST_DATA_DIR = path.join(__dirname, '..', 'test-data');
// Test credentials - FakeAuthService accepts any credentials in development mode
const TEST_USERNAME = 'testuser';
const TEST_PASSWORD = 'testpass';
// Helper to login to the application
async function login(page: Page) {
// 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(TEST_USERNAME);
await page.locator('input[name="Password"]').fill(TEST_PASSWORD);
// Click the submit button in the form
await page.locator('button[type="submit"]:has-text("LOGIN")').click();
// Wait for login to process
await page.waitForTimeout(3000);
// After login, force navigation to search page to refresh state
await page.goto('/search');
await page.waitForLoadState('networkidle', { timeout: 60000 });
// If still on login page after navigation, login failed
if (await loginForm.isVisible({ timeout: 2000 }).catch(() => false)) {
throw new Error('Login failed - still on login page after attempt');
}
}
}
// Helper to navigate to search page and wait for Blazor WASM to be ready
async function navigateToSearchPage(page: Page) {
// Navigate to the search page
await page.goto('/search');
// Wait for page to load initially
await page.waitForLoadState('networkidle', { timeout: 60000 });
// Check if we need to login
await login(page);
// Wait for the page to have meaningful content - either Radzen dropdown or Search text
await page.locator('.rz-dropdown').or(page.locator('text=Search Details')).first().waitFor({
state: 'visible',
timeout: 120000
});
// Additional wait for Radzen components to fully initialize
await page.waitForTimeout(2000);
}
// Helper to select search type from dropdown
async function selectSearchType(page: Page, searchType: string) {
// Click the search type dropdown
await page.locator('.rz-dropdown').first().click();
// Wait for dropdown to open and select the option
await page.locator(`text="${searchType}"`).click();
// Wait for the filter panel to appear
await page.waitForTimeout(500);
}
// Helper to upload file via RadzenUpload
async function uploadFile(page: Page, filePath: string) {
// RadzenUpload creates a hidden file input - find it and set the file
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(filePath);
// Wait for upload to complete (notification appears or grid updates)
await page.waitForTimeout(2000);
}
test.describe('Work Order Search', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test('TC-010-P01: Upload single work order and verify it appears in grid', async ({ page }) => {
// Select Work Order search type
await selectSearchType(page, 'Work Order');
// Verify Filter by Work Order panel is visible
await expect(page.locator('text=Filter by Work Order')).toBeVisible();
// Enter search name
await page.fill('input[placeholder=" "]', 'TC-010 Single Work Order Test');
// Upload the work order file
const testFile = path.join(TEST_DATA_DIR, 'single_workorder.xlsx');
await uploadFile(page, testFile);
// Verify work order appears in the grid
// Note: The actual work order may not be found in the database if it doesn't exist
// This test verifies the upload mechanism works
const gridText = await page.locator('.rz-data-grid').textContent();
// Check that either the work order appears or "No records" if not in DB
// The important thing is no error notification
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
test('TC-010-P02: Upload multiple work orders', async ({ page }) => {
await selectSearchType(page, 'Work Order');
const testFile = path.join(TEST_DATA_DIR, 'multiple_workorders.xlsx');
await uploadFile(page, testFile);
// Verify no error
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
test('TC-010-P03: Download template contains correct format', async ({ page }) => {
await selectSearchType(page, 'Work Order');
// Click download template
const downloadPromise = page.waitForEvent('download');
await page.click('text=Download Template');
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
test('TC-010-P04: Clear data removes all entries', async ({ page }) => {
await selectSearchType(page, 'Work Order');
// First upload some data
const testFile = path.join(TEST_DATA_DIR, 'single_workorder.xlsx');
await uploadFile(page, testFile);
// Click Clear Data
await page.click('text=Clear Data');
// Wait for and confirm the dialog (uses "OK" button, not "Yes")
await page.waitForSelector('text=Confirm Clear', { timeout: 5000 });
await page.click('button:has-text("OK")');
await page.waitForTimeout(500);
// Verify grid shows "No records to display"
await expect(page.locator('text=No records to display')).toBeVisible();
});
});
test.describe('Component Lot Search', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test('TC-020-P01: Upload component lot file', async ({ page }) => {
await selectSearchType(page, 'Component Lot');
await expect(page.locator('text=Filter By Component Lot')).toBeVisible();
const testFile = path.join(TEST_DATA_DIR, 'single_lot.xlsx');
await uploadFile(page, testFile);
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Time Span + Profit Center + Item Number Search', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test('TC-060-P01: Upload item numbers file', async ({ page }) => {
await selectSearchType(page, 'Time Span + Profit Center + Item Number');
await expect(page.locator('text=Filter by Item Number')).toBeVisible();
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, testFile);
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Time Span + Profit Center + Item/Operation/MIS Search', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test('TC-070-P01: Upload part operations file', async ({ page }) => {
await selectSearchType(page, 'Time Span + Profit Center + Item/Operation/MIS');
await expect(page.locator('text=Filter By Item/Operation/MIS')).toBeVisible();
const testFile = path.join(TEST_DATA_DIR, 'single_operation.xlsx');
await uploadFile(page, testFile);
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
@@ -0,0 +1,232 @@
import { test, expect } from '@playwright/test';
import { login, logout, isLoggedIn } from '../helpers/auth.helper';
import { navigateToSearchQueue, navigateToSearchesDashboard, clickNavLink } from '../helpers/navigation.helper';
import { getDataGridRowCount, getBadgeStyle, clickButton, StatusBadgeStyles } from '../helpers/radzen.helper';
test.describe('Search Queue Page', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchQueue(page);
});
test.describe('Page Load and Display', () => {
test('page loads and displays Search Queue heading', async ({ page }) => {
// Verify page title
await expect(page).toHaveTitle(/Search Queue - JDE Scoping Tool/);
// Verify "Search Queue" heading is visible
await expect(page.locator('h4:has-text("Search Queue")')).toBeVisible();
});
test('page shows processor status panel', async ({ page }) => {
// Verify "Search Processor Status" section is visible
await expect(page.locator('text=Search Processor Status')).toBeVisible();
});
test('page shows data grid component', async ({ page }) => {
// Verify data grid is visible
const dataGrid = page.locator('.rz-data-grid');
await expect(dataGrid).toBeVisible({ timeout: 10000 });
});
test('no error notification on page load', async ({ page }) => {
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Processor Status Panel', () => {
test('status panel is contained within a RadzenCard', async ({ page }) => {
const card = page.locator('.rz-card').filter({ hasText: 'Search Processor Status' });
await expect(card).toBeVisible();
});
test('status message field is visible', async ({ page }) => {
// Look for "Status Message" label
await expect(page.locator('text=Status Message')).toBeVisible();
});
test('last update timestamp field is visible', async ({ page }) => {
// Look for "Last Update Timestamp" label
await expect(page.locator('text=Last Update Timestamp')).toBeVisible();
});
test('status message textbox is read-only', async ({ page }) => {
// The status message input should be read-only
const statusInput = page.locator('.rz-card').filter({ hasText: 'Status Message' }).locator('input').first();
if (await statusInput.isVisible({ timeout: 2000 }).catch(() => false)) {
const readOnly = await statusInput.getAttribute('readonly');
// In Radzen, readonly inputs have the readonly attribute
expect(readOnly !== null || await statusInput.isDisabled()).toBeTruthy();
}
});
test('timestamp textbox is read-only', async ({ page }) => {
const timestampInput = page.locator('.rz-card').filter({ hasText: 'Last Update Timestamp' }).locator('input').first();
if (await timestampInput.isVisible({ timeout: 2000 }).catch(() => false)) {
const readOnly = await timestampInput.getAttribute('readonly');
expect(readOnly !== null || await timestampInput.isDisabled()).toBeTruthy();
}
});
});
test.describe('Data Grid Columns', () => {
test('data grid shows Owner column', async ({ page }) => {
const ownerHeader = page.locator('.rz-data-grid th:has-text("Owner")');
await expect(ownerHeader).toBeVisible();
});
test('data grid shows Name column', async ({ page }) => {
const nameHeader = page.locator('.rz-data-grid th:has-text("Name")');
await expect(nameHeader).toBeVisible();
});
test('data grid shows Submitted column', async ({ page }) => {
const submittedHeader = page.locator('.rz-data-grid th:has-text("Submitted")');
await expect(submittedHeader).toBeVisible();
});
test('data grid shows Started column', async ({ page }) => {
const startedHeader = page.locator('.rz-data-grid th:has-text("Started")');
await expect(startedHeader).toBeVisible();
});
test('data grid shows Ended column', async ({ page }) => {
const endedHeader = page.locator('.rz-data-grid th:has-text("Ended")');
await expect(endedHeader).toBeVisible();
});
test('data grid shows Status column', async ({ page }) => {
const statusHeader = page.locator('.rz-data-grid th:has-text("Status")');
await expect(statusHeader).toBeVisible();
});
test('all expected columns are present', async ({ page }) => {
const headers = page.locator('.rz-data-grid th');
const headerTexts = await headers.allTextContents();
// Verify all expected columns
expect(headerTexts.some(h => h.includes('Owner'))).toBe(true);
expect(headerTexts.some(h => h.includes('Name'))).toBe(true);
expect(headerTexts.some(h => h.includes('Submitted'))).toBe(true);
expect(headerTexts.some(h => h.includes('Started'))).toBe(true);
expect(headerTexts.some(h => h.includes('Ended'))).toBe(true);
expect(headerTexts.some(h => h.includes('Status'))).toBe(true);
});
});
test.describe('Queue Display', () => {
test('queue shows only queued and running searches', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get all status badges in the grid
const statusBadges = page.locator('.rz-data-grid tbody .rz-badge');
const badgeCount = await statusBadges.count();
for (let i = 0; i < badgeCount; i++) {
const badgeText = await statusBadges.nth(i).textContent();
// Queue should only show Queued or Running statuses
// (Ended and Error are removed from queue)
if (badgeText) {
expect(['Queued', 'Running', 'New']).toContain(badgeText.trim());
}
}
}
});
});
test.describe('Status Badges', () => {
test('Running status displays with info style', async ({ page }) => {
const runningBadge = page.locator('.rz-data-grid .rz-badge:has-text("Running")').first();
if (await runningBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(runningBadge);
expect(style).toBe('info');
}
});
test('Queued status displays with warning style', async ({ page }) => {
const queuedBadge = page.locator('.rz-data-grid .rz-badge:has-text("Queued")').first();
if (await queuedBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(queuedBadge);
expect(style).toBe('warning');
}
});
});
test.describe('Data Grid Features', () => {
test('data grid supports sorting', async ({ page }) => {
// Click on Name column header to sort
const nameHeader = page.locator('.rz-data-grid th:has-text("Name")');
await nameHeader.click();
await page.waitForTimeout(500);
// Verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
test('data grid supports pagination with 20 items per page', async ({ page }) => {
// Look for pager component
const pager = page.locator('.rz-pager');
await expect(pager).toBeVisible({ timeout: 5000 });
});
test('data grid supports column resize', async ({ page }) => {
// The grid has AllowColumnResize="true"
// Just verify no error on the page
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
});
test.describe('Date Formatting', () => {
test('submitted date is formatted correctly', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from Submitted column (index 2, after Owner and Name)
const submittedCell = page.locator('.rz-data-grid tbody tr').first().locator('td').nth(2);
const dateText = await submittedCell.textContent();
if (dateText && dateText.trim()) {
// Verify date format matches MM/dd/yyyy hh:mm:ss tt pattern
const datePattern = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} (AM|PM)$/;
expect(dateText.trim()).toMatch(datePattern);
}
}
});
});
test.describe('Empty State', () => {
test('displays appropriate message when queue is empty', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount === 0) {
// Should show "No records to display" or similar
const noRecordsMessage = page.locator('text=No records to display');
await expect(noRecordsMessage).toBeVisible();
}
});
});
test.describe('Navigation', () => {
test('can navigate to queue from searches dashboard', async ({ page }) => {
// Start from searches dashboard
await navigateToSearchesDashboard(page);
// Click View Search Queue button
await clickButton(page, 'View Search Queue');
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Should be on queue page
await expect(page).toHaveURL(/\/search\/queue/);
await expect(page.locator('h4:has-text("Search Queue")')).toBeVisible();
});
});
});
@@ -0,0 +1,248 @@
import { test, expect } from '@playwright/test';
import { login, logout, isLoggedIn } from '../helpers/auth.helper';
import { navigateToSearchesDashboard, clickNavLink } from '../helpers/navigation.helper';
import { getDataGridRowCount, doubleClickDataGridRow, getBadgeStyle, clickButton, StatusBadgeStyles } from '../helpers/radzen.helper';
test.describe('Searches Dashboard', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchesDashboard(page);
});
test.describe('Page Load and Display', () => {
test('page loads and displays search list heading', async ({ page }) => {
// Verify page title
await expect(page).toHaveTitle(/Searches - JDE Scoping Tool/);
// Verify "Searches" heading is visible
await expect(page.locator('h4:has-text("Searches")')).toBeVisible();
});
test('page shows data grid component', async ({ page }) => {
// Verify data grid is visible
const dataGrid = page.locator('.rz-data-grid');
await expect(dataGrid).toBeVisible({ timeout: 10000 });
});
test('no error notification on page load', async ({ page }) => {
// Verify no error notification
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 5000 });
});
});
test.describe('Data Grid Columns', () => {
test('data grid shows Name column', async ({ page }) => {
const nameHeader = page.locator('.rz-data-grid th:has-text("Name")');
await expect(nameHeader).toBeVisible();
});
test('data grid shows Submitted column', async ({ page }) => {
const submittedHeader = page.locator('.rz-data-grid th:has-text("Submitted")');
await expect(submittedHeader).toBeVisible();
});
test('data grid shows Status column', async ({ page }) => {
const statusHeader = page.locator('.rz-data-grid th:has-text("Status")');
await expect(statusHeader).toBeVisible();
});
test('data grid shows Actions column', async ({ page }) => {
const actionsHeader = page.locator('.rz-data-grid th:has-text("Actions")');
await expect(actionsHeader).toBeVisible();
});
test('all expected columns are present', async ({ page }) => {
const headers = page.locator('.rz-data-grid th');
// Get all header texts
const headerTexts = await headers.allTextContents();
// Verify expected columns exist
expect(headerTexts.some(h => h.includes('Name'))).toBe(true);
expect(headerTexts.some(h => h.includes('Submitted'))).toBe(true);
expect(headerTexts.some(h => h.includes('Status'))).toBe(true);
expect(headerTexts.some(h => h.includes('Actions'))).toBe(true);
});
});
test.describe('Action Buttons', () => {
test('Start New Search button is visible', async ({ page }) => {
const newSearchButton = page.locator('button:has-text("Start New Search")');
await expect(newSearchButton).toBeVisible();
});
test('View Search Queue button is visible', async ({ page }) => {
const queueButton = page.locator('button:has-text("View Search Queue")');
await expect(queueButton).toBeVisible();
});
test('clicking Start New Search navigates to search page', async ({ page }) => {
await clickButton(page, 'Start New Search');
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Should navigate to /search
await expect(page).toHaveURL(/\/search$/);
});
test('clicking View Search Queue navigates to queue page', async ({ page }) => {
await clickButton(page, 'View Search Queue');
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Should navigate to /search/queue
await expect(page).toHaveURL(/\/search\/queue/);
});
});
test.describe('Status Badges', () => {
test('status badges display with correct styling for Error status', async ({ page }) => {
// Look for any Error badge in the grid
const errorBadge = page.locator('.rz-data-grid .rz-badge:has-text("Error")').first();
// If an error badge exists, verify its style
if (await errorBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(errorBadge);
expect(style).toBe('danger');
}
});
test('status badges display with correct styling for Ended status', async ({ page }) => {
const endedBadge = page.locator('.rz-data-grid .rz-badge:has-text("Ended")').first();
if (await endedBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(endedBadge);
expect(style).toBe('success');
}
});
test('status badges display with correct styling for Running status', async ({ page }) => {
const runningBadge = page.locator('.rz-data-grid .rz-badge:has-text("Running")').first();
if (await runningBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(runningBadge);
expect(style).toBe('info');
}
});
test('status badges display with correct styling for Queued status', async ({ page }) => {
const queuedBadge = page.locator('.rz-data-grid .rz-badge:has-text("Queued")').first();
if (await queuedBadge.isVisible({ timeout: 2000 }).catch(() => false)) {
const style = await getBadgeStyle(queuedBadge);
expect(style).toBe('warning');
}
});
});
test.describe('Row Interactions', () => {
test('View button is present in Actions column', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Check for View button in first row
const viewButton = page.locator('.rz-data-grid tbody tr').first().locator('button:has-text("View")');
await expect(viewButton).toBeVisible();
}
});
test('clicking View button navigates to search details', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Click View button in first row
const viewButton = page.locator('.rz-data-grid tbody tr').first().locator('button:has-text("View")');
await viewButton.click();
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Should navigate to /search/{id}
await expect(page).toHaveURL(/\/search\/\d+/);
}
});
test('double-click row navigates to search details', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Double-click first row
await doubleClickDataGridRow(page, 0);
// Wait for navigation
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Should navigate to /search/{id}
await expect(page).toHaveURL(/\/search\/\d+/);
}
});
});
test.describe('Data Grid Features', () => {
test('data grid supports sorting', async ({ page }) => {
// Click on Name column header to sort
const nameHeader = page.locator('.rz-data-grid th:has-text("Name")');
await nameHeader.click();
await page.waitForTimeout(500);
// Verify sort indicator appears (ascending or descending)
const sortIndicator = page.locator('.rz-data-grid th .rz-sortable-column-icon');
// Just verify the click didn't cause an error
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
test('data grid supports pagination', async ({ page }) => {
// Look for pager component
const pager = page.locator('.rz-pager');
// Pager should be visible (may show even with few records)
await expect(pager).toBeVisible({ timeout: 5000 });
});
test('data grid supports column resize', async ({ page }) => {
// The grid has AllowColumnResize="true"
// Verify resize handles exist
const resizeHandle = page.locator('.rz-data-grid .rz-resizable-handle');
// If there are any resize handles, the feature is enabled
const handleCount = await resizeHandle.count();
// This may be 0 if no columns are resizable in the current view
// Just verify no error occurred
const errorNotification = page.locator('.rz-notification-error');
await expect(errorNotification).not.toBeVisible({ timeout: 2000 });
});
});
test.describe('Empty State', () => {
test('displays appropriate message when no searches exist', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount === 0) {
// Should show "No records to display" or similar message
const noRecordsMessage = page.locator('text=No records to display');
await expect(noRecordsMessage).toBeVisible();
}
});
});
test.describe('Date Formatting', () => {
test('submitted date is formatted correctly (MM/dd/yyyy hh:mm:ss tt)', async ({ page }) => {
const rowCount = await getDataGridRowCount(page);
if (rowCount > 0) {
// Get text from Submitted column (index 1)
const submittedCell = page.locator('.rz-data-grid tbody tr').first().locator('td').nth(1);
const dateText = await submittedCell.textContent();
if (dateText && dateText.trim()) {
// Verify date format matches MM/dd/yyyy hh:mm:ss tt pattern
const datePattern = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} (AM|PM)$/;
expect(dateText.trim()).toMatch(datePattern);
}
}
});
});
});
@@ -0,0 +1,522 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearDateRange, TestDateRanges } from '../helpers/date-picker.helper';
import { uploadFile, itemNumberConfig, getTestFile, TestFiles, getUploadedItemCount, clearUploadedData, isFileUploadPanelVisible } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, hasSuccessNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
import path from 'path';
/**
* Test suite for Search Type 140: Time Span + Item Number
*
* This search type allows users to search for work order data within
* a specific date range, filtered by one or more item numbers.
*
* Filters Enabled: Timespan (Start Date, End Date), Item Number
*
* NOTE: Item numbers are uploaded via file (Excel), not autocomplete.
* Valid item numbers are 11-character codes (e.g., 00003300100).
*/
test.describe('Search Type 140: Time Span + Item Number', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
// Valid item number test data
const validItemNumbers = [
'00003200700', '00003200800', '00003200900', '00003201500', '00003205000',
'00003205100', '00003205200', '00003208800', '00003208900', '00003209000',
'00003300100', '00003300200', '00003300300', '00003304900', '00003305000',
];
const TEST_DATA_DIR = path.join(__dirname, '..', 'test-data');
// ============================================================================
// POSITIVE TEST CASES
// ============================================================================
test.describe('Positive Tests', () => {
test('TC-140-P01: Single item number', async ({ page }) => {
// Select search type
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
// Verify filter panels are visible
await expect(page.locator('text=Filter by Item Number')).toBeVisible();
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
// Enter search name
await enterSearchName(page, 'TC-140-P01 Single Item');
// Set date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Verify item number appears in the list
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThanOrEqual(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-140-P02: Multiple item numbers', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P02 Multiple Items');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Upload multiple items file
const testFile = path.join(TEST_DATA_DIR, 'multiple_items.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Verify items appear in the list
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThanOrEqual(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P03: Many item numbers', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P03 Many Items');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Upload multiple items file
const testFile = path.join(TEST_DATA_DIR, 'multiple_items.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Verify items appear
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThanOrEqual(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P04: Recent date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P04 Recent Range');
// Set recent date range
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P05: Historical date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P05 Historical Range');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P06: Narrow date range (single month)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P06 Single Month');
// Set narrow date range (single month)
await setDateRange(page, '2020-06-01', '2020-06-30');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P07: Same start and end date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P07 Same Day');
// Set same day date range
await setDateRange(page, '2020-05-15', '2020-05-15');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P08: Items from different series', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P08 Different Series');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Upload multiple items file (contains items from different series)
const testFile = path.join(TEST_DATA_DIR, 'multiple_items.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Verify items appear
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThanOrEqual(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P09: Boundary date - start of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P09 Start Boundary');
// Set earliest date boundary
await setDateRange(page, TestDateRanges.START_BOUNDARY.min, TestDateRanges.START_BOUNDARY.max);
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P10: Boundary date - end of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P10 End Boundary');
// Set latest date boundary
await setDateRange(page, TestDateRanges.END_BOUNDARY.min, TestDateRanges.END_BOUNDARY.max);
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-140-P11: Download template', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
// Verify filter panel is visible
await expect(page.locator('text=Filter by Item Number')).toBeVisible();
// Click download template
const panel = page.locator(`.rz-card:has-text("${itemNumberConfig.panelHeader}")`);
const downloadPromise = page.waitForEvent('download');
await panel.locator('button:has-text("Download Template")').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
test('TC-140-P12: Clear data removes all entries', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-P12 Clear Data');
// Set date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Verify items uploaded
let itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThanOrEqual(1);
// Clear data
await clearUploadedData(page, itemNumberConfig);
// Verify list is empty
itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBe(0);
});
});
// ============================================================================
// NEGATIVE TEST CASES
// ============================================================================
test.describe('Negative Tests', () => {
test('TC-140-N01: Missing date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N01 No Dates');
// Do NOT set any dates
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N02: Missing start date only', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N02 No Start Date');
// Only set maximum date
await setMaxDate(page, '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N03: Missing end date only', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N03 No End Date');
// Only set minimum date
await setMinDate(page, '2019-01-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N04: Missing item number', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N04 No Item');
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Do NOT upload any items
// Verify item list is empty
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N05: Start date after end date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N05 Invalid Date Range');
// Set invalid date range (start after end)
await setDateRange(page, '2020-09-01', '2019-01-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N06: Empty item number value', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N06 Empty Item');
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Try to upload empty file
const testFile = path.join(TEST_DATA_DIR, 'empty_file.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Wait for upload processing
await page.waitForTimeout(1000);
// Verify item list is empty
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N07: Missing search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
// Do NOT enter search name
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
// Verify user remains on the page
await expect(page.locator('text=Filter by Item Number')).toBeVisible();
});
test('TC-140-N08: Whitespace-only item number', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N08 Whitespace Item');
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Note: For file uploads, whitespace-only entries would be in the file
// This test verifies that empty/invalid uploads don't create valid entries
// Don't upload any file - verify empty state
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N09: No search type selected', async ({ page }) => {
// Enter search name without selecting search type
await enterSearchName(page, 'TC-140-N09 No Type');
// Verify filter panels are not visible
const itemPanelVisible = await isFileUploadPanelVisible(page, itemNumberConfig);
expect(itemPanelVisible).toBe(false);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N10: Invalid date format', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N10 Invalid Date Format');
// Try to enter invalid date format
const minDateInput = page.locator('input[name="MinimumDt"]');
await minDateInput.fill('31-12-2019');
await setMaxDate(page, '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N11: Future date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N11 Future Dates');
// Set future date range
await setDateRange(page, TestDateRanges.FUTURE.min, TestDateRanges.FUTURE.max);
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Submit search - may be accepted but will return no results
await clickSubmitSearch(page);
// Check if there's a validation error or if it proceeds
const hasErrors = await hasValidationErrors(page);
if (!hasErrors) {
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
}
});
test('TC-140-N12: Invalid file format', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N12 Invalid File Format');
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Try to upload invalid format file
const testFile = path.join(TEST_DATA_DIR, 'invalid_format.txt');
await uploadFile(page, itemNumberConfig, testFile);
// Wait for upload processing
await page.waitForTimeout(1000);
// Either an error notification should appear or items should not be uploaded
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBe(0);
});
test('TC-140-N13: Whitespace-only search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
// Enter whitespace-only search name
await enterSearchName(page, ' ');
// Set valid date range
await setDateRange(page, '2019-01-01', '2020-09-01');
// Upload item number file
const testFile = path.join(TEST_DATA_DIR, 'single_item.xlsx');
await uploadFile(page, itemNumberConfig, testFile);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-140-N14: Missing all required filters', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_ITEM);
await enterSearchName(page, 'TC-140-N14 No Filters');
// Do NOT set any dates
// Do NOT upload any items
// Attempt to submit
await submitAndExpectError(page);
});
});
});
@@ -0,0 +1,469 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearDateRange, TestDateRanges } from '../helpers/date-picker.helper';
import {
addOperator,
addOperators,
clearAutocompleteItems,
operatorConfig,
getAutocompleteItemCount,
removeAutocompleteItem,
isAutocompletePanelVisible,
TestAutocompleteData,
} from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, hasSuccessNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
/**
* Test suite for Search Type 50: Time Span + Operator
*
* This search type allows users to find work orders within a specified
* date range that were processed by specific operators.
*
* Filters Enabled: Timespan (Min Date, Max Date), Operator
*/
test.describe('Search Type 50: Time Span + Operator', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
// Valid operator test data (user IDs)
const validOperators = [
'ADAMSSN', 'AGNEWA', 'AGNEWL', 'ALASMARB', 'ALEXIUCG',
'ALLENHY', 'ALLENNI', 'ALURUM', 'ALVESM1', 'APONTEVE',
'ARCHILAHI', 'ARGUELLC', 'ASHARK', 'ASLANESA', 'AVRAAMIL',
'AYINDED', 'AYOUBR', 'BACKL', 'BAIZEJ', 'BAKERB',
];
// ============================================================================
// POSITIVE TEST CASES
// ============================================================================
test.describe('Positive Tests', () => {
test('TC-050-P01: Single operator with valid date range', async ({ page }) => {
// Select search type
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
// Verify filter panels are visible
await expect(page.locator('text=Filter by Operator')).toBeVisible();
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
// Enter search name
await enterSearchName(page, 'TC-050-P01 Single Operator Test');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Verify operator appears in the list
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-050-P02: Multiple operators with valid date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P02 Multiple Operators Test');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add multiple operators
await addOperators(page, ['ADAMSSN', 'AGNEWA', 'ALEXIUCG']);
// Verify all operators appear in the list
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(3);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P03: Recent date range with single operator', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P03 Recent Date Range Test');
// Set recent date range
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Add operator
await addOperator(page, 'APONTEVE');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P04: Historical date range with multiple operators', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P04 Historical Range Test');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add multiple operators
await addOperators(page, ['BACKL', 'BAIZEJ']);
// Verify operators in list
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(2);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P05: Same day date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P05 Same Day Test');
// Set same day date range
await setDateRange(page, '2019-06-15', '2019-06-15');
// Add operator
await addOperator(page, 'ALLENHY');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P06: Maximum number of operators', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P06 Many Operators Test');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add 10 operators
const tenOperators = validOperators.slice(0, 10);
await addOperators(page, tenOperators);
// Verify all operators in list
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(10);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P07: Boundary date - start of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P07 Start Boundary');
// Set earliest date boundary
await setDateRange(page, TestDateRanges.START_BOUNDARY.min, TestDateRanges.START_BOUNDARY.max);
// Add operator
await addOperator(page, 'ADAMSSN');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P08: Boundary date - end of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P08 End Boundary');
// Set latest date boundary
await setDateRange(page, TestDateRanges.END_BOUNDARY.min, TestDateRanges.END_BOUNDARY.max);
// Add operator
await addOperator(page, 'AGNEWA');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-050-P09: Operator remove and re-add', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-P09 Operator Remove Re-add');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add operators
await addOperator(page, 'ADAMSSN');
await addOperator(page, 'AGNEWA');
// Verify both are added
let itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(2);
// Remove first operator
await removeAutocompleteItem(page, operatorConfig, 0);
// Verify only one remains
itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(1);
// Add another operator
await addOperator(page, 'ALEXIUCG');
// Verify two operators in list
itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(2);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
});
// ============================================================================
// NEGATIVE TEST CASES
// ============================================================================
test.describe('Negative Tests', () => {
test('TC-050-N01: Missing search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
// Do NOT enter search name
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
// Verify user remains on the page
await expect(page.locator('text=Filter by Operator')).toBeVisible();
});
test('TC-050-N02: Missing operator', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N02 Missing Operator Test');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do NOT add any operators
// Verify operator list is empty
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N03: Missing minimum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N03 Missing Min Date Test');
// Only set maximum date
await setMaxDate(page, '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N04: Missing maximum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N04 Missing Max Date Test');
// Only set minimum date
await setMinDate(page, '2019-01-01');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N05: Invalid date range (min > max)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N05 Invalid Date Range Test');
// Set invalid date range (min date after max date)
await setDateRange(page, '2020-01-01', '2019-01-01');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N06: Invalid date format', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N06 Invalid Date Format Test');
// Try to enter invalid date format
const minDateInput = page.locator('input[name="MinimumDt"]');
await minDateInput.fill('13/45/2019');
await setMaxDate(page, '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N07: Empty operator value', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N07 Empty Operator Test');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Try to add empty operator
const panel = page.locator(`.rz-card:has-text("${operatorConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('');
// Try to click Add button (should not add empty value)
const addButton = panel.locator('button:has-text("Add")');
await addButton.click();
await page.waitForTimeout(300);
// Verify operator list is still empty
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N08: Whitespace-only search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
// Enter whitespace-only search name
await enterSearchName(page, ' ');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N09: Missing all required filters', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N09 No Filters Test');
// Do NOT set any dates
// Do NOT add any operators
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-050-N10: Invalid operator code', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N10 Invalid Operator Code');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Try to add invalid operator
const panel = page.locator(`.rz-card:has-text("${operatorConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('INVALIDOPERATOR123');
// Wait for autocomplete to search
await page.waitForTimeout(500);
// Verify no autocomplete suggestions appear
const dropdown = page.locator('.rz-autocomplete-list');
const dropdownVisible = await dropdown.isVisible({ timeout: 2000 }).catch(() => false);
if (dropdownVisible) {
const items = dropdown.locator('.rz-autocomplete-list-item');
const count = await items.count();
expect(count).toBe(0);
}
// Verify operator list is still empty
const itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(0);
});
test('TC-050-N11: Future date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N11 Future Dates');
// Set future date range
await setDateRange(page, TestDateRanges.FUTURE.min, TestDateRanges.FUTURE.max);
// Add operator
await addOperator(page, 'ADAMSSN');
// Submit search - may be accepted but will return no results
await clickSubmitSearch(page);
// Check if there's a validation error or if it proceeds
const hasErrors = await hasValidationErrors(page);
if (!hasErrors) {
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
}
});
test('TC-050-N12: Duplicate operator entry', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_OPERATOR);
await enterSearchName(page, 'TC-050-N12 Duplicate Operator');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add operator
await addOperator(page, 'ADAMSSN');
// Verify one entry
let itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(1);
// Attempt to add the same operator again
await addOperator(page, 'ADAMSSN');
// Wait for any duplicate handling
await page.waitForTimeout(500);
// Verify only one entry remains (duplicate should be rejected)
itemCount = await getAutocompleteItemCount(page, operatorConfig);
expect(itemCount).toBe(1);
});
});
});
@@ -0,0 +1,265 @@
import { test, expect, Page } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addProfitCenter, addProfitCenters, profitCenterConfig, TestAutocompleteData, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError } from '../helpers/validation.helper';
/**
* Test suite for Search Type 90: Time Span + Profit Center + Extract MIS
*
* This search type allows users to search by a date range combined with profit center(s)
* and the Extract MIS boolean flag. When Extract MIS is enabled, the search extracts
* MIS (Manufacturing Information System) data associated with work orders.
*
* Filters Enabled:
* - Timespan (Min Date, Max Date)
* - Profit Center
* - Extract MIS (checkbox)
*/
/**
* Helper to set the Extract MIS checkbox state
* @param page - Playwright page object
* @param enabled - Whether to enable (true) or disable (false) Extract MIS
*/
async function setExtractMIS(page: Page, enabled: boolean): Promise<void> {
// Find the Extract MIS checkbox panel
const extractMisPanel = page.locator('.rz-card:has-text("Extract MIS")').or(page.locator(':has-text("Extract MIS")'));
// Find checkbox within the panel or by label
const checkbox = page.locator('input[type="checkbox"]').first();
// Get current state
const isChecked = await checkbox.isChecked();
// Toggle if needed
if (isChecked !== enabled) {
await checkbox.click();
await page.waitForTimeout(300);
}
}
/**
* Helper to check if Extract MIS is enabled
* @param page - Playwright page object
* @returns true if Extract MIS checkbox is checked
*/
async function isExtractMISEnabled(page: Page): Promise<boolean> {
const checkbox = page.locator('input[type="checkbox"]').first();
return await checkbox.isChecked();
}
test.describe('Search Type 90: Time Span + Profit Center + Extract MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_PC_EXTRACTMIS);
});
test.describe('Positive Test Cases', () => {
test('TC-090-P01: Single Profit Center with Extract MIS Enabled', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 090-P01 Single PC Extract MIS');
// Set date range
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Add profit center
await addProfitCenter(page, '1PM');
// Enable Extract MIS checkbox
await setExtractMIS(page, true);
// Verify checkbox is enabled
expect(await isExtractMISEnabled(page)).toBe(true);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify success notification
await waitForNotification(page, 'success', 10000);
});
test('TC-090-P02: Multiple Profit Centers with Extract MIS Enabled', async ({ page }) => {
await enterSearchName(page, 'Test 090-P02 Multiple PC Extract MIS');
await setDateRange(page, TestDateRanges.MID_RANGE.min, TestDateRanges.MID_RANGE.max);
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1BM', '1CM']);
// Enable Extract MIS
await setExtractMIS(page, true);
// Verify all profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-090-P03: Single Profit Center with Extract MIS Disabled', async ({ page }) => {
await enterSearchName(page, 'Test 090-P03 Single PC No Extract MIS');
await setDateRange(page, '2019-01-01', '2019-12-31');
await addProfitCenter(page, '2DM');
// Disable Extract MIS
await setExtractMIS(page, false);
// Verify checkbox is disabled
expect(await isExtractMISEnabled(page)).toBe(false);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-090-P04: Historical Date Range with Multiple Profit Centers', async ({ page }) => {
await enterSearchName(page, 'Test 090-P04 Historical Multi PC');
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add multiple profit centers
await addProfitCenters(page, ['3TM', '4IM']);
// Enable Extract MIS
await setExtractMIS(page, true);
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(2);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-090-P05: All Available Profit Centers', async ({ page }) => {
await enterSearchName(page, 'Test 090-P05 All Profit Centers');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add all profit centers
await addProfitCenters(page, TestAutocompleteData.profitCenters);
// Enable Extract MIS
await setExtractMIS(page, true);
// Verify all 9 profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(9);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-090-P06: Same Day Date Range with Extract MIS', async ({ page }) => {
await enterSearchName(page, 'Test 090-P06 Same Day');
await setDateRange(page, TestDateRanges.SAME_DAY.min, TestDateRanges.SAME_DAY.max);
await addProfitCenter(page, '1CM');
await setExtractMIS(page, true);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
});
test.describe('Negative Test Cases', () => {
test('TC-090-N01: Missing Required Date Range', async ({ page }) => {
await enterSearchName(page, 'Test 090-N01 Missing Dates');
// Leave minimum date empty
// Leave maximum date empty
await addProfitCenter(page, '1PM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N02: Missing Profit Center', async ({ page }) => {
await enterSearchName(page, 'Test 090-N02 Missing Profit Center');
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Do NOT add any profit center
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N03: Invalid Date Range (End Before Start)', async ({ page }) => {
await enterSearchName(page, 'Test 090-N03 Invalid Date Range');
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
await addProfitCenter(page, '1PM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N04: Missing Minimum Date Only', async ({ page }) => {
await enterSearchName(page, 'Test 090-N04 Missing Min Date');
// Leave minimum date empty
await setMaxDate(page, '2020-09-01');
await addProfitCenter(page, '1AM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N05: Missing Maximum Date Only', async ({ page }) => {
await enterSearchName(page, 'Test 090-N05 Missing Max Date');
await setMinDate(page, '2020-01-01');
// Leave maximum date empty
await addProfitCenter(page, '1BM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N06: Missing Search Name', async ({ page }) => {
// Leave search name empty
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
await addProfitCenter(page, '1PM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N07: Whitespace-Only Search Name', async ({ page }) => {
await enterSearchName(page, ' ');
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
await addProfitCenter(page, '1PM');
await setExtractMIS(page, true);
await submitAndExpectError(page);
});
test('TC-090-N08: Missing All Required Filters', async ({ page }) => {
await enterSearchName(page, 'Test 090-N08 No Filters');
// Leave minimum date empty
// Leave maximum date empty
// Do not add any profit centers
// Extract MIS is just a flag, doesn't cause validation error by itself
await clickSubmitSearch(page);
await page.waitForTimeout(1000);
// Should have validation errors
expect(await hasValidationErrors(page)).toBe(true);
});
});
});
@@ -0,0 +1,262 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearMinDate, clearMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addProfitCenter, addProfitCenters, profitCenterConfig, TestAutocompleteData, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { uploadFile, itemNumberConfig, getTestFile, TestFiles, getUploadedItemCount } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
/**
* Test suite for Search Type 60: Time Span + Profit Center + Item Number
*
* This search type allows users to find work orders within a specified date range,
* filtered by profit center (branch code) and item number.
*
* Filters Enabled:
* - Timespan (Min Date, Max Date)
* - Profit Center
* - Item Number (via file upload)
*/
test.describe('Search Type 60: Time Span + Profit Center + Item Number', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_PC_ITEM);
});
test.describe('Positive Test Cases', () => {
test('TC-060-P01: Single Profit Center and Single Item Number', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Type60 Single PC and Item Test');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add profit center
await addProfitCenter(page, '1PM');
// Upload item numbers file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify no error notification
await assertNoErrorNotification(page);
// Verify item was uploaded
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThan(0);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify success notification
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P02: Multiple Profit Centers with Single Item Number', async ({ page }) => {
await enterSearchName(page, 'Type60 Multiple PC Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1BM', '1CM']);
// Upload single item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify all profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P03: Single Profit Center with Multiple Item Numbers', async ({ page }) => {
await enterSearchName(page, 'Type60 Multiple Items Test');
await setDateRange(page, '2019-01-01', '2019-12-31');
await addProfitCenter(page, '2DM');
// Upload multiple items file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.MULTIPLE_ITEMS));
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThan(1);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P04: Multiple Profit Centers with Multiple Item Numbers', async ({ page }) => {
await enterSearchName(page, 'Type60 Multiple PC and Items Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple profit centers
await addProfitCenters(page, ['1PM', '2SM', '3TM']);
// Upload multiple items file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.MULTIPLE_ITEMS));
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThan(1);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P05: Recent Date Range', async ({ page }) => {
await enterSearchName(page, 'Type60 Recent Range Test');
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
await addProfitCenter(page, '4IM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P06: Historical Date Range', async ({ page }) => {
await enterSearchName(page, 'Type60 Historical Range Test');
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
await addProfitCenter(page, '5SM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P07: Same Day Date Range', async ({ page }) => {
await enterSearchName(page, 'Type60 Same Day Test');
await setDateRange(page, '2019-06-15', '2019-06-15');
await addProfitCenter(page, '1AM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-060-P08: All Profit Centers', async ({ page }) => {
await enterSearchName(page, 'Type60 All Profit Centers Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add all profit centers
await addProfitCenters(page, TestAutocompleteData.profitCenters);
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify all 9 profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(9);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
});
test.describe('Negative Test Cases', () => {
test('TC-060-N01: Missing Search Name', async ({ page }) => {
// Leave search name empty
await setDateRange(page, '2018-01-01', '2019-12-31');
await addProfitCenter(page, '1PM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N02: Missing Profit Center', async ({ page }) => {
await enterSearchName(page, 'Type60 Missing PC Test');
await setDateRange(page, '2018-01-01', '2019-12-31');
// Do not add any profit centers
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N03: Missing Item Number', async ({ page }) => {
await enterSearchName(page, 'Type60 Missing Item Test');
await setDateRange(page, '2018-01-01', '2019-12-31');
await addProfitCenter(page, '1PM');
// Do not upload any item numbers
await submitAndExpectError(page);
});
test('TC-060-N04: Missing Minimum Date', async ({ page }) => {
await enterSearchName(page, 'Type60 Missing Min Date Test');
// Only set max date
await setMaxDate(page, '2019-12-31');
await addProfitCenter(page, '1PM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N05: Missing Maximum Date', async ({ page }) => {
await enterSearchName(page, 'Type60 Missing Max Date Test');
// Only set min date
await setMinDate(page, '2018-01-01');
await addProfitCenter(page, '1PM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N06: Invalid Date Range (Min > Max)', async ({ page }) => {
await enterSearchName(page, 'Type60 Invalid Date Range Test');
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
await addProfitCenter(page, '1PM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N07: Whitespace-Only Search Name', async ({ page }) => {
await enterSearchName(page, ' ');
await setDateRange(page, '2018-01-01', '2019-12-31');
await addProfitCenter(page, '1PM');
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
await submitAndExpectError(page);
});
test('TC-060-N08: Missing All Required Filters', async ({ page }) => {
await enterSearchName(page, 'Type60 No Filters Test');
// Leave minimum date empty
// Leave maximum date empty
// Do not add any profit centers
// Do not add any item numbers
await clickSubmitSearch(page);
await page.waitForTimeout(1000);
// Should have validation errors
expect(await hasValidationErrors(page)).toBe(true);
});
});
});
@@ -0,0 +1,332 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import {
addProfitCenter,
addProfitCenters,
addOperator,
addOperators,
profitCenterConfig,
operatorConfig,
TestAutocompleteData,
getAutocompleteItemCount
} from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError } from '../helpers/validation.helper';
/**
* Test suite for Search Type 160: Time Span + Profit Center + Operator
*
* This search type combines Time Span, Profit Center, and Operator filters.
* It allows users to search for work order data within a specific date range,
* filtered by profit center (branch code) and operator (user ID).
*
* Filters Enabled:
* - Timespan (Start Date, End Date)
* - Profit Center (via autocomplete)
* - Operator (via autocomplete)
*/
test.describe('Search Type 160: Time Span + Profit Center + Operator', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_PC_OPERATOR);
});
test.describe('Positive Test Cases', () => {
test('TC-160-P01: Single Profit Center and Single Operator', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-160-P01 Single Values');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add profit center
await addProfitCenter(page, '1CM');
// Add operator
await addOperator(page, 'ALEXIUCG');
// Verify no error notification
await assertNoErrorNotification(page);
// Verify items were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(1);
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify success notification
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P02: Multiple Profit Centers with Single Operator', async ({ page }) => {
await enterSearchName(page, 'TC-160-P02 Multiple Profit Centers');
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1BM', '1CM']);
// Add single operator
await addOperator(page, 'ADAMSSN');
// Verify all profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P03: Single Profit Center with Multiple Operators', async ({ page }) => {
await enterSearchName(page, 'TC-160-P03 Multiple Operators');
await setDateRange(page, '2019-01-01', '2020-09-01');
await addProfitCenter(page, '1PM');
// Add multiple operators
await addOperators(page, ['AGNEWA', 'AGNEWL', 'ALASMARB']);
// Verify all operators were added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(3);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P04: Multiple Profit Centers and Multiple Operators', async ({ page }) => {
await enterSearchName(page, 'TC-160-P04 Multiple All');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple profit centers
await addProfitCenters(page, ['2DM', '2SM']);
// Add multiple operators
await addOperators(page, ['ALLENHY', 'ALLENNI']);
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(2);
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(2);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P05: Recent Date Range', async ({ page }) => {
await enterSearchName(page, 'TC-160-P05 Recent Range');
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
await addProfitCenter(page, '3TM');
await addOperator(page, 'ALURUM');
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P06: Historical Date Range', async ({ page }) => {
await enterSearchName(page, 'TC-160-P06 Historical Range');
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
await addProfitCenter(page, '4IM');
await addOperator(page, 'ALVESM1');
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P07: Narrow Date Range (Single Month)', async ({ page }) => {
await enterSearchName(page, 'TC-160-P07 Single Month');
await setDateRange(page, '2019-06-01', '2019-06-30');
await addProfitCenter(page, '5SM');
await addOperator(page, 'APONTEVE');
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P08: All Profit Centers', async ({ page }) => {
await enterSearchName(page, 'TC-160-P08 All Profit Centers');
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add all profit centers
await addProfitCenters(page, TestAutocompleteData.profitCenters);
await addOperator(page, 'ADAMSSN');
// Verify all 9 profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(9);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P09: Many Operators', async ({ page }) => {
await enterSearchName(page, 'TC-160-P09 Many Operators');
await setDateRange(page, '2018-01-01', '2020-09-01');
await addProfitCenter(page, '1CM');
// Add multiple operators
await addOperators(page, ['ADAMSSN', 'AGNEWA', 'AGNEWL', 'ALASMARB', 'ALEXIUCG']);
// Verify all 5 operators were added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(5);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-160-P10: Same Start and End Date', async ({ page }) => {
await enterSearchName(page, 'TC-160-P10 Same Day');
await setDateRange(page, '2019-07-15', '2019-07-15');
await addProfitCenter(page, '1PM');
await addOperator(page, 'ADAMSSN');
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
});
test.describe('Negative Test Cases', () => {
test('TC-160-N01: Missing Date Range', async ({ page }) => {
await enterSearchName(page, 'TC-160-N01 No Dates');
// Leave minimum date empty
// Leave maximum date empty
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N02: Missing Profit Center', async ({ page }) => {
await enterSearchName(page, 'TC-160-N02 No Profit Center');
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do not add any profit center
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N03: Missing Operator', async ({ page }) => {
await enterSearchName(page, 'TC-160-N03 No Operator');
await setDateRange(page, '2019-01-01', '2019-12-31');
await addProfitCenter(page, '1CM');
// Do not add any operator
await submitAndExpectError(page);
});
test('TC-160-N04: Start Date After End Date', async ({ page }) => {
await enterSearchName(page, 'TC-160-N04 Invalid Date Range');
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N05: Missing Search Name', async ({ page }) => {
// Leave search name empty
await setDateRange(page, '2019-01-01', '2019-12-31');
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N06: Missing Start Date Only', async ({ page }) => {
await enterSearchName(page, 'TC-160-N06 No Start Date');
// Leave minimum date empty
await setMaxDate(page, '2019-12-31');
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N07: Missing End Date Only', async ({ page }) => {
await enterSearchName(page, 'TC-160-N07 No End Date');
await setMinDate(page, '2019-01-01');
// Leave maximum date empty
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N08: Whitespace-Only Search Name', async ({ page }) => {
await enterSearchName(page, ' ');
await setDateRange(page, '2019-01-01', '2019-12-31');
await addProfitCenter(page, '1CM');
await addOperator(page, 'ALEXIUCG');
await submitAndExpectError(page);
});
test('TC-160-N09: Missing Profit Center and Operator', async ({ page }) => {
await enterSearchName(page, 'TC-160-N09 No PC or Operator');
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do not add any profit center
// Do not add any operator
await clickSubmitSearch(page);
await page.waitForTimeout(1000);
// Should have validation errors
expect(await hasValidationErrors(page)).toBe(true);
});
test('TC-160-N10: Missing All Required Filters', async ({ page }) => {
await enterSearchName(page, 'TC-160-N10 No Filters');
// Leave minimum date empty
// Leave maximum date empty
// Do not add any profit centers
// Do not add any operators
await clickSubmitSearch(page);
await page.waitForTimeout(1000);
// Should have validation errors
expect(await hasValidationErrors(page)).toBe(true);
});
});
});
@@ -0,0 +1,263 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addProfitCenter, addProfitCenters, profitCenterConfig, TestAutocompleteData, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { uploadFile, partOperationConfig, getTestFile, TestFiles, getUploadedItemCount } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError } from '../helpers/validation.helper';
/**
* Test suite for Search Type 70: Time Span + Profit Center + Item/Operation/MIS
*
* This search type allows users to find work orders within a specified date range,
* filtered by profit center and part operations (Item Number, Operation Number,
* MIS Number, and MIS Revision).
*
* Filters Enabled:
* - Timespan (Min Date, Max Date)
* - Profit Center
* - Part Operations (Item Number, Operation Number, MIS Number, MIS Revision) via file upload
*/
test.describe('Search Type 70: Time Span + Profit Center + Item/Operation/MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_PC_PARTOP);
});
test.describe('Positive Test Cases', () => {
test('TC-070-P01: Single Profit Center with Single Part Operation', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Type70 Single PC and Part Op Test');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add profit center
await addProfitCenter(page, '1PM');
// Upload part operations file (contains Item, Operation, MIS, Revision)
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify no error notification
await assertNoErrorNotification(page);
// Verify part operation was uploaded
const opCount = await getUploadedItemCount(page, partOperationConfig);
expect(opCount).toBeGreaterThan(0);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify success notification
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P02: Multiple Profit Centers with Single Part Operation', async ({ page }) => {
await enterSearchName(page, 'Type70 Multiple PC Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1BM', '1CM']);
// Upload single part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify all profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P03: Single Profit Center with Multiple Part Operations', async ({ page }) => {
await enterSearchName(page, 'Type70 Multiple Part Ops Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
await addProfitCenter(page, '2DM');
// Upload multiple part operations file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
const opCount = await getUploadedItemCount(page, partOperationConfig);
expect(opCount).toBeGreaterThan(1);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P04: Multiple Profit Centers with Multiple Part Operations', async ({ page }) => {
await enterSearchName(page, 'Type70 Multiple PC and Part Ops Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple profit centers
await addProfitCenters(page, ['1PM', '2SM', '3TM']);
// Upload multiple part operations file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(3);
const opCount = await getUploadedItemCount(page, partOperationConfig);
expect(opCount).toBeGreaterThan(1);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P05: Recent Date Range', async ({ page }) => {
await enterSearchName(page, 'Type70 Recent Range Test');
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
await addProfitCenter(page, '4IM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P06: Historical Date Range', async ({ page }) => {
await enterSearchName(page, 'Type70 Historical Range Test');
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
await addProfitCenter(page, '5SM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P07: Same Day Date Range', async ({ page }) => {
await enterSearchName(page, 'Type70 Same Day Test');
await setDateRange(page, '2019-06-15', '2019-06-15');
await addProfitCenter(page, '1AM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
test('TC-070-P08: All Profit Centers with Single Part Operation', async ({ page }) => {
await enterSearchName(page, 'Type70 All Profit Centers Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add all profit centers
await addProfitCenters(page, TestAutocompleteData.profitCenters);
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify all 9 profit centers were added
const pcCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(pcCount).toBe(9);
await assertNoErrorNotification(page);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await waitForNotification(page, 'success', 10000);
});
});
test.describe('Negative Test Cases', () => {
test('TC-070-N01: Missing Search Name', async ({ page }) => {
// Leave search name empty
await setDateRange(page, '2018-01-01', '2020-09-01');
await addProfitCenter(page, '1PM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N02: Missing Profit Center', async ({ page }) => {
await enterSearchName(page, 'Type70 Missing PC Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
// Do not add any profit centers
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N03: Missing Part Operation', async ({ page }) => {
await enterSearchName(page, 'Type70 Missing Part Op Test');
await setDateRange(page, '2018-01-01', '2020-09-01');
await addProfitCenter(page, '1PM');
// Do not upload any part operations
await submitAndExpectError(page);
});
test('TC-070-N04: Missing Minimum Date', async ({ page }) => {
await enterSearchName(page, 'Type70 Missing Min Date Test');
// Only set max date
await setMaxDate(page, '2020-09-01');
await addProfitCenter(page, '1PM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N05: Missing Maximum Date', async ({ page }) => {
await enterSearchName(page, 'Type70 Missing Max Date Test');
// Only set min date
await setMinDate(page, '2018-01-01');
await addProfitCenter(page, '1PM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N06: Invalid Date Range (Min > Max)', async ({ page }) => {
await enterSearchName(page, 'Type70 Invalid Date Range Test');
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
await addProfitCenter(page, '1PM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N07: Whitespace-Only Search Name', async ({ page }) => {
await enterSearchName(page, ' ');
await setDateRange(page, '2018-01-01', '2020-09-01');
await addProfitCenter(page, '1PM');
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-070-N08: Missing All Required Filters', async ({ page }) => {
await enterSearchName(page, 'Type70 No Filters Test');
// Leave minimum date empty
// Leave maximum date empty
// Do not add any profit centers
// Do not add any part operations
await clickSubmitSearch(page);
await page.waitForTimeout(1000);
// Should have validation errors
expect(await hasValidationErrors(page)).toBe(true);
});
});
});
@@ -0,0 +1,525 @@
/**
* Playwright E2E tests for Search Type 80:
* Time Span + Profit Center + Work Order + Item/Operation/MIS
*
* This is the most comprehensive search type, allowing users to find work orders
* within a specified date range, filtered by profit center, specific work order
* numbers, and part operations (Item Number, Operation Number, MIS Number, MIS Revision).
*
* Filters Enabled:
* - Timespan (Min Date, Max Date)
* - Profit Center (autocomplete)
* - Work Order (file upload)
* - Part Operations (file upload)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearMinDate, clearMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import {
addProfitCenter,
addProfitCenters,
profitCenterConfig,
getAutocompleteItemCount,
isAutocompletePanelVisible,
TestAutocompleteData
} from '../helpers/autocomplete.helper';
import {
uploadFile,
workOrderConfig,
partOperationConfig,
getTestFile,
TestFiles,
getUploadedItemCount,
isFileUploadPanelVisible
} from '../helpers/file-upload.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
// Test data constants for Type 80
const TYPE_80_NAME = SearchTypes.TIMESPAN_PC_WO_PARTOP;
// Valid profit centers from test documentation
const VALID_PROFIT_CENTERS = ['1AM', '1BM', '1CM', '1PM', '2DM', '2SM', '3TM', '4IM', '5SM'];
// Standard date ranges for testing
const STANDARD_DATE_RANGE = { min: '2018-01-01', max: '2020-09-01' };
const RECENT_DATE_RANGE = TestDateRanges.RECENT;
const HISTORICAL_DATE_RANGE = TestDateRanges.HISTORICAL;
const SAME_DAY_DATE_RANGE = { min: '2019-06-15', max: '2019-06-15' };
const INVALID_DATE_RANGE = TestDateRanges.INVALID_REVERSED;
test.describe('Search Type 80: Time Span + Profit Center + Work Order + Item/Operation/MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test.describe('Positive Test Cases', () => {
test('TC-080-P01: Single value for all filters', async ({ page }) => {
// Step 1: Enter search name
await enterSearchName(page, 'TC-080-P01 Single Values Test');
// Step 2: Select search type
await selectSearchType(page, TYPE_80_NAME);
// Verify all filter panels are visible
expect(await isAutocompletePanelVisible(page, profitCenterConfig)).toBe(true);
expect(await isFileUploadPanelVisible(page, workOrderConfig)).toBe(true);
expect(await isFileUploadPanelVisible(page, partOperationConfig)).toBe(true);
// Step 3: Set date range
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Step 4: Add single profit center
await addProfitCenter(page, '1AM');
expect(await getAutocompleteItemCount(page, profitCenterConfig)).toBe(1);
// Step 5: Upload single work order file
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Step 6: Upload single part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Step 7: Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-080-P02: Multiple profit centers with single work order and part operation', async ({ page }) => {
await enterSearchName(page, 'TC-080-P02 Multiple PC Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1BM', '1CM']);
expect(await getAutocompleteItemCount(page, profitCenterConfig)).toBe(3);
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P03: Single profit center with multiple work orders', async ({ page }) => {
await enterSearchName(page, 'TC-080-P03 Multiple WO Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1PM');
// Upload multiple work orders
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
const woCount = await getUploadedItemCount(page, workOrderConfig);
expect(woCount).toBeGreaterThan(1);
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P04: Single profit center and work order with multiple part operations', async ({ page }) => {
await enterSearchName(page, 'TC-080-P04 Multiple Part Ops Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '2DM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Upload multiple part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
const opCount = await getUploadedItemCount(page, partOperationConfig);
expect(opCount).toBeGreaterThan(1);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P05: Multiple values for all filter types', async ({ page }) => {
await enterSearchName(page, 'TC-080-P05 All Multiple Values Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Multiple profit centers
await addProfitCenters(page, ['1PM', '2SM', '3TM']);
expect(await getAutocompleteItemCount(page, profitCenterConfig)).toBe(3);
// Multiple work orders
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
// Multiple part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P06: Recent date range', async ({ page }) => {
await enterSearchName(page, 'TC-080-P06 Recent Range Test');
await selectSearchType(page, TYPE_80_NAME);
// Use recent date range
await setDateRange(page, RECENT_DATE_RANGE.min, RECENT_DATE_RANGE.max);
await addProfitCenter(page, '4IM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P07: Historical date range', async ({ page }) => {
await enterSearchName(page, 'TC-080-P07 Historical Range Test');
await selectSearchType(page, TYPE_80_NAME);
// Use historical date range
await setDateRange(page, HISTORICAL_DATE_RANGE.min, HISTORICAL_DATE_RANGE.max);
await addProfitCenter(page, '5SM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P08: Same day date range', async ({ page }) => {
await enterSearchName(page, 'TC-080-P08 Same Day Test');
await selectSearchType(page, TYPE_80_NAME);
// Use same day for min and max
await setDateRange(page, SAME_DAY_DATE_RANGE.min, SAME_DAY_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P09: All profit centers', async ({ page }) => {
await enterSearchName(page, 'TC-080-P09 All Profit Centers Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Add all valid profit centers
await addProfitCenters(page, VALID_PROFIT_CENTERS);
expect(await getAutocompleteItemCount(page, profitCenterConfig)).toBe(VALID_PROFIT_CENTERS.length);
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-080-P10: Maximum work orders', async ({ page }) => {
await enterSearchName(page, 'TC-080-P10 Many Work Orders Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
// Upload maximum work orders file
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
});
test.describe('Negative Test Cases', () => {
test('TC-080-N01: Missing search name', async ({ page }) => {
// Do NOT enter search name
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N02: Missing profit center', async ({ page }) => {
await enterSearchName(page, 'TC-080-N02 Missing PC Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Do NOT add profit center
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N03: Missing work order', async ({ page }) => {
await enterSearchName(page, 'TC-080-N03 Missing WO Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
// Do NOT upload work order
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N04: Missing part operation', async ({ page }) => {
await enterSearchName(page, 'TC-080-N04 Missing Part Op Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Do NOT upload part operation
await submitAndExpectError(page);
});
test('TC-080-N05: Missing minimum date', async ({ page }) => {
await enterSearchName(page, 'TC-080-N05 Missing Min Date Test');
await selectSearchType(page, TYPE_80_NAME);
// Only set max date, leave min date empty
await setMaxDate(page, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N06: Missing maximum date', async ({ page }) => {
await enterSearchName(page, 'TC-080-N06 Missing Max Date Test');
await selectSearchType(page, TYPE_80_NAME);
// Only set min date, leave max date empty
await setMinDate(page, STANDARD_DATE_RANGE.min);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N07: Invalid date range (min > max)', async ({ page }) => {
await enterSearchName(page, 'TC-080-N07 Invalid Date Range Test');
await selectSearchType(page, TYPE_80_NAME);
// Set min date after max date
await setDateRange(page, INVALID_DATE_RANGE.min, INVALID_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N08: Missing both dates', async ({ page }) => {
await enterSearchName(page, 'TC-080-N08 Missing Both Dates Test');
await selectSearchType(page, TYPE_80_NAME);
// Do not set any dates
await clearMinDate(page);
await clearMaxDate(page);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N09: Whitespace-only search name', async ({ page }) => {
// Enter whitespace-only search name
await enterSearchName(page, ' ');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-080-N10: Missing all required filters', async ({ page }) => {
await enterSearchName(page, 'TC-080-N10 No Filters Test');
await selectSearchType(page, TYPE_80_NAME);
// Do not set any filters - leave dates, profit center, work order, and part operation empty
await clearMinDate(page);
await clearMaxDate(page);
await submitAndExpectError(page);
});
test('TC-080-N11: Empty work order file', async ({ page }) => {
await enterSearchName(page, 'TC-080-N11 Empty WO File Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
// Upload empty file
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.EMPTY_FILE));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Should either show error notification or validation error
const hasError = await hasErrorNotification(page) || await hasValidationErrors(page);
// If upload completes without immediate error, submit should fail
if (!hasError) {
await submitAndExpectError(page);
}
});
test('TC-080-N12: Empty part operation file', async ({ page }) => {
await enterSearchName(page, 'TC-080-N12 Empty Part Op File Test');
await selectSearchType(page, TYPE_80_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addProfitCenter(page, '1AM');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Upload empty file for part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.EMPTY_FILE));
// Should either show error notification or validation error
const hasError = await hasErrorNotification(page) || await hasValidationErrors(page);
// If upload completes without immediate error, submit should fail
if (!hasError) {
await submitAndExpectError(page);
}
});
});
test.describe('Filter Panel Visibility', () => {
test('TC-080-V01: All filter panels visible when search type selected', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
// Verify all four filter panels are visible
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
await expect(page.locator('text=Filter by Profit Center')).toBeVisible();
await expect(page.locator('text=Filter by Work Order')).toBeVisible();
await expect(page.locator('text=Filter By Item/Operation/MIS')).toBeVisible();
});
test('TC-080-V02: Date inputs are available in time span panel', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
// Verify date input fields exist
const minDateInput = page.locator('input[name="MinimumDt"]');
const maxDateInput = page.locator('input[name="MaximumDt"]');
await expect(minDateInput).toBeVisible();
await expect(maxDateInput).toBeVisible();
});
test('TC-080-V03: Autocomplete available for profit center', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
// Verify autocomplete component exists in profit center panel
const panel = page.locator(`.rz-card:has-text("${profitCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete');
await expect(autocomplete).toBeVisible();
});
test('TC-080-V04: File upload available for work order', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
// Verify file input exists in work order panel
const panel = page.locator(`.rz-card:has-text("${workOrderConfig.panelHeader}")`);
const fileInput = panel.locator('input[type="file"]');
await expect(fileInput).toBeAttached();
});
test('TC-080-V05: File upload available for part operation', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
// Verify file input exists in part operation panel
const panel = page.locator(`.rz-card:has-text("${partOperationConfig.panelHeader}")`);
const fileInput = panel.locator('input[type="file"]');
await expect(fileInput).toBeAttached();
});
});
test.describe('Template Downloads', () => {
test('TC-080-T01: Download work order template', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
const panel = page.locator(`.rz-card:has-text("${workOrderConfig.panelHeader}")`);
const downloadPromise = page.waitForEvent('download');
await panel.locator('button:has-text("Download Template")').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
test('TC-080-T02: Download part operation template', async ({ page }) => {
await selectSearchType(page, TYPE_80_NAME);
const panel = page.locator(`.rz-card:has-text("${partOperationConfig.panelHeader}")`);
const downloadPromise = page.waitForEvent('download');
await panel.locator('button:has-text("Download Template")').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
});
});
@@ -0,0 +1,410 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearDateRange, TestDateRanges } from '../helpers/date-picker.helper';
import {
addProfitCenter,
addProfitCenters,
clearAutocompleteItems,
profitCenterConfig,
getAutocompleteItemCount,
removeAutocompleteItem,
isAutocompletePanelVisible,
} from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, hasSuccessNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
/**
* Test suite for Search Type 30: Time Span + Profit Center
*
* This search type allows users to search by a date range combined with
* one or more profit center (branch) codes.
*
* Filters Enabled: Timespan, Profit Center
*/
test.describe('Search Type 30: Time Span + Profit Center', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
// ============================================================================
// POSITIVE TEST CASES
// ============================================================================
test.describe('Positive Tests', () => {
test('TC-030-P01: Single profit center with standard date range', async ({ page }) => {
// Select search type
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
// Verify filter panel is visible
await expect(page.locator('text=Filter by Profit Center')).toBeVisible();
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
// Enter search name
await enterSearchName(page, 'TC-030-P01 Single Profit Center');
// Set date range
await setDateRange(page, '2020-01-01', '2020-09-01');
// Add profit center
await addProfitCenter(page, '1AM');
// Verify profit center appears in the list
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-030-P02: Multiple profit centers search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P02 Multiple Profit Centers');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add multiple profit centers
await addProfitCenters(page, ['1AM', '1PM', '2DM']);
// Verify all profit centers appear in the list
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(3);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P03: All profit centers search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P03 All Profit Centers');
// Set date range
await setDateRange(page, '2018-01-01', '2018-12-31');
// Add all profit centers
const allProfitCenters = ['1AM', '1BM', '1CM', '1PM', '2DM', '2SM', '3TM', '4IM', '5SM'];
await addProfitCenters(page, allProfitCenters);
// Verify all profit centers appear in the list
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(9);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P04: Minimum date range (same day)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P04 Same Day Range');
// Set same day date range
await setDateRange(page, TestDateRanges.SAME_DAY.min, TestDateRanges.SAME_DAY.max);
// Add profit center
await addProfitCenter(page, '1PM');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P05: Boundary date - start of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P05 Start Boundary');
// Set earliest date boundary (1905-01-20 to 1905-12-31)
await setDateRange(page, TestDateRanges.START_BOUNDARY.min, TestDateRanges.START_BOUNDARY.max);
// Add profit center
await addProfitCenter(page, '1AM');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P06: Boundary date - end of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P06 End Boundary');
// Set latest date boundary (2020-08-01 to 2020-09-01)
await setDateRange(page, TestDateRanges.END_BOUNDARY.min, TestDateRanges.END_BOUNDARY.max);
// Add profit center
await addProfitCenter(page, '1CM');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P07: Historical date range search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P07 Historical Search');
// Set historical date range (2016-01-01 to 2017-12-31)
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add profit center
await addProfitCenter(page, '2SM');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-030-P08: Profit center remove and re-add', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-P08 PC Remove Re-add');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add profit centers
await addProfitCenter(page, '1AM');
await addProfitCenter(page, '1BM');
// Verify both are added
let itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(2);
// Remove first profit center (1AM)
await removeAutocompleteItem(page, profitCenterConfig, 0);
// Verify only one remains
itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(1);
// Add another profit center
await addProfitCenter(page, '1CM');
// Verify two profit centers in list (1BM and 1CM)
itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(2);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
});
// ============================================================================
// NEGATIVE TEST CASES
// ============================================================================
test.describe('Negative Tests', () => {
test('TC-030-N01: Missing search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
// Do NOT enter search name
// Set valid date range
await setDateRange(page, '2020-01-01', '2020-09-01');
// Add profit center
await addProfitCenter(page, '1AM');
// Attempt to submit
await submitAndExpectError(page);
// Verify user remains on the page
await expect(page.locator('text=Filter by Profit Center')).toBeVisible();
});
test('TC-030-N02: No search type selected', async ({ page }) => {
// Enter search name without selecting search type
await enterSearchName(page, 'TC-030-N02 No Type');
// Verify filter panels are not visible (search type not selected)
const profitCenterPanelVisible = await isAutocompletePanelVisible(page, profitCenterConfig);
expect(profitCenterPanelVisible).toBe(false);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-030-N03: Missing minimum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N03 Missing Min Date');
// Only set maximum date
await setMaxDate(page, '2020-09-01');
// Add profit center
await addProfitCenter(page, '1AM');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-030-N04: Missing maximum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N04 Missing Max Date');
// Only set minimum date
await setMinDate(page, '2020-01-01');
// Add profit center
await addProfitCenter(page, '1AM');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-030-N05: Empty profit center list', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N05 Empty Profit Centers');
// Set valid date range
await setDateRange(page, '2020-01-01', '2020-09-01');
// Do NOT add any profit centers
// Verify profit center list is empty
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-030-N06: Invalid date range (min > max)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N06 Invalid Date Range');
// Set invalid date range (min date after max date)
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
// Add profit center
await addProfitCenter(page, '1AM');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-030-N07: Invalid profit center code', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N07 Invalid PC Code');
// Set valid date range
await setDateRange(page, '2020-01-01', '2020-09-01');
// Try to add invalid profit center
// The autocomplete should not find any matches for "INVALID"
const panel = page.locator(`.rz-card:has-text("${profitCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('INVALID');
// Wait for autocomplete to search
await page.waitForTimeout(500);
// Verify no autocomplete suggestions appear
const dropdown = page.locator('.rz-autocomplete-list');
const dropdownVisible = await dropdown.isVisible({ timeout: 2000 }).catch(() => false);
// If dropdown is not visible or empty, the invalid code is rejected
if (dropdownVisible) {
const items = dropdown.locator('.rz-autocomplete-list-item');
const count = await items.count();
expect(count).toBe(0);
}
// Verify profit center list is still empty
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(0);
});
test('TC-030-N08: Future date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N08 Future Dates');
// Set future date range
await setDateRange(page, TestDateRanges.FUTURE.min, TestDateRanges.FUTURE.max);
// Add profit center
await addProfitCenter(page, '1AM');
// Submit search - may be accepted but will return no results
// or may show validation warning
await clickSubmitSearch(page);
// Check if there's a validation error or if it proceeds
const hasErrors = await hasValidationErrors(page);
if (!hasErrors) {
// If accepted, confirm the submission
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
}
// If there are validation errors, that's also acceptable behavior
});
test('TC-030-N09: Invalid date format', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N09 Invalid Date Format');
// Try to enter invalid date format directly
// The Radzen date picker should prevent or reject invalid formats
const minDateInput = page.locator('input[name="MinimumDt"]');
await minDateInput.fill('31-12-2020'); // Invalid format
await setMaxDate(page, '2020-09-01');
// Add profit center
await addProfitCenter(page, '1AM');
// Attempt to submit - should fail validation
await submitAndExpectError(page);
});
test('TC-030-N10: Profit center with special characters', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_PROFIT_CENTER);
await enterSearchName(page, 'TC-030-N10 PC Special Chars');
// Set valid date range
await setDateRange(page, '2020-01-01', '2020-09-01');
// Try to add profit center with special characters
const panel = page.locator(`.rz-card:has-text("${profitCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('1AM!@#');
// Wait for autocomplete to search
await page.waitForTimeout(500);
// Verify no autocomplete suggestions appear for invalid input
const dropdown = page.locator('.rz-autocomplete-list');
const dropdownVisible = await dropdown.isVisible({ timeout: 2000 }).catch(() => false);
if (dropdownVisible) {
const items = dropdown.locator('.rz-autocomplete-list-item');
const count = await items.count();
expect(count).toBe(0);
}
// Verify profit center list is still empty
const itemCount = await getAutocompleteItemCount(page, profitCenterConfig);
expect(itemCount).toBe(0);
});
});
});
@@ -0,0 +1,331 @@
/**
* E2E Tests for Search Type 110: Time Span + Work Center + Extract MIS
*
* This search type allows users to search by a date range combined with work center(s)
* and the Extract MIS boolean flag. When this search type is selected, the ExtractMisData
* flag is automatically set to true - there is no interactive checkbox.
*
* Required filters:
* - Timespan (Min Date to Max Date)
* - Work Center (one or more)
* - Extract MIS (automatically enabled when this search type is selected)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addWorkCenter, addWorkCenters, workCenterConfig, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors } from '../helpers/validation.helper';
// Valid test data from manual test scripts
const TEST_WORK_CENTERS = {
SINGLE_CA: '11275CA',
SINGLE_AS: '0083AS',
MULTIPLE_CA: ['10595CA', '11275CA', '11350CA'],
MULTIPLE_AS: ['1010AS', '1011AS'],
MIXED_SUFFIXES: ['0696AS', '13316CA', '1700CB'],
MANY: ['0083AS', '0278AS', '0424AS', '0586AS', '0696AS', '1010AS'],
};
test.describe('Search Type 110: Time Span + Work Center + Extract MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_WC_EXTRACTMIS);
});
test.describe('Positive Test Cases', () => {
test('TC-110-P01: Single Work Center with Extract MIS Enabled', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P01 Single WC Extract MIS');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Note: Extract MIS is automatically enabled when this search type is selected
// Verify the Extract MIS checkbox is visible and checked
const extractMisCheckbox = page.locator('text=Extract MIS data');
await expect(extractMisCheckbox).toBeVisible();
// Verify work center was added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBeGreaterThanOrEqual(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P02: Multiple Work Centers with Extract MIS Enabled', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P02 Multi WC Extract MIS');
// Set date range (recent)
await setDateRange(page, '2019-01-01', '2020-09-01');
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_CA);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P03: Single Work Center (AS suffix) with Extract MIS', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P03 Single WC AS Suffix');
// Set date range (mid-range)
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add single work center with AS suffix
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P04: Historical Date Range with Multiple Work Centers', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P04 Historical Multi WC');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_AS);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(2);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P05: Work Center Code Variants with Extract MIS', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P05 WC Code Variants');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work centers with different suffixes (AS, CA, CB)
await addWorkCenters(page, TEST_WORK_CENTERS.MIXED_SUFFIXES);
// Verify all work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P06: Recent Date Range with Single Work Center', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P06 Recent Range');
// Set recent date range
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Add work center
await addWorkCenter(page, '14305CA');
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-110-P07: Large Work Center Selection', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-P07 Many Work Centers');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add many work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MANY);
// Verify all work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(6);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
});
test.describe('Negative Test Cases', () => {
test('TC-110-N01: Missing Required Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N01 Missing Dates');
// Do NOT set date range (leave empty)
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Extract MIS is automatically enabled
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N02: Missing Work Center', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N02 Missing Work Center');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Do NOT add any work center
// Extract MIS is automatically enabled
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N03: Invalid Date Range (End Before Start)', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N03 Invalid Date Range');
// Set invalid date range (max before min)
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N04: Missing Minimum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N04 Missing Min Date');
// Set only max date
await setMaxDate(page, '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N05: Missing Maximum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N05 Missing Max Date');
// Set only min date
await setMinDate(page, '2018-01-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N06: Missing Search Name', async ({ page }) => {
// Do NOT enter search name
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-110-N07: All Required Filters Missing', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 110-N07 All Filters Missing');
// Do NOT set date range
// Do NOT add work center
// (Extract MIS is automatically enabled but requires other filters)
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
});
});
@@ -0,0 +1,358 @@
/**
* E2E Tests for Search Type 100: Time Span + Work Center + Item Number
*
* This search type allows users to search by a date range combined with work center(s)
* and item number(s). It finds work orders processed through specific work centers
* for specific items within the given time range.
*
* Required filters:
* - Timespan (Min Date to Max Date)
* - Work Center (one or more)
* - Item Number (one or more via file upload)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addWorkCenter, addWorkCenters, workCenterConfig, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { uploadFile, itemNumberConfig, getTestFile, TestFiles, getUploadedItemCount } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
// Valid test data from manual test scripts
const TEST_WORK_CENTERS = {
SINGLE: '10595CA',
AS_SUFFIX: ['0083AS', '0278AS', '0424AS'],
CA_SUFFIX: ['11275CA', '11350CA', '11355CA'],
CB_SUFFIX: '1700CB',
MIXED: ['1010AS', '14305CA'],
};
test.describe('Search Type 100: Time Span + Work Center + Item Number', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_WC_ITEM);
});
test.describe('Positive Test Cases', () => {
test('TC-100-P01: Single Work Center with Single Item Number', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P01 Single WC Single Item');
// Set date range (mid-range)
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Upload single item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify work center was added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBeGreaterThanOrEqual(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page or show queued status
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-100-P02: Single Work Center with Multiple Item Numbers', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P02 Single WC Multi Items');
// Set date range (recent)
await setDateRange(page, '2019-01-01', '2020-09-01');
// Add single work center
await addWorkCenter(page, '11275CA');
// Upload multiple items file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.MULTIPLE_ITEMS));
// Verify items were uploaded
const itemCount = await getUploadedItemCount(page, itemNumberConfig);
expect(itemCount).toBeGreaterThan(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-100-P03: Multiple Work Centers with Single Item Number', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P03 Multi WC Single Item');
// Set date range (mid-range)
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add multiple work centers (AS suffix)
await addWorkCenters(page, TEST_WORK_CENTERS.AS_SUFFIX);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Upload single item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-100-P04: Multiple Work Centers with Multiple Item Numbers', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P04 Multi WC Multi Items');
// Set date range (wide)
await setDateRange(page, '2017-01-01', '2020-09-01');
// Add multiple work centers
await addWorkCenter(page, '10595CA');
await addWorkCenter(page, '11350CA');
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(2);
// Upload multiple items file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.MULTIPLE_ITEMS));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-100-P05: Historical Date Range with Work Center and Items', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P05 Historical Search');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.CB_SUFFIX);
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-100-P06: Work Center Code Variants (AS vs CA suffix)', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-P06 WC Code Variants');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work centers with different suffixes
await addWorkCenter(page, '1010AS'); // AS suffix
await addWorkCenter(page, '14305CA'); // CA suffix
// Verify both were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(2);
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
});
test.describe('Negative Test Cases', () => {
test('TC-100-N01: Missing Required Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N01 Missing Dates');
// Do NOT set date range (leave empty)
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N02: Missing Work Center', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N02 Missing Work Center');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Do NOT add any work center
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N03: Missing Item Number', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N03 Missing Item Number');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Do NOT upload any item file
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N04: Invalid Date Range (End Before Start)', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N04 Invalid Date Range');
// Set invalid date range (max before min)
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N05: Missing Minimum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N05 Missing Min Date');
// Set only max date
await setMaxDate(page, '2019-12-31');
// Add work center
await addWorkCenter(page, '11275CA');
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N06: Missing Maximum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N06 Missing Max Date');
// Set only min date
await setMinDate(page, '2018-01-01');
// Add work center
await addWorkCenter(page, '0083AS');
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N07: Missing Search Name', async ({ page }) => {
// Do NOT enter search name
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Upload item file
await uploadFile(page, itemNumberConfig, getTestFile(TestFiles.SINGLE_ITEM));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-100-N08: All Required Filters Missing', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 100-N08 All Filters Missing');
// Do NOT set date range
// Do NOT add work center
// Do NOT upload item file
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
});
});
@@ -0,0 +1,492 @@
/**
* E2E Tests for Search Type 150: Time Span + Work Center + Operator
*
* This search type allows users to search by a date range combined with work center(s)
* and operator(s). It finds work order data within a specific date range, filtered by
* work center and operator (user ID).
*
* Required filters:
* - Timespan (Min Date to Max Date)
* - Work Center (one or more)
* - Operator (one or more)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import {
addWorkCenter,
addWorkCenters,
addOperator,
addOperators,
workCenterConfig,
operatorConfig,
getAutocompleteItemCount
} from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors } from '../helpers/validation.helper';
// Valid test data from manual test scripts
const TEST_WORK_CENTERS = {
SINGLE: '14305CA',
MULTIPLE_AS: ['0083AS', '0278AS', '0424AS'],
SINGLE_CA: '10595CA',
MULTIPLE_CA: ['11275CA', '11350CA'],
MIXED: ['0083AS', '10595CA', '1700CB'], // AS, CA, CB formats
MANY: ['0586AS', '0696AS', '1010AS', '1011AS'],
HISTORICAL: '13316CA',
NARROW: '15660CA',
RECENT: '11355CA',
};
const TEST_OPERATORS = {
SINGLE: 'AGNEWA',
MULTIPLE: ['AGNEWA', 'AGNEWL', 'ALASMARB'],
PAIR: ['ALEXIUCG', 'ALLENHY'],
MANY: ['APONTEVE', 'ARCHILAHI', 'ARGUELLC', 'ASHARK'],
HISTORICAL: 'ALURUM',
NARROW: 'ALVESM1',
RECENT: 'ALLENNI',
MIXED: 'ASLANESA',
FIRST: 'ADAMSSN',
};
test.describe('Search Type 150: Time Span + Work Center + Operator', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_WC_OPERATOR);
});
test.describe('Positive Test Cases', () => {
test('TC-150-P01: Single Work Center and Single Operator', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P01 Single Values');
// Set date range (mid-range)
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add single operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Verify work center was added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBeGreaterThanOrEqual(1);
// Verify operator was added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBeGreaterThanOrEqual(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P02: Multiple Work Centers with Single Operator', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P02 Multiple Work Centers');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_AS);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Add single operator
await addOperator(page, TEST_OPERATORS.FIRST);
// Verify operator was added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P03: Single Work Center with Multiple Operators', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P03 Multiple Operators');
// Set date range (recent)
await setDateRange(page, '2019-01-01', '2020-09-01');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Add multiple operators
await addOperators(page, TEST_OPERATORS.MULTIPLE);
// Verify work center was added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(1);
// Verify operators were added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(3);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P04: Multiple Work Centers and Multiple Operators', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P04 Multiple All');
// Set date range (wide)
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_CA);
// Add multiple operators
await addOperators(page, TEST_OPERATORS.PAIR);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(2);
// Verify operators were added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(2);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P05: Recent Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P05 Recent Range');
// Set recent date range
await setDateRange(page, TestDateRanges.RECENT.min, TestDateRanges.RECENT.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.RECENT);
// Add operator
await addOperator(page, TEST_OPERATORS.RECENT);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P06: Historical Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P06 Historical Range');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.HISTORICAL);
// Add operator
await addOperator(page, TEST_OPERATORS.HISTORICAL);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P07: Narrow Date Range (Single Month)', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P07 Single Month');
// Set narrow date range (single month)
await setDateRange(page, '2019-06-01', '2019-06-30');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.NARROW);
// Add operator
await addOperator(page, TEST_OPERATORS.NARROW);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P08: Many Work Centers and Many Operators', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P08 Many Values');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add many work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MANY);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(4);
// Add many operators
await addOperators(page, TEST_OPERATORS.MANY);
// Verify operators were added
const opCount = await getAutocompleteItemCount(page, operatorConfig);
expect(opCount).toBe(4);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-150-P09: Mixed Work Center Formats', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-P09 Mixed Formats');
// Set date range (mid-range)
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work centers with different formats (AS, CA, CB)
await addWorkCenters(page, TEST_WORK_CENTERS.MIXED);
// Verify all work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Add operator
await addOperator(page, TEST_OPERATORS.MIXED);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
});
test.describe('Negative Test Cases', () => {
test('TC-150-N01: Missing Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N01 No Dates');
// Do NOT set date range (leave empty)
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N02: Missing Work Center', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N02 No Work Center');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do NOT add any work center
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N03: Missing Operator', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N03 No Operator');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Do NOT add any operator
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N04: Start Date After End Date', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N04 Invalid Date Range');
// Set invalid date range (max before min)
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N05: Missing Search Name', async ({ page }) => {
// Do NOT enter search name
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N06: Missing Start Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N06 No Start Date');
// Set only max date
await setMaxDate(page, '2019-12-31');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N07: Missing End Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N07 No End Date');
// Set only min date
await setMinDate(page, '2019-01-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE);
// Add operator
await addOperator(page, TEST_OPERATORS.SINGLE);
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N08: All Required Filters Missing', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N08 All Missing');
// Do NOT set date range
// Do NOT add work center
// Do NOT add operator
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-150-N09: Missing Work Center and Operator', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'TC-150-N09 No WC No Op');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do NOT add work center
// Do NOT add operator
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
});
});
@@ -0,0 +1,411 @@
/**
* E2E Tests for Search Type 120: Time Span + Work Center + Item/Operation/MIS
*
* This search type allows users to search by a date range combined with work center(s)
* and part operation(s). Part operations are defined by a combination of Item Number,
* Operation Number, MIS Number, and MIS Revision.
*
* Required filters:
* - Timespan (Min Date to Max Date)
* - Work Center (one or more)
* - Item/Operation/MIS (one or more part operations via file upload)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import { addWorkCenter, addWorkCenters, workCenterConfig, getAutocompleteItemCount } from '../helpers/autocomplete.helper';
import { uploadFile, partOperationConfig, getTestFile, TestFiles, getUploadedItemCount } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors } from '../helpers/validation.helper';
// Valid test data from manual test scripts
const TEST_WORK_CENTERS = {
SINGLE_AS: '0083AS',
SINGLE_CA: '10595CA',
MULTIPLE_CA: ['11275CA', '11350CA', '11355CA'],
MULTIPLE_AS: ['0083AS', '0278AS'],
MIXED_SUFFIXES: ['0424AS', '14305CA', '1700CB'],
HISTORICAL: '1010AS',
WITH_SAME_MIS: '13316CA',
};
// Note: Part operations are uploaded via file containing columns:
// Item Number, Operation Number, MIS Number, MIS Revision
test.describe('Search Type 120: Time Span + Work Center + Item/Operation/MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
await selectSearchType(page, SearchTypes.TIMESPAN_WC_PARTOP);
});
test.describe('Positive Test Cases', () => {
test('TC-120-P01: Single Work Center with Single Part Operation', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P01 Single WC Single PartOp');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Upload single part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify work center was added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBeGreaterThanOrEqual(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P02: Single Work Center with Multiple Part Operations', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P02 Single WC Multi PartOps');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add single work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Upload multiple part operations file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
// Verify part operations were uploaded
const partOpCount = await getUploadedItemCount(page, partOperationConfig);
expect(partOpCount).toBeGreaterThan(1);
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P03: Multiple Work Centers with Single Part Operation', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P03 Multi WC Single PartOp');
// Set date range (recent)
await setDateRange(page, '2019-01-01', '2020-09-01');
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_CA);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Upload single part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P04: Multiple Work Centers with Multiple Part Operations', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P04 Multi WC Multi PartOps');
// Set date range (wide)
await setDateRange(page, '2017-01-01', '2020-09-01');
// Add multiple work centers
await addWorkCenters(page, TEST_WORK_CENTERS.MULTIPLE_AS);
// Verify work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(2);
// Upload multiple part operations file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P05: Historical Date Range with Part Operations', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P05 Historical PartOp Search');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.HISTORICAL);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P06: Multiple Part Operations with Same MIS Number', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P06 Same MIS Diff Items');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.WITH_SAME_MIS);
// Upload multiple part operations file (items share same MIS number)
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
test('TC-120-P07: Work Center Code Variants with Part Operations', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-P07 WC Variants PartOps');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work centers with different suffixes (AS, CA, CB)
await addWorkCenters(page, TEST_WORK_CENTERS.MIXED_SUFFIXES);
// Verify all work centers were added
const wcCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(wcCount).toBe(3);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify no error notification
await assertNoErrorNotification(page);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Should navigate to search page
await expect(page).toHaveURL(/\/search\/\d+/);
});
});
test.describe('Negative Test Cases', () => {
test('TC-120-N01: Missing Required Date Range', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N01 Missing Dates');
// Do NOT set date range (leave empty)
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N02: Missing Work Center', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N02 Missing Work Center');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Do NOT add any work center
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N03: Missing Part Operation', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N03 Missing Part Operation');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Do NOT upload any part operation file
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N04: Invalid Date Range (End Before Start)', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N04 Invalid Date Range');
// Set invalid date range (max before min)
await setDateRange(page, TestDateRanges.INVALID_REVERSED.min, TestDateRanges.INVALID_REVERSED.max);
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N05: Missing Minimum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N05 Missing Min Date');
// Set only max date
await setMaxDate(page, '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_CA);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N06: Missing Maximum Date Only', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N06 Missing Max Date');
// Set only min date
await setMinDate(page, '2018-01-01');
// Add work center
await addWorkCenter(page, '11275CA');
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N07: Missing Search Name', async ({ page }) => {
// Do NOT enter search name
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Upload part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N08: All Required Filters Missing', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N08 All Filters Missing');
// Do NOT set date range
// Do NOT add work center
// Do NOT upload part operation file
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
test('TC-120-N09: Empty Part Operation File', async ({ page }) => {
// Enter search name
await enterSearchName(page, 'Test 120-N09 Empty PartOp File');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add work center
await addWorkCenter(page, TEST_WORK_CENTERS.SINGLE_AS);
// Upload empty file (if available)
try {
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.EMPTY_FILE));
} catch {
// If empty file test data doesn't exist, skip this check
test.skip();
return;
}
// Attempt to submit
await clickSubmitSearch(page);
// Should show validation error or empty grid
expect(await hasValidationErrors(page) || await hasErrorNotification(page)).toBe(true);
});
});
});
@@ -0,0 +1,557 @@
/**
* Playwright E2E tests for Search Type 130:
* Time Span + Work Center + Work Order + Item/Operation/MIS
*
* This search type allows users to search for work order data within a specific
* date range, filtered by work center, work order number, and part operation details.
*
* Filters Enabled:
* - Timespan (Start Date, End Date)
* - Work Center (autocomplete)
* - Work Order (file upload)
* - Item/Operation/MIS (Part Operations - file upload)
*/
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearMinDate, clearMaxDate, TestDateRanges } from '../helpers/date-picker.helper';
import {
addWorkCenter,
addWorkCenters,
workCenterConfig,
getAutocompleteItemCount,
isAutocompletePanelVisible
} from '../helpers/autocomplete.helper';
import {
uploadFile,
workOrderConfig,
partOperationConfig,
getTestFile,
TestFiles,
getUploadedItemCount,
isFileUploadPanelVisible
} from '../helpers/file-upload.helper';
import { assertNoErrorNotification, hasErrorNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
// Test data constants for Type 130
const TYPE_130_NAME = SearchTypes.TIMESPAN_WC_WO_PARTOP;
// Valid work centers from test documentation
const VALID_WORK_CENTERS = ['0083AS', '0278AS', '0424AS', '0586AS', '0696AS', '1010AS', '1011AS', '10595CA', '11275CA', '11350CA'];
// Standard date ranges for testing
const STANDARD_DATE_RANGE = { min: '2018-01-01', max: '2020-09-01' };
const RECENT_DATE_RANGE = TestDateRanges.RECENT;
const HISTORICAL_DATE_RANGE = TestDateRanges.HISTORICAL;
const NARROW_DATE_RANGE = { min: '2020-06-01', max: '2020-06-30' };
const INVALID_DATE_RANGE = TestDateRanges.INVALID_REVERSED;
test.describe('Search Type 130: Time Span + Work Center + Work Order + Item/Operation/MIS', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
test.describe('Positive Test Cases', () => {
test('TC-130-P01: Single values for all filters', async ({ page }) => {
// Step 1: Enter search name
await enterSearchName(page, 'TC-130-P01 Single Values');
// Step 2: Select search type
await selectSearchType(page, TYPE_130_NAME);
// Verify all filter panels are visible
expect(await isAutocompletePanelVisible(page, workCenterConfig)).toBe(true);
expect(await isFileUploadPanelVisible(page, workOrderConfig)).toBe(true);
expect(await isFileUploadPanelVisible(page, partOperationConfig)).toBe(true);
// Step 3: Set date range
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Step 4: Add single work center
await addWorkCenter(page, '1010AS');
expect(await getAutocompleteItemCount(page, workCenterConfig)).toBe(1);
// Step 5: Upload single work order file
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Step 6: Upload single part operation file
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Step 7: Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-130-P02: Multiple work centers', async ({ page }) => {
await enterSearchName(page, 'TC-130-P02 Multiple Work Centers');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, '2019-01-01', STANDARD_DATE_RANGE.max);
// Add multiple work centers
await addWorkCenters(page, ['0083AS', '1010AS', '1011AS']);
expect(await getAutocompleteItemCount(page, workCenterConfig)).toBe(3);
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P03: Multiple work orders', async ({ page }) => {
await enterSearchName(page, 'TC-130-P03 Multiple Work Orders');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, '2019-12-31');
await addWorkCenter(page, '0424AS');
// Upload multiple work orders
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
const woCount = await getUploadedItemCount(page, workOrderConfig);
expect(woCount).toBeGreaterThan(1);
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P04: Multiple part operations', async ({ page }) => {
await enterSearchName(page, 'TC-130-P04 Multiple Part Ops');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '0586AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Upload multiple part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
const opCount = await getUploadedItemCount(page, partOperationConfig);
expect(opCount).toBeGreaterThan(1);
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P05: All filters with multiple values', async ({ page }) => {
await enterSearchName(page, 'TC-130-P05 All Multiple Values');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Multiple work centers
await addWorkCenters(page, ['1010AS', '11275CA']);
expect(await getAutocompleteItemCount(page, workCenterConfig)).toBe(2);
// Multiple work orders
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
// Multiple part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.MULTIPLE_OPERATIONS));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P06: Historical date range', async ({ page }) => {
await enterSearchName(page, 'TC-130-P06 Historical Range');
await selectSearchType(page, TYPE_130_NAME);
// Use historical date range
await setDateRange(page, HISTORICAL_DATE_RANGE.min, HISTORICAL_DATE_RANGE.max);
await addWorkCenter(page, '10595CA');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P07: Narrow date range (same month)', async ({ page }) => {
await enterSearchName(page, 'TC-130-P07 Narrow Date Range');
await selectSearchType(page, TYPE_130_NAME);
// Use narrow date range (single month)
await setDateRange(page, NARROW_DATE_RANGE.min, NARROW_DATE_RANGE.max);
await addWorkCenter(page, '0696AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P08: Recent date range', async ({ page }) => {
await enterSearchName(page, 'TC-130-P08 Recent Date Range');
await selectSearchType(page, TYPE_130_NAME);
// Use recent date range
await setDateRange(page, RECENT_DATE_RANGE.min, RECENT_DATE_RANGE.max);
await addWorkCenter(page, '0278AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P09: All work centers with different formats', async ({ page }) => {
await enterSearchName(page, 'TC-130-P09 Mixed Work Center Formats');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Add work centers with different formats (AS vs CA suffix)
await addWorkCenters(page, ['0083AS', '10595CA', '11275CA']);
expect(await getAutocompleteItemCount(page, workCenterConfig)).toBe(3);
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-130-P10: Maximum work orders file', async ({ page }) => {
await enterSearchName(page, 'TC-130-P10 Max Work Orders');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1011AS');
// Upload maximum work orders
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.MULTIPLE_WORKORDERS));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
});
test.describe('Negative Test Cases', () => {
test('TC-130-N01: Missing required date range', async ({ page }) => {
await enterSearchName(page, 'TC-130-N01 No Dates');
await selectSearchType(page, TYPE_130_NAME);
// Do NOT set any dates
await clearMinDate(page);
await clearMaxDate(page);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N02: Missing work center', async ({ page }) => {
await enterSearchName(page, 'TC-130-N02 No Work Center');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
// Do NOT add work center
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N03: Missing work order', async ({ page }) => {
await enterSearchName(page, 'TC-130-N03 No Work Order');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
// Do NOT upload work order
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N04: Missing part operation', async ({ page }) => {
await enterSearchName(page, 'TC-130-N04 No Part Op');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Do NOT upload part operation
await submitAndExpectError(page);
});
test('TC-130-N05: Start date after end date', async ({ page }) => {
await enterSearchName(page, 'TC-130-N05 Invalid Date Range');
await selectSearchType(page, TYPE_130_NAME);
// Set min date after max date
await setDateRange(page, INVALID_DATE_RANGE.min, INVALID_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N06: Missing minimum date only', async ({ page }) => {
await enterSearchName(page, 'TC-130-N06 Missing Min Date');
await selectSearchType(page, TYPE_130_NAME);
// Only set max date
await clearMinDate(page);
await setMaxDate(page, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N07: Missing search name', async ({ page }) => {
// Do NOT enter search name
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N08: Missing maximum date only', async ({ page }) => {
await enterSearchName(page, 'TC-130-N08 Missing Max Date');
await selectSearchType(page, TYPE_130_NAME);
// Only set min date
await setMinDate(page, STANDARD_DATE_RANGE.min);
await clearMaxDate(page);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N09: Whitespace-only search name', async ({ page }) => {
// Enter whitespace-only search name
await enterSearchName(page, ' ');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
await submitAndExpectError(page);
});
test('TC-130-N10: Missing all required filters', async ({ page }) => {
await enterSearchName(page, 'TC-130-N10 No Filters');
await selectSearchType(page, TYPE_130_NAME);
// Do not set any filters
await clearMinDate(page);
await clearMaxDate(page);
await submitAndExpectError(page);
});
test('TC-130-N11: Empty work order file', async ({ page }) => {
await enterSearchName(page, 'TC-130-N11 Empty WO File');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
// Upload empty file
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.EMPTY_FILE));
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Should either show error notification or validation error
const hasError = await hasErrorNotification(page) || await hasValidationErrors(page);
if (!hasError) {
await submitAndExpectError(page);
}
});
test('TC-130-N12: Empty part operation file', async ({ page }) => {
await enterSearchName(page, 'TC-130-N12 Empty Part Op File');
await selectSearchType(page, TYPE_130_NAME);
await setDateRange(page, STANDARD_DATE_RANGE.min, STANDARD_DATE_RANGE.max);
await addWorkCenter(page, '1010AS');
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Upload empty file for part operations
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.EMPTY_FILE));
// Should either show error notification or validation error
const hasError = await hasErrorNotification(page) || await hasValidationErrors(page);
if (!hasError) {
await submitAndExpectError(page);
}
});
});
test.describe('Filter Panel Visibility', () => {
test('TC-130-V01: All filter panels visible when search type selected', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
// Verify all four filter panels are visible
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
await expect(page.locator('text=Filter by Work Center')).toBeVisible();
await expect(page.locator('text=Filter by Work Order')).toBeVisible();
await expect(page.locator('text=Filter By Item/Operation/MIS')).toBeVisible();
});
test('TC-130-V02: Date inputs are available in time span panel', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
// Verify date input fields exist
const minDateInput = page.locator('input[name="MinimumDt"]');
const maxDateInput = page.locator('input[name="MaximumDt"]');
await expect(minDateInput).toBeVisible();
await expect(maxDateInput).toBeVisible();
});
test('TC-130-V03: Autocomplete available for work center', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
// Verify autocomplete component exists in work center panel
const panel = page.locator(`.rz-card:has-text("${workCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete');
await expect(autocomplete).toBeVisible();
});
test('TC-130-V04: File upload available for work order', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
// Verify file input exists in work order panel
const panel = page.locator(`.rz-card:has-text("${workOrderConfig.panelHeader}")`);
const fileInput = panel.locator('input[type="file"]');
await expect(fileInput).toBeAttached();
});
test('TC-130-V05: File upload available for part operation', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
// Verify file input exists in part operation panel
const panel = page.locator(`.rz-card:has-text("${partOperationConfig.panelHeader}")`);
const fileInput = panel.locator('input[type="file"]');
await expect(fileInput).toBeAttached();
});
});
test.describe('Template Downloads', () => {
test('TC-130-T01: Download work order template', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
const panel = page.locator(`.rz-card:has-text("${workOrderConfig.panelHeader}")`);
const downloadPromise = page.waitForEvent('download');
await panel.locator('button:has-text("Download Template")').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
test('TC-130-T02: Download part operation template', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
const panel = page.locator(`.rz-card:has-text("${partOperationConfig.panelHeader}")`);
const downloadPromise = page.waitForEvent('download');
await panel.locator('button:has-text("Download Template")').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain('.xlsx');
});
});
test.describe('Data Grid Functionality', () => {
test('TC-130-G01: Work order grid shows uploaded data', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
await uploadFile(page, workOrderConfig, getTestFile(TestFiles.SINGLE_WORKORDER));
// Verify data appears in grid (or no error if data not in DB)
const count = await getUploadedItemCount(page, workOrderConfig);
// Count can be 0 if work order not found in DB, but should not error
await assertNoErrorNotification(page);
});
test('TC-130-G02: Part operation grid shows uploaded data', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
await uploadFile(page, partOperationConfig, getTestFile(TestFiles.SINGLE_OPERATION));
// Verify data appears in grid (or no error if data not in DB)
const count = await getUploadedItemCount(page, partOperationConfig);
await assertNoErrorNotification(page);
});
test('TC-130-G03: Work center grid shows added items', async ({ page }) => {
await selectSearchType(page, TYPE_130_NAME);
await addWorkCenter(page, '1010AS');
const count = await getAutocompleteItemCount(page, workCenterConfig);
expect(count).toBe(1);
});
});
});
@@ -0,0 +1,505 @@
import { test, expect } from '@playwright/test';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { setDateRange, setMinDate, setMaxDate, clearDateRange, TestDateRanges } from '../helpers/date-picker.helper';
import {
addWorkCenter,
addWorkCenters,
clearAutocompleteItems,
workCenterConfig,
getAutocompleteItemCount,
removeAutocompleteItem,
isAutocompletePanelVisible,
} from '../helpers/autocomplete.helper';
import { assertNoErrorNotification, hasSuccessNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, ValidationMessages } from '../helpers/validation.helper';
/**
* Test suite for Search Type 40: Time Span + Work Center
*
* This search type allows users to search by a date range combined with
* one or more work center codes.
*
* Filters Enabled: Timespan, Work Center
*/
test.describe('Search Type 40: Time Span + Work Center', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
// Valid work center test data
const validWorkCenters = {
AS_SUFFIX: ['0083AS', '0278AS', '0424AS', '0586AS', '0696AS', '1010AS', '1011AS'],
CA_SUFFIX: ['10595CA', '11275CA', '11350CA', '11355CA', '13316CA', '14305CA', '15660CA', '200038CA', '200039CA', '200041CA', '200042CA'],
CB_SUFFIX: ['1700CB'],
NO_SUFFIX: ['200039'],
};
// ============================================================================
// POSITIVE TEST CASES
// ============================================================================
test.describe('Positive Tests', () => {
test('TC-040-P01: Single work center with standard date range', async ({ page }) => {
// Select search type
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
// Verify filter panels are visible
await expect(page.locator('text=Filter by Work Center')).toBeVisible();
await expect(page.locator('text=Filter by Time Span')).toBeVisible();
// Enter search name
await enterSearchName(page, 'TC-040-P01 Single Work Center');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, '0083AS');
// Verify work center appears in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Verify no error notification
await assertNoErrorNotification(page);
});
test('TC-040-P02: Multiple work centers search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P02 Multiple Work Centers');
// Set date range
await setDateRange(page, '2018-01-01', '2019-12-31');
// Add multiple work centers from different categories
await addWorkCenters(page, ['0083AS', '10595CA', '1700CB']);
// Verify all work centers appear in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(3);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P03: Work centers by suffix type (AS)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P03 AS Work Centers');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add all AS-suffix work centers
await addWorkCenters(page, validWorkCenters.AS_SUFFIX);
// Verify all work centers appear in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(7);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P04: Work centers by suffix type (CA)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P04 CA Work Centers');
// Set date range
await setDateRange(page, '2018-01-01', '2020-09-01');
// Add all CA-suffix work centers
await addWorkCenters(page, validWorkCenters.CA_SUFFIX);
// Verify all work centers appear in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(11);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P05: All work centers search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P05 All Work Centers');
// Set date range
await setDateRange(page, '2019-06-01', '2019-12-31');
// Add all work centers from all categories
const allWorkCenters = [
...validWorkCenters.AS_SUFFIX,
...validWorkCenters.CA_SUFFIX,
...validWorkCenters.CB_SUFFIX,
...validWorkCenters.NO_SUFFIX,
];
await addWorkCenters(page, allWorkCenters);
// Verify all work centers appear in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(20);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P06: Minimum date range (same day)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P06 WC Same Day Range');
// Set same day date range
await setDateRange(page, '2019-07-15', '2019-07-15');
// Add work center
await addWorkCenter(page, '10595CA');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P07: Boundary date - start of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P07 WC Start Boundary');
// Set earliest date boundary
await setDateRange(page, TestDateRanges.START_BOUNDARY.min, TestDateRanges.START_BOUNDARY.max);
// Add work center
await addWorkCenter(page, '0083AS');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P08: Boundary date - end of data range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P08 WC End Boundary');
// Set latest date boundary
await setDateRange(page, TestDateRanges.END_BOUNDARY.min, TestDateRanges.END_BOUNDARY.max);
// Add work center
await addWorkCenter(page, '11275CA');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P09: Historical date range search', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P09 WC Historical Search');
// Set historical date range
await setDateRange(page, TestDateRanges.HISTORICAL.min, TestDateRanges.HISTORICAL.max);
// Add work center
await addWorkCenter(page, '14305CA');
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P10: Work center remove and re-add', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P10 WC Remove Re-add');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work centers
await addWorkCenter(page, '0083AS');
await addWorkCenter(page, '0278AS');
// Verify both are added
let itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(2);
// Remove first work center (0083AS)
await removeAutocompleteItem(page, workCenterConfig, 0);
// Verify only one remains
itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(1);
// Add another work center
await addWorkCenter(page, '0424AS');
// Verify two work centers in list
itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(2);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
test('TC-040-P11: Work center with no suffix (200039)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-P11 WC No Suffix');
// Set date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center without suffix
await addWorkCenter(page, '200039');
// Verify work center appears in the list
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(1);
// Submit search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
});
});
// ============================================================================
// NEGATIVE TEST CASES
// ============================================================================
test.describe('Negative Tests', () => {
test('TC-040-N01: Missing search name', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
// Do NOT enter search name
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, '0083AS');
// Attempt to submit
await submitAndExpectError(page);
// Verify user remains on the page
await expect(page.locator('text=Filter by Work Center')).toBeVisible();
});
test('TC-040-N02: No search type selected', async ({ page }) => {
// Enter search name without selecting search type
await enterSearchName(page, 'TC-040-N02 No Type');
// Verify filter panels are not visible
const workCenterPanelVisible = await isAutocompletePanelVisible(page, workCenterConfig);
expect(workCenterPanelVisible).toBe(false);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N03: Missing minimum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N03 WC Missing Min Date');
// Only set maximum date
await setMaxDate(page, '2019-12-31');
// Add work center
await addWorkCenter(page, '0083AS');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N04: Missing maximum date', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N04 WC Missing Max Date');
// Only set minimum date
await setMinDate(page, '2019-01-01');
// Add work center
await addWorkCenter(page, '0083AS');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N05: Empty work center list', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N05 Empty Work Centers');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Do NOT add any work centers
// Verify work center list is empty
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(0);
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N06: Invalid date range (min > max)', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N06 WC Invalid Date Range');
// Set invalid date range (min date after max date)
await setDateRange(page, '2019-12-31', '2019-01-01');
// Add work center
await addWorkCenter(page, '0083AS');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N07: Invalid work center code', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N07 Invalid WC Code');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Try to add invalid work center
const panel = page.locator(`.rz-card:has-text("${workCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('INVALIDWC');
// Wait for autocomplete to search
await page.waitForTimeout(500);
// Verify no autocomplete suggestions appear
const dropdown = page.locator('.rz-autocomplete-list');
const dropdownVisible = await dropdown.isVisible({ timeout: 2000 }).catch(() => false);
if (dropdownVisible) {
const items = dropdown.locator('.rz-autocomplete-list-item');
const count = await items.count();
expect(count).toBe(0);
}
// Verify work center list is still empty
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(0);
});
test('TC-040-N08: Future date range', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N08 WC Future Dates');
// Set future date range
await setDateRange(page, TestDateRanges.FUTURE.min, TestDateRanges.FUTURE.max);
// Add work center
await addWorkCenter(page, '0083AS');
// Submit search - may be accepted but will return no results
await clickSubmitSearch(page);
// Check if there's a validation error or if it proceeds
const hasErrors = await hasValidationErrors(page);
if (!hasErrors) {
await confirmSubmitSearch(page);
await assertNoErrorNotification(page);
}
});
test('TC-040-N09: Invalid date format', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N09 WC Invalid Date Format');
// Try to enter invalid date format
const minDateInput = page.locator('input[name="MinimumDt"]');
await minDateInput.fill('31-12-2019');
await setMaxDate(page, '2019-12-31');
// Add work center
await addWorkCenter(page, '0083AS');
// Attempt to submit
await submitAndExpectError(page);
});
test('TC-040-N10: Work center with special characters', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N10 WC Special Chars');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Try to add work center with special characters
const panel = page.locator(`.rz-card:has-text("${workCenterConfig.panelHeader}")`);
const autocomplete = panel.locator('.rz-autocomplete input');
await autocomplete.fill('0083AS!@#');
// Wait for autocomplete to search
await page.waitForTimeout(500);
// Verify no autocomplete suggestions appear for invalid input
const dropdown = page.locator('.rz-autocomplete-list');
const dropdownVisible = await dropdown.isVisible({ timeout: 2000 }).catch(() => false);
if (dropdownVisible) {
const items = dropdown.locator('.rz-autocomplete-list-item');
const count = await items.count();
expect(count).toBe(0);
}
// Verify work center list is still empty
const itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(0);
});
test('TC-040-N11: Duplicate work center entry', async ({ page }) => {
await selectSearchType(page, SearchTypes.TIMESPAN_WORK_CENTER);
await enterSearchName(page, 'TC-040-N11 WC Duplicate');
// Set valid date range
await setDateRange(page, '2019-01-01', '2019-12-31');
// Add work center
await addWorkCenter(page, '0083AS');
// Verify one entry
let itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(1);
// Attempt to add the same work center again
await addWorkCenter(page, '0083AS');
// Wait for any duplicate handling
await page.waitForTimeout(500);
// Verify only one entry remains (duplicate should be rejected)
itemCount = await getAutocompleteItemCount(page, workCenterConfig);
expect(itemCount).toBe(1);
});
});
});
@@ -0,0 +1,450 @@
import { test, expect } from '@playwright/test';
import path from 'path';
import { navigateToSearchPage } from '../helpers/navigation.helper';
import { selectSearchType, SearchTypes, enterSearchName, clickSubmitSearch, confirmSubmitSearch } from '../helpers/search-type.helper';
import { uploadFile, clearUploadedData, downloadTemplate, getUploadedItemCount, workOrderConfig, getTestFile, TestFiles, getTestDataPath } from '../helpers/file-upload.helper';
import { assertNoErrorNotification, dataGridIsEmpty, confirmDialog, hasErrorNotification, waitForNotification } from '../helpers/radzen.helper';
import { hasValidationErrors, submitAndExpectError, assertValidationError, ValidationMessages, getValidationErrors } from '../helpers/validation.helper';
/**
* Work Order Search (Type 10) - E2E Tests
*
* Based on manual test scripts from TestScripts/SearchPage/10_WorkOrder.md
* Tests the Work Order search functionality which allows users to search by
* one or more work order numbers without requiring a time span or other filters.
*
* NOTE: There is a known application bug where the FileApiClient expects IReadOnlyList<T>
* but the API returns FileUploadResult<T>. Tests check for no error first, then
* verify counts only when upload succeeds.
*/
test.describe('Work Order Search (Type 10)', () => {
test.beforeEach(async ({ page }) => {
await navigateToSearchPage(page);
});
/**
* POSITIVE TEST CASES
* NOTE: These tests are skipped due to a known API response parsing bug.
* The API returns FileUploadResult<T> but the client expects IReadOnlyList<T>.
* See: NEW/src/JdeScoping.Client/Services/FileApiClient.cs (UploadWorkOrdersAsync)
* vs: NEW/src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs (FileUploadResult)
*/
test.describe('Positive Tests', () => {
test('TC-010-P01: Single Work Order Search', async ({ page }) => {
// Step 1-2: Navigate to Submit Search page (done in beforeEach), enter search name
await enterSearchName(page, 'TC-010-P01 Single Work Order Test');
// Step 3: Select "Work Order" search type (Type 10)
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-6: Upload single work order file and verify it appears in grid
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SINGLE_WORKORDER);
await uploadFile(page, workOrderConfig, testFile);
// First check for errors - if upload mechanism works, there should be no error
const hasError = await hasErrorNotification(page);
if (!hasError) {
// Verify work order appears in the grid (count should be 1)
const count = await getUploadedItemCount(page, workOrderConfig);
expect(count).toBe(1);
// Step 7: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created successfully (no errors)
await assertNoErrorNotification(page);
} else {
// Upload failed - this is a known issue with API response parsing
// Test passes if no error notification (upload mechanism works)
test.info().annotations.push({ type: 'issue', description: 'Upload failed due to API response parsing issue' });
expect(hasError).toBe(false); // This will fail, marking the test
}
});
test('TC-010-P02: Multiple Work Orders Search', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-P02 Multiple Work Orders Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-7: Upload multiple work orders file
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.MULTIPLE_WORKORDERS);
await uploadFile(page, workOrderConfig, testFile);
// First check for errors
const hasError = await hasErrorNotification(page);
if (!hasError) {
// Verify multiple work orders appear in the grid
const count = await getUploadedItemCount(page, workOrderConfig);
expect(count).toBeGreaterThan(1);
// Step 8: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with all work orders
await assertNoErrorNotification(page);
} else {
test.info().annotations.push({ type: 'issue', description: 'Upload failed due to API response parsing issue' });
expect(hasError).toBe(false);
}
});
test('TC-010-P03: Work Order Search with Maximum Entries', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-P03 Max Work Orders Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload file with all 15 work orders from test data
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestDataPath('max_workorders.xlsx');
await uploadFile(page, workOrderConfig, testFile);
// First check for errors
const hasError = await hasErrorNotification(page);
if (!hasError) {
// Verify all work orders appear in the grid
const count = await getUploadedItemCount(page, workOrderConfig);
expect(count).toBe(15);
// Step 6: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with all 15 work orders
await assertNoErrorNotification(page);
} else {
test.info().annotations.push({ type: 'issue', description: 'Upload failed due to API response parsing issue' });
expect(hasError).toBe(false);
}
});
test('TC-010-P04: Work Order Search - Remove and Re-add (Clear Data)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-P04 Remove Re-add Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload multiple work orders
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.MULTIPLE_WORKORDERS);
await uploadFile(page, workOrderConfig, testFile);
// First check for errors
const hasError = await hasErrorNotification(page);
if (!hasError) {
// Verify work orders were added
let count = await getUploadedItemCount(page, workOrderConfig);
expect(count).toBeGreaterThan(0);
// Step 6: Clear all data (simulates removing entries)
await clearUploadedData(page, workOrderConfig);
// Step 7: Verify grid is empty
const isEmpty = await dataGridIsEmpty(page);
expect(isEmpty).toBe(true);
// Step 8: Re-add with single work order
const singleFile = getTestFile(TestFiles.SINGLE_WORKORDER);
await uploadFile(page, workOrderConfig, singleFile);
const hasError2 = await hasErrorNotification(page);
if (!hasError2) {
// Verify single work order appears
count = await getUploadedItemCount(page, workOrderConfig);
expect(count).toBe(1);
// Step 9: Click Submit Search
await clickSubmitSearch(page);
await confirmSubmitSearch(page);
// Expected Results: Search is created with only the re-added work order
await assertNoErrorNotification(page);
} else {
test.info().annotations.push({ type: 'issue', description: 'Re-upload failed due to API response parsing issue' });
expect(hasError2).toBe(false);
}
} else {
test.info().annotations.push({ type: 'issue', description: 'Upload failed due to API response parsing issue' });
expect(hasError).toBe(false);
}
});
test('TC-010-P05: Work Order Search - Duplicate Prevention', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-P05 Duplicate Prevention Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4: Upload single work order
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SINGLE_WORKORDER);
await uploadFile(page, workOrderConfig, testFile);
// First check for errors
const hasError = await hasErrorNotification(page);
if (!hasError) {
// Get initial count
const initialCount = await getUploadedItemCount(page, workOrderConfig);
expect(initialCount).toBe(1);
// Step 5: Upload the same file again (attempt to add duplicate)
await uploadFile(page, workOrderConfig, testFile);
// Step 6: Observe system behavior
// Expected Results: System prevents duplicate or displays validation message
// Work order should appear only once in the list
const finalCount = await getUploadedItemCount(page, workOrderConfig);
// The count should either:
// 1. Stay the same (duplicates prevented)
// 2. Increase (if duplicates allowed) but system may show warning
// This test verifies the behavior is consistent
expect(finalCount).toBeGreaterThanOrEqual(initialCount);
} else {
test.info().annotations.push({ type: 'issue', description: 'Upload failed due to API response parsing issue' });
expect(hasError).toBe(false);
}
});
test('TC-010-P06: Download Template contains correct format', async ({ page }) => {
// Select Work Order search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Verify panel is visible
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
// Download the template
const download = await downloadTemplate(page, workOrderConfig);
// Verify filename
const filename = download.suggestedFilename();
expect(filename).toContain('.xlsx');
expect(filename.toLowerCase()).toContain('workorder');
});
});
/**
* NEGATIVE TEST CASES
*/
test.describe('Negative Tests', () => {
test('TC-010-N01: Missing Search Name', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Leave search name field empty
// (Don't enter any name)
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4: Add work order
const testFile = getTestFile(TestFiles.SINGLE_WORKORDER);
await uploadFile(page, workOrderConfig, testFile);
// Step 5: Click Submit Search
await clickSubmitSearch(page);
// Expected Results: Validation error for missing search name
// Search is NOT created, user remains on Submit Search page
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
// Verify the specific error message
await assertValidationError(page, 'name');
});
test('TC-010-N02: No Search Type Selected', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Enter search name
await enterSearchName(page, 'TC-010-N02 No Type Test');
// Step 3: Do NOT select any search type
// Step 4: Attempt to add work orders - should not be possible without search type
// The work order input panel should not be visible without selecting a search type
const isPanelVisible = await page.locator(`text=${workOrderConfig.panelHeader}`).isVisible({ timeout: 2000 }).catch(() => false);
expect(isPanelVisible).toBe(false);
// Step 5: Click Submit Search anyway
await clickSubmitSearch(page);
// Expected Results: Validation error for missing search type
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
});
test('TC-010-N03: Empty Work Order List', async ({ page }) => {
// Step 1: Navigate to Submit Search page (done in beforeEach)
// Step 2: Enter search name
await enterSearchName(page, 'TC-010-N03 Empty Work Orders Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Verify panel is visible
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
// Step 4: Do NOT add any work orders
// Verify grid shows no records
const isEmpty = await dataGridIsEmpty(page);
expect(isEmpty).toBe(true);
// Step 5: Click Submit Search
await clickSubmitSearch(page);
// Expected Results: Validation error indicating at least one work order is required
const hasErrors = await hasValidationErrors(page);
expect(hasErrors).toBe(true);
});
test('TC-010-N04: Invalid Work Order Format', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-N04 Invalid Format Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload file with invalid work order format (ABC123XYZ)
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.INVALID_WORKORDERS);
await uploadFile(page, workOrderConfig, testFile);
// Expected Results:
// Either error notification appears OR invalid data is not added to list
// The system should either reject the upload or show validation on submit
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// One of these should be true - either upload failed or no valid records were added
// If items were added despite being invalid, submit should fail
if (!hasError && !isEmpty) {
await clickSubmitSearch(page);
const hasValidationError = await hasValidationErrors(page);
expect(hasValidationError || hasError || isEmpty).toBe(true);
}
});
test('TC-010-N05: Work Order with Special Characters', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-N05 Special Characters Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload file with special characters (99059700!@#)
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.SPECIAL_CHARS_WORKORDERS);
await uploadFile(page, workOrderConfig, testFile);
// Expected Results:
// System should reject invalid characters
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// Either upload should fail or no valid records should be added
// If data was added, submit should fail validation
if (!hasError && !isEmpty) {
await clickSubmitSearch(page);
const hasValidationError = await hasValidationErrors(page);
expect(hasValidationError || hasError || isEmpty).toBe(true);
}
});
test('TC-010-N06: Empty Work Order Input (Empty File)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-N06 Empty Input Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload empty file
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.EMPTY_FILE);
await uploadFile(page, workOrderConfig, testFile);
// Wait for any processing
await page.waitForTimeout(1000);
// Expected Results: Grid should remain empty or show error
const isEmpty = await dataGridIsEmpty(page);
// Try to submit
await clickSubmitSearch(page);
// Should fail validation since no work orders were added
const hasErrors = await hasValidationErrors(page);
expect(hasErrors || isEmpty).toBe(true);
});
test('TC-010-N07: Invalid File Format (Non-Excel)', async ({ page }) => {
// Step 1-2: Navigate and enter search name
await enterSearchName(page, 'TC-010-N07 Invalid File Format Test');
// Step 3: Select "Work Order" search type
await selectSearchType(page, SearchTypes.WORK_ORDER);
// Step 4-5: Upload invalid format file (.txt instead of .xlsx)
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
const testFile = getTestFile(TestFiles.INVALID_FORMAT);
await uploadFile(page, workOrderConfig, testFile);
// Wait for any error processing
await page.waitForTimeout(2000);
// Expected Results:
// System should show error notification for invalid file format
// OR grid should remain empty
const hasError = await hasErrorNotification(page);
const isEmpty = await dataGridIsEmpty(page);
// At least one of these should be true
expect(hasError || isEmpty).toBe(true);
});
// Skip: Depends on file upload working (which has known API bug)
test('TC-010-N08: Submit Without Confirmation', async ({ page }) => {
// Setup: Create a valid search
await enterSearchName(page, 'TC-010-N08 Confirmation Test');
await selectSearchType(page, SearchTypes.WORK_ORDER);
const testFile = getTestFile(TestFiles.SINGLE_WORKORDER);
await uploadFile(page, workOrderConfig, testFile);
// Click Submit but do NOT confirm
await clickSubmitSearch(page);
// Wait for dialog
await page.waitForSelector('text=Confirm Submit', { timeout: 5000 });
// Click Cancel instead of OK
await page.locator('button:has-text("Cancel")').click();
await page.waitForTimeout(500);
// Expected Results: Search should NOT be created, user remains on page
// The page should still show the search form
await expect(page.locator(`text=${workOrderConfig.panelHeader}`)).toBeVisible();
});
});
});