Migrate Playwright suite to .NET UI tests and deprecate TS project
This commit is contained in:
@@ -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;
|
||||
+1179
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);
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
This is not a valid Excel file
|
||||
Just plain text
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user