1207 lines
49 KiB
TypeScript
1207 lines
49 KiB
TypeScript
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import { login } from './helpers/auth';
|
||
|
|
import { epic, feature, severity, description, tag } from './helpers/allure';
|
||
|
|
import { navigateToManagement, navigateToTerminals } from './helpers/navigation';
|
||
|
|
import * as fs from 'fs';
|
||
|
|
|
||
|
|
test.describe('Management Tests', () => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
epic('Management');
|
||
|
|
await login(page);
|
||
|
|
await navigateToManagement(page);
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should add a new cashier user', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Add a new cashier user by filling the form, selecting branch, role, and an unassociated terminal ID');
|
||
|
|
|
||
|
|
const timestamp = Date.now();
|
||
|
|
const fullName = `Test Cashier ${timestamp}`;
|
||
|
|
const username = `cashier${timestamp}`;
|
||
|
|
const password = 'Mav@1234';
|
||
|
|
|
||
|
|
// First, go to terminals page to find an unassociated terminal ID
|
||
|
|
let freeTerminalId = '';
|
||
|
|
|
||
|
|
await test.step('Find unassociated terminal from Terminals page', async () => {
|
||
|
|
// Navigate to terminals page to find a free terminal
|
||
|
|
await navigateToTerminals(page);
|
||
|
|
await page.waitForTimeout(5000);
|
||
|
|
|
||
|
|
const tableVisible = await page.locator('table tbody tr').first().isVisible({ timeout: 15000 }).catch(() => false);
|
||
|
|
if (!tableVisible) {
|
||
|
|
console.log('Terminal table not loaded, skipping terminal lookup');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const headers = await page.locator('table thead th').allTextContents();
|
||
|
|
console.log('Terminal table headers:', headers);
|
||
|
|
|
||
|
|
// Find Cashier column index and Terminal Id column index
|
||
|
|
const cashierColIdx = headers.findIndex(h => h.toLowerCase().includes('cashier') && !h.toLowerCase().includes('id') && !h.toLowerCase().includes('username'));
|
||
|
|
const terminalIdColIdx = headers.findIndex(h => h.toLowerCase().includes('terminal id'));
|
||
|
|
console.log(`Cashier col: ${cashierColIdx}, Terminal Id col: ${terminalIdColIdx}`);
|
||
|
|
|
||
|
|
// Search through pages for a terminal with no cashier
|
||
|
|
for (let pageNum = 1; pageNum <= 3 && !freeTerminalId; pageNum++) {
|
||
|
|
if (pageNum > 1) {
|
||
|
|
const pageBtn = page.locator(`button:has-text("${pageNum}")`).first();
|
||
|
|
const vis = await pageBtn.isVisible().catch(() => false);
|
||
|
|
if (vis) {
|
||
|
|
await pageBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
} else break;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rows = page.locator('table tbody tr');
|
||
|
|
const rowCount = await rows.count();
|
||
|
|
console.log(`Page ${pageNum}: ${rowCount} rows`);
|
||
|
|
|
||
|
|
for (let i = 0; i < rowCount; i++) {
|
||
|
|
const cells = rows.nth(i).locator('td');
|
||
|
|
const cashierText = (await cells.nth(cashierColIdx >= 0 ? cashierColIdx : 3).textContent({ timeout: 2000 }).catch(() => ''))?.trim() || '';
|
||
|
|
const termIdText = (await cells.nth(terminalIdColIdx >= 0 ? terminalIdColIdx : 7).textContent({ timeout: 2000 }).catch(() => ''))?.trim() || '';
|
||
|
|
|
||
|
|
console.log(`Row ${i}: cashier="${cashierText}" terminalId="${termIdText}"`);
|
||
|
|
|
||
|
|
// Unassociated = cashier cell is empty or "Unassigned"
|
||
|
|
if ((!cashierText || cashierText === 'Unassigned' || cashierText === '-') && termIdText && termIdText !== '-' && /^\d{8,}$/.test(termIdText)) {
|
||
|
|
freeTerminalId = termIdText;
|
||
|
|
console.log(`✓ Found free terminal ID (8+ digits): ${freeTerminalId}`);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!freeTerminalId) {
|
||
|
|
console.log('⚠ No unassociated terminal found — run "remove cashier" test first');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Navigate back to Management', async () => {
|
||
|
|
await navigateToManagement(page);
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Click Add User button', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 }).catch(() => {});
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
const addUserBtn = page.locator('button:has-text("Add User")').first();
|
||
|
|
await addUserBtn.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await addUserBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Add User modal opened');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Full Name', async () => {
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
const fullNameInput = page.locator('#fullName').first();
|
||
|
|
await fullNameInput.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await fullNameInput.fill(fullName);
|
||
|
|
console.log('✓ Full name filled:', fullName);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Username', async () => {
|
||
|
|
const usernameInput = page.locator('#username').first();
|
||
|
|
await usernameInput.fill(username);
|
||
|
|
console.log('✓ Username filled:', username);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Password', async () => {
|
||
|
|
const passwordInput = page.locator('#password').first();
|
||
|
|
await passwordInput.fill(password);
|
||
|
|
console.log('✓ Password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Confirm Password', async () => {
|
||
|
|
const confirmInput = page.locator('#confirmPassword').first();
|
||
|
|
await confirmInput.fill(password);
|
||
|
|
console.log('✓ Confirm password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Branch from dropdown', async () => {
|
||
|
|
const branchSelect = page.locator('.ant-select').filter({ hasText: /Select a branch/i }).first();
|
||
|
|
const isVisible = await branchSelect.isVisible().catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await branchSelect.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Branch dropdown opened');
|
||
|
|
|
||
|
|
const options = page.locator('.ant-select-item-option');
|
||
|
|
const optionCount = await options.count();
|
||
|
|
console.log(`Found ${optionCount} branch options`);
|
||
|
|
|
||
|
|
for (let i = 0; i < optionCount; i++) {
|
||
|
|
const optionText = (await options.nth(i).textContent())?.trim();
|
||
|
|
if (optionText && optionText !== '-' && optionText.length > 1) {
|
||
|
|
await options.nth(i).click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Branch selected:', optionText);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log('Branch dropdown not found');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Role: CASHIER', async () => {
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const roleSelect = page.locator('.ant-select:has(.anticon-solution)').first();
|
||
|
|
const roleSelectSelector = roleSelect.locator('.ant-select-selector');
|
||
|
|
await roleSelectSelector.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Role dropdown clicked');
|
||
|
|
|
||
|
|
const cashierOption = page.locator('.ant-select-item-option-content', { hasText: 'Cashier' }).first();
|
||
|
|
await cashierOption.waitFor({ state: 'visible', timeout: 5000 });
|
||
|
|
await cashierOption.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ CASHIER role selected');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select unassociated Terminal ID', async () => {
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
const terminalLabel = page.locator('label[for="terminalId"]');
|
||
|
|
const terminalLabelVisible = await terminalLabel.isVisible({ timeout: 5000 }).catch(() => false);
|
||
|
|
console.log('Terminal label visible:', terminalLabelVisible);
|
||
|
|
|
||
|
|
if (terminalLabelVisible) {
|
||
|
|
const terminalSelect = page.locator('.ant-select:has(.anticon-mobile)').first();
|
||
|
|
const terminalSelector = terminalSelect.locator('.ant-select-selector');
|
||
|
|
await terminalSelector.click();
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
console.log('✓ Terminal dropdown opened');
|
||
|
|
|
||
|
|
await page.waitForSelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option', { timeout: 5000 }).catch(() => {});
|
||
|
|
const dropdownOptions = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option');
|
||
|
|
const count = await dropdownOptions.count();
|
||
|
|
console.log(`Visible terminal options: ${count}`);
|
||
|
|
|
||
|
|
if (count > 0 && freeTerminalId) {
|
||
|
|
// Try to find the specific free terminal we identified
|
||
|
|
let selected = false;
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const optText = (await dropdownOptions.nth(i).textContent())?.trim();
|
||
|
|
console.log(`Option ${i}: "${optText}"`);
|
||
|
|
if (optText?.includes(freeTerminalId)) {
|
||
|
|
await dropdownOptions.nth(i).click({ force: true });
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Terminal selected (matched free ID):', optText);
|
||
|
|
selected = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!selected) {
|
||
|
|
// Pick first option with 8+ digit ID
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const optText = (await dropdownOptions.nth(i).textContent())?.trim();
|
||
|
|
const idMatch = optText?.match(/Id:\s*(\d+)/i);
|
||
|
|
if (idMatch && idMatch[1].length >= 8) {
|
||
|
|
await dropdownOptions.nth(i).click({ force: true });
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Terminal selected (8+ digit fallback):', optText);
|
||
|
|
selected = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!selected) {
|
||
|
|
console.log('⚠ No terminal with 8+ digit ID found in dropdown');
|
||
|
|
}
|
||
|
|
} else if (count > 0) {
|
||
|
|
// No freeTerminalId from lookup — pick first with 8+ digit ID
|
||
|
|
let selected = false;
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const optText = (await dropdownOptions.nth(i).textContent())?.trim();
|
||
|
|
console.log(`Option ${i}: "${optText}"`);
|
||
|
|
const idMatch = optText?.match(/Id:\s*(\d+)/i);
|
||
|
|
if (idMatch && idMatch[1].length >= 8) {
|
||
|
|
await dropdownOptions.nth(i).click({ force: true });
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Terminal selected (8+ digit):', optText);
|
||
|
|
selected = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!selected) {
|
||
|
|
console.log('⚠ No terminal with 8+ digit ID found');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log('No terminal options — run terminals test first to free up a terminal');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log('Terminal label not visible — CASHIER role may not have been selected');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Submit the form', async () => {
|
||
|
|
// The submit button in the modal footer has text "Add User" with UserAddOutlined icon
|
||
|
|
const submitBtn = page.locator('[role="dialog"] button:has-text("Add User"), .ant-modal button:has-text("Add User")').last();
|
||
|
|
const isVisible = await submitBtn.isVisible().catch(() => false);
|
||
|
|
|
||
|
|
if (isVisible) {
|
||
|
|
console.log('Clicking Add User submit button...');
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log('✓ Form submitted');
|
||
|
|
} else {
|
||
|
|
console.log('Submit button not found');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify new cashier appears in table', async () => {
|
||
|
|
// Wait for modal to close and reload page
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
await navigateToManagement(page);
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
|
||
|
|
// Check full page body for the username
|
||
|
|
const bodyText = await page.locator('body').textContent().catch(() => '');
|
||
|
|
let userFound = bodyText?.includes(username) || false;
|
||
|
|
|
||
|
|
if (!userFound) {
|
||
|
|
// Try checking through a couple pages
|
||
|
|
for (let p = 0; p < 3 && !userFound; p++) {
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const nextVis = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (nextVis) {
|
||
|
|
await nextBtn.click();
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
const text = await page.locator('body').textContent().catch(() => '');
|
||
|
|
if (text?.includes(username)) {
|
||
|
|
userFound = true;
|
||
|
|
}
|
||
|
|
} else break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`User "${username}" found:`, userFound);
|
||
|
|
expect(userFound).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Add cashier test completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should add a new finance user', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Add a new finance user by filling the form, selecting branch and Finance role, then verify in table');
|
||
|
|
|
||
|
|
const timestamp = Date.now();
|
||
|
|
const fullName = `Test Finance ${timestamp}`;
|
||
|
|
const username = `finance${timestamp}`;
|
||
|
|
const email = `finance${timestamp}@test.com`;
|
||
|
|
const password = 'Mav@1234';
|
||
|
|
|
||
|
|
await test.step('Click Add User button', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 20000 }).catch(() => {});
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
// Find the Add User button - it has the user-add icon and "Add User" text
|
||
|
|
const addUserBtn = page.locator('button').filter({ hasText: 'Add User' }).first();
|
||
|
|
await addUserBtn.waitFor({ state: 'visible', timeout: 20000 });
|
||
|
|
await addUserBtn.click({ force: true });
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Add User modal opened');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Full Name', async () => {
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
const fullNameInput = page.locator('#fullName').first();
|
||
|
|
await fullNameInput.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await fullNameInput.fill(fullName);
|
||
|
|
console.log('✓ Full name filled:', fullName);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Username', async () => {
|
||
|
|
const usernameInput = page.locator('#username').first();
|
||
|
|
await usernameInput.fill(email);
|
||
|
|
console.log('✓ Username (email) filled:', email);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Password', async () => {
|
||
|
|
const passwordInput = page.locator('#password').first();
|
||
|
|
await passwordInput.fill(password);
|
||
|
|
console.log('✓ Password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Confirm Password', async () => {
|
||
|
|
const confirmInput = page.locator('#confirmPassword').first();
|
||
|
|
await confirmInput.fill(password);
|
||
|
|
console.log('✓ Confirm password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Branch from dropdown', async () => {
|
||
|
|
const branchSelect = page.locator('.ant-select').filter({ hasText: /Select a branch/i }).first();
|
||
|
|
const isVisible = await branchSelect.isVisible().catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await branchSelect.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Branch dropdown opened');
|
||
|
|
|
||
|
|
const options = page.locator('.ant-select-item-option');
|
||
|
|
const optionCount = await options.count();
|
||
|
|
for (let i = 0; i < optionCount; i++) {
|
||
|
|
const optionText = (await options.nth(i).textContent())?.trim();
|
||
|
|
if (optionText && optionText !== '-' && optionText.length > 1) {
|
||
|
|
await options.nth(i).click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Branch selected:', optionText);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Role: FINANCE', async () => {
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const roleSelect = page.locator('.ant-select:has(.anticon-solution)').first();
|
||
|
|
const roleSelectSelector = roleSelect.locator('.ant-select-selector');
|
||
|
|
await roleSelectSelector.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Role dropdown clicked');
|
||
|
|
|
||
|
|
const financeOption = page.locator('.ant-select-item-option-content', { hasText: 'Finance' }).first();
|
||
|
|
await financeOption.waitFor({ state: 'visible', timeout: 5000 });
|
||
|
|
await financeOption.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ FINANCE role selected');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Submit the form', async () => {
|
||
|
|
const submitBtn = page.locator('.ant-modal button:has-text("Add User")').last();
|
||
|
|
const isVisible = await submitBtn.isVisible().catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log('✓ Form submitted');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify new finance user appears in table', async () => {
|
||
|
|
// Wait for modal to fully close before interacting with the page
|
||
|
|
await page.waitForSelector('.ant-modal', { state: 'hidden', timeout: 15000 }).catch(() => {});
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
// Click the Finance button - find all buttons with "Finance" text and filter out the one that also has "Cashier"
|
||
|
|
const allButtons = page.locator('button').filter({ hasText: 'Finance' });
|
||
|
|
const count = await allButtons.count();
|
||
|
|
console.log('Buttons with "Finance" text:', count);
|
||
|
|
|
||
|
|
let financeBtn = null;
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const text = await allButtons.nth(i).textContent();
|
||
|
|
console.log(`Button ${i} text: "${text}"`);
|
||
|
|
if (text?.includes('Finance') && !text?.includes('Cashier')) {
|
||
|
|
financeBtn = allButtons.nth(i);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!financeBtn) {
|
||
|
|
// Fallback: just use the last button with Finance text (should be Finance button)
|
||
|
|
financeBtn = allButtons.last();
|
||
|
|
}
|
||
|
|
|
||
|
|
const visible = await financeBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
||
|
|
console.log('Finance button visible:', visible);
|
||
|
|
if (visible) {
|
||
|
|
// Use force: true to bypass modal overlay that's intercepting clicks
|
||
|
|
await financeBtn.click({ force: true });
|
||
|
|
console.log('✓ Finance button clicked');
|
||
|
|
|
||
|
|
// Wait for the table to reload with Finance users
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
|
||
|
|
// Check if Finance filter is active by looking for active button state
|
||
|
|
const financeBtnAfterClick = page.locator('button').filter({ hasText: 'Finance' }).first();
|
||
|
|
const btnClass = await financeBtnAfterClick.getAttribute('class');
|
||
|
|
console.log('Finance button class after click:', btnClass);
|
||
|
|
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 }).catch(() => {});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search across all pages for the new user
|
||
|
|
let userFound = false;
|
||
|
|
for (let p = 0; p < 10 && !userFound; p++) {
|
||
|
|
const allText = await page.locator('table').first().textContent().catch(() => '');
|
||
|
|
if (p === 0) console.log('Table content (first 400 chars):', allText?.substring(0, 400));
|
||
|
|
if (allText?.includes(fullName)) {
|
||
|
|
userFound = true;
|
||
|
|
console.log('✓ User found in Finance table');
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const hasNext = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (!hasNext) break;
|
||
|
|
|
||
|
|
// Use force: true to bypass any modal overlays
|
||
|
|
await nextBtn.click({ force: true });
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Looking for: "${fullName}"`);
|
||
|
|
console.log('User found:', userFound);
|
||
|
|
expect(userFound).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Add finance user test completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should add a new terminal manager user', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Add a new terminal manager user by filling the form, selecting branch and Terminal Manager role, then verify in table');
|
||
|
|
|
||
|
|
const timestamp = Date.now();
|
||
|
|
const fullName = `Test Terminal Manager ${timestamp}`;
|
||
|
|
const email = `tmanager${timestamp}@test.com`;
|
||
|
|
const password = 'Mav@1234';
|
||
|
|
|
||
|
|
await test.step('Click Add User button', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 }).catch(() => {});
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
const addUserBtn = page.locator('button').filter({ hasText: 'Add User' }).first();
|
||
|
|
await addUserBtn.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await addUserBtn.click({ force: true });
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Add User modal opened');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Full Name', async () => {
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
const fullNameInput = page.locator('#fullName').first();
|
||
|
|
await fullNameInput.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await fullNameInput.fill(fullName);
|
||
|
|
console.log('✓ Full name filled:', fullName);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Username', async () => {
|
||
|
|
const usernameInput = page.locator('#username').first();
|
||
|
|
await usernameInput.fill(email);
|
||
|
|
console.log('✓ Username (email) filled:', email);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Password', async () => {
|
||
|
|
const passwordInput = page.locator('#password').first();
|
||
|
|
await passwordInput.fill(password);
|
||
|
|
console.log('✓ Password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Confirm Password', async () => {
|
||
|
|
const confirmInput = page.locator('#confirmPassword').first();
|
||
|
|
await confirmInput.fill(password);
|
||
|
|
console.log('✓ Confirm password filled');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Role: TERMINAL MANAGER', async () => {
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const roleSelect = page.locator('.ant-select:has(.anticon-solution)').first();
|
||
|
|
const roleSelectSelector = roleSelect.locator('.ant-select-selector');
|
||
|
|
await roleSelectSelector.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Role dropdown clicked');
|
||
|
|
|
||
|
|
const terminalManagerOption = page.locator('.ant-select-item-option-content', { hasText: 'Terminal Manager' }).first();
|
||
|
|
await terminalManagerOption.waitFor({ state: 'visible', timeout: 5000 });
|
||
|
|
await terminalManagerOption.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ TERMINAL MANAGER role selected');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Branch from dropdown', async () => {
|
||
|
|
const branchSelect = page.locator('.ant-select').filter({ hasText: /Select a branch/i }).first();
|
||
|
|
const isVisible = await branchSelect.isVisible().catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await branchSelect.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Branch dropdown opened');
|
||
|
|
|
||
|
|
const options = page.locator('.ant-select-item-option');
|
||
|
|
const optionCount = await options.count();
|
||
|
|
for (let i = 0; i < optionCount; i++) {
|
||
|
|
const optionText = (await options.nth(i).textContent())?.trim();
|
||
|
|
if (optionText && optionText !== '-' && optionText.length > 1) {
|
||
|
|
await options.nth(i).click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ Branch selected:', optionText);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log('Branch dropdown not shown for this role');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Submit the form', async () => {
|
||
|
|
const submitBtn = page.locator('.ant-modal button:has-text("Add User")').last();
|
||
|
|
const isVisible = await submitBtn.isVisible().catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log('✓ Form submitted');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify new terminal manager user appears in table', async () => {
|
||
|
|
await page.waitForSelector('.ant-modal', { state: 'hidden', timeout: 15000 }).catch(() => {});
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
// Switch to the Terminal Managers tab (distinct from Cashiers/Finance)
|
||
|
|
const allButtons = page.locator('button').filter({ hasText: 'Terminal Managers' });
|
||
|
|
const count = await allButtons.count();
|
||
|
|
console.log('Buttons with "Terminal Managers" text:', count);
|
||
|
|
expect(count, 'A "Terminal Managers" tab button should exist').toBeGreaterThan(0);
|
||
|
|
|
||
|
|
const terminalManagersBtn = allButtons.first();
|
||
|
|
await terminalManagersBtn.click({ force: true });
|
||
|
|
console.log('✓ Terminal Managers tab clicked');
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 }).catch(() => {});
|
||
|
|
|
||
|
|
// Search across all pages for the new user
|
||
|
|
let userFound = false;
|
||
|
|
for (let p = 0; p < 10 && !userFound; p++) {
|
||
|
|
const allText = await page.locator('table').first().textContent().catch(() => '');
|
||
|
|
if (p === 0) console.log('Table content (first 400 chars):', allText?.substring(0, 400));
|
||
|
|
if (allText?.includes(fullName)) {
|
||
|
|
userFound = true;
|
||
|
|
console.log('✓ User found in Terminal Managers table');
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const hasNext = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (!hasNext) break;
|
||
|
|
|
||
|
|
await nextBtn.click({ force: true });
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Looking for: "${fullName}"`);
|
||
|
|
console.log('User found:', userFound);
|
||
|
|
expect(userFound, 'Newly created terminal manager should appear in the Terminal Managers table').toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Add terminal manager user test completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should switch to Branches tab', async ({ page }) => {
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('normal');
|
||
|
|
description('Verify switching between Users and Branches tabs works correctly and Branches table displays data');
|
||
|
|
|
||
|
|
await test.step('Click Branches tab', async () => {
|
||
|
|
const branchesTab = page.locator('[role="tab"]:has-text("Branches")').first();
|
||
|
|
await branchesTab.waitFor({ state: 'visible', timeout: 20000 });
|
||
|
|
await branchesTab.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Clicked Branches tab');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify Branches table is displayed', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 }).catch(() => {});
|
||
|
|
const headers = await page.locator('table thead th').allTextContents();
|
||
|
|
console.log('Branches table headers:', headers.join(' | '));
|
||
|
|
|
||
|
|
const hasId = headers.some(h => h.toLowerCase().includes('id'));
|
||
|
|
const hasBranchName = headers.some(h => h.toLowerCase().includes('branch name'));
|
||
|
|
const hasAddress = headers.some(h => h.toLowerCase().includes('address'));
|
||
|
|
console.log(`Has ID: ${hasId}, Has Branch name: ${hasBranchName}, Has Address: ${hasAddress}`);
|
||
|
|
expect(hasId || hasBranchName).toBeTruthy();
|
||
|
|
|
||
|
|
const rows = await page.locator('table tbody tr').count();
|
||
|
|
console.log('Branch rows:', rows);
|
||
|
|
expect(rows).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify Add Branch button is visible', async () => {
|
||
|
|
const addBranchBtn = page.locator('button:has-text("Add Branch")').first();
|
||
|
|
const isVisible = await addBranchBtn.isVisible().catch(() => false);
|
||
|
|
console.log('Add Branch button visible:', isVisible);
|
||
|
|
expect(isVisible).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Switch back to Users tab', async () => {
|
||
|
|
const usersTab = page.locator('[role="tab"]:has-text("Users")').first();
|
||
|
|
await usersTab.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Switched back to Users tab');
|
||
|
|
|
||
|
|
const rows = await page.locator('table tbody tr').count();
|
||
|
|
console.log('Users rows after switching back:', rows);
|
||
|
|
expect(rows).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Tab switching verified');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should add a new branch', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Add a new branch by filling branch name and address, then verify it appears in the Branches table');
|
||
|
|
|
||
|
|
const timestamp = Date.now();
|
||
|
|
const branchName = `Test Branch ${timestamp}`;
|
||
|
|
const branchAddress = `Test Address ${timestamp}`;
|
||
|
|
|
||
|
|
await test.step('Switch to Branches tab', async () => {
|
||
|
|
const branchesTab = page.locator('[role="tab"]:has-text("Branches")').first();
|
||
|
|
await branchesTab.waitFor({ state: 'visible', timeout: 20000 });
|
||
|
|
await branchesTab.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Switched to Branches tab');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Click Add Branch button', async () => {
|
||
|
|
const addBranchBtn = page.locator('button:has-text("Add Branch")').first();
|
||
|
|
await addBranchBtn.waitFor({ state: 'visible', timeout: 10000 });
|
||
|
|
await addBranchBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
console.log('✓ Add Branch modal opened');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Branch Name', async () => {
|
||
|
|
// The modal has labels: "Branch", "Branch address", "Region", "City"
|
||
|
|
const nameInput = page.locator('#branchName').first();
|
||
|
|
const visible = await nameInput.isVisible({ timeout: 5000 }).catch(() => false);
|
||
|
|
if (visible) {
|
||
|
|
await nameInput.fill(branchName);
|
||
|
|
} else {
|
||
|
|
// Try by placeholder as fallback
|
||
|
|
const byPlaceholder = page.locator('#branchName');
|
||
|
|
await byPlaceholder.fill(branchName);
|
||
|
|
}
|
||
|
|
console.log('✓ Branch name filled:', branchName);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Fill Branch Address', async () => {
|
||
|
|
const addressInput = page.locator('#username');
|
||
|
|
await addressInput.fill(branchAddress);
|
||
|
|
console.log('✓ Branch address filled:', branchAddress);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select Region', async () => {
|
||
|
|
// Wait a moment for all fields to render
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
// Click on the region select div to open dropdown
|
||
|
|
const regionSelect = page.locator('.ant-select').filter({ hasText: 'Select a region' }).first();
|
||
|
|
await regionSelect.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Region dropdown opened');
|
||
|
|
|
||
|
|
// Type "Asir" to search/filter the region
|
||
|
|
await page.keyboard.type('Asir');
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Typed "Asir"');
|
||
|
|
|
||
|
|
// Click on the visible option
|
||
|
|
const regionOption = page.locator('.ant-select-item-option-content', { hasText: 'Asir' }).first();
|
||
|
|
await regionOption.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Region "Asir" selected');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Select City', async () => {
|
||
|
|
// Wait for region selection to complete
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
// Click on the city select div to open dropdown
|
||
|
|
const citySelect = page.locator('.ant-select').filter({ hasText: 'Select a city' }).first();
|
||
|
|
await citySelect.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ City dropdown opened');
|
||
|
|
|
||
|
|
// Type first letter to select first city option or type a specific city name
|
||
|
|
// Get first city option that appears
|
||
|
|
const firstCityOption = page.locator('.ant-select-item-option-content').first();
|
||
|
|
const cityName = await firstCityOption.textContent();
|
||
|
|
console.log('Selecting city:', cityName?.trim());
|
||
|
|
|
||
|
|
// Type the city name or just press Enter for first option
|
||
|
|
await page.keyboard.press('Enter');
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ City selected:', cityName?.trim());
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Add District', async () => {
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
// Click "Add new district" button to reveal the input
|
||
|
|
const addNewDistrictBtn = page.locator('button:has-text("Add new district")').first();
|
||
|
|
const btnVisible = await addNewDistrictBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
||
|
|
if (btnVisible) {
|
||
|
|
await addNewDistrictBtn.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ Clicked Add new district');
|
||
|
|
|
||
|
|
// Fill the district name in the text input that appears
|
||
|
|
const districtInput = page.locator('input.ant-input[type="text"]:not([role="combobox"])').last();
|
||
|
|
await districtInput.waitFor({ state: 'visible', timeout: 5000 });
|
||
|
|
const districtName = `District ${Date.now()}`;
|
||
|
|
await districtInput.fill(districtName);
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ District name filled:', districtName);
|
||
|
|
|
||
|
|
// Click "Add" button (primary button with plus icon)
|
||
|
|
const addBtn = page.locator('button.ant-btn-primary:has-text("Add")').first();
|
||
|
|
await addBtn.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
console.log('✓ District added');
|
||
|
|
} else {
|
||
|
|
// Fallback: just select from existing district dropdown (rc_select_7)
|
||
|
|
const districtSelect = page.locator('#rc_select_7');
|
||
|
|
await districtSelect.click();
|
||
|
|
await page.waitForTimeout(800);
|
||
|
|
|
||
|
|
const option = page.locator('[role="option"]').first();
|
||
|
|
await option.waitFor({ state: 'visible', timeout: 5000 });
|
||
|
|
const optText = await option.textContent();
|
||
|
|
await option.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
console.log('✓ District selected:', optText?.trim());
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Submit the form', async () => {
|
||
|
|
// Click the "Add Branch" button with plus-square icon
|
||
|
|
const submitBtn = page.locator('button:has(.anticon-plus-square):has-text("Add Branch")').first();
|
||
|
|
const isVisible = await submitBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
||
|
|
if (isVisible) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log('✓ Form submitted');
|
||
|
|
} else {
|
||
|
|
// Fallback
|
||
|
|
const fallback = page.locator('button:has-text("Add branch")').last();
|
||
|
|
await fallback.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log('✓ Form submitted (fallback)');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify new branch appears in table', async () => {
|
||
|
|
// Wait for modal to close
|
||
|
|
await page.waitForTimeout(5000);
|
||
|
|
|
||
|
|
// Check the page body for the branch name
|
||
|
|
const bodyText = await page.locator('body').textContent().catch(() => '');
|
||
|
|
console.log(`Looking for: "${branchName}"`);
|
||
|
|
const branchFound = bodyText?.includes(branchName) || false;
|
||
|
|
console.log('Branch found on page:', branchFound);
|
||
|
|
expect(branchFound).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Add branch test completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should download report from Users tab and verify content', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Download CSV report from Users tab and cross-check content against the table on the portal');
|
||
|
|
|
||
|
|
const tableRows: string[][] = [];
|
||
|
|
let tableHeaders: string[] = [];
|
||
|
|
|
||
|
|
await test.step('Collect ALL table data from portal (all pages)', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 });
|
||
|
|
tableHeaders = await page.locator('table thead th').allTextContents();
|
||
|
|
console.log('Table headers:', tableHeaders.join(' | '));
|
||
|
|
|
||
|
|
// Use only the first visible table
|
||
|
|
const table = page.locator('table').first();
|
||
|
|
|
||
|
|
// This table only grows (every "add user" test run creates a real,
|
||
|
|
// permanent record), so an unbounded loop eventually times out the
|
||
|
|
// whole test - cap it like the refunds export test does.
|
||
|
|
const maxPages = 20;
|
||
|
|
let pageNum = 1;
|
||
|
|
while (true) {
|
||
|
|
const rows = table.locator('tbody tr:not(.ant-table-placeholder)');
|
||
|
|
const rowCount = await rows.count();
|
||
|
|
|
||
|
|
for (let i = 0; i < rowCount; i++) {
|
||
|
|
const cells = await rows.nth(i).locator('td').allTextContents();
|
||
|
|
const rowData = cells.map(c => c.trim());
|
||
|
|
// Skip rows where all cells are empty
|
||
|
|
const hasContent = rowData.some(c => c.length > 0 && !c.match(/^No data$/i));
|
||
|
|
if (hasContent) {
|
||
|
|
tableRows.push(rowData);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pageNum >= maxPages) {
|
||
|
|
console.log(`⚠ Reached safety cap of ${maxPages} pages - stopping early, table may have more rows`);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const hasNext = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (hasNext) {
|
||
|
|
await nextBtn.click();
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
pageNum++;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Total rows collected across ${pageNum} pages: ${tableRows.length}`);
|
||
|
|
console.log('First row:', tableRows[0]?.join(' | '));
|
||
|
|
expect(tableRows.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
let csvContent = '';
|
||
|
|
|
||
|
|
await test.step('Download CSV report', async () => {
|
||
|
|
const downloadBtn = page.locator('button:has-text("Download Report")').first();
|
||
|
|
await downloadBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
const csvOption = page.locator('text=CSV').first();
|
||
|
|
await csvOption.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
|
||
|
|
const dlBtn = page.locator('.ant-modal-wrap button span:text-is("Download")').first();
|
||
|
|
await dlBtn.click();
|
||
|
|
|
||
|
|
const download = await downloadPromise;
|
||
|
|
expect(download).toBeTruthy();
|
||
|
|
console.log('✓ Downloaded:', download.suggestedFilename());
|
||
|
|
|
||
|
|
const filePath = await download.path();
|
||
|
|
if (filePath) {
|
||
|
|
csvContent = fs.readFileSync(filePath, 'utf-8');
|
||
|
|
console.log('CSV lines:', csvContent.split('\n').length);
|
||
|
|
console.log('CSV first 300 chars:', csvContent.substring(0, 300));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Cross-check table data against CSV', async () => {
|
||
|
|
expect(csvContent).toBeTruthy();
|
||
|
|
const csvLines = csvContent.split('\n').filter((l: string) => l.trim());
|
||
|
|
const csvDataLines = csvLines.length - 1; // minus header
|
||
|
|
console.log('CSV header:', csvLines[0]);
|
||
|
|
console.log(`Portal total rows: ${tableRows.length}`);
|
||
|
|
console.log(`CSV data rows: ${csvDataLines}`);
|
||
|
|
|
||
|
|
let matchCount = 0;
|
||
|
|
const unmatched: string[] = [];
|
||
|
|
for (const row of tableRows) {
|
||
|
|
const keyValue = row[1] || row[0];
|
||
|
|
if (keyValue && csvContent.includes(keyValue)) {
|
||
|
|
matchCount++;
|
||
|
|
} else {
|
||
|
|
unmatched.push(keyValue);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log(`Matched ${matchCount}/${tableRows.length} portal rows found in CSV`);
|
||
|
|
if (unmatched.length > 0) console.log('Unmatched:', unmatched.join(', '));
|
||
|
|
expect(matchCount).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Users download + cross-check completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should download finance report and verify content', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Download Finance CSV report and cross-check content against the finance table on the portal');
|
||
|
|
|
||
|
|
const tableRows: string[][] = [];
|
||
|
|
|
||
|
|
await test.step('Switch to Finance view and collect ALL data', async () => {
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 20000 }).catch(() => {});
|
||
|
|
|
||
|
|
const financeBtn = page.locator('button span:text-is("Finance")').first();
|
||
|
|
await financeBtn.waitFor({ state: 'visible', timeout: 20000 });
|
||
|
|
await financeBtn.click({ force: true });
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
|
||
|
|
await page.waitForSelector('table tbody tr', { timeout: 10000 });
|
||
|
|
const headers = await page.locator('table thead th').allTextContents();
|
||
|
|
console.log('Finance table headers:', headers.join(' | '));
|
||
|
|
|
||
|
|
const table = page.locator('table').first();
|
||
|
|
|
||
|
|
let pageNum = 1;
|
||
|
|
while (true) {
|
||
|
|
const rows = table.locator('tbody tr:not(.ant-table-placeholder)');
|
||
|
|
const rowCount = await rows.count();
|
||
|
|
|
||
|
|
for (let i = 0; i < rowCount; i++) {
|
||
|
|
const cells = await rows.nth(i).locator('td').allTextContents();
|
||
|
|
const rowData = cells.map(c => c.trim());
|
||
|
|
const hasContent = rowData.some(c => c.length > 0 && !c.match(/^No data$/i));
|
||
|
|
if (hasContent) {
|
||
|
|
tableRows.push(rowData);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const hasNext = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (hasNext) {
|
||
|
|
await nextBtn.click();
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
pageNum++;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Total finance rows collected across ${pageNum} pages: ${tableRows.length}`);
|
||
|
|
expect(tableRows.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
let csvContent = '';
|
||
|
|
|
||
|
|
await test.step('Download Finance CSV report', async () => {
|
||
|
|
const cashiersBtn = page.locator('button span:text-is("Cashiers")').first();
|
||
|
|
const cashiersVisible = await cashiersBtn.isVisible().catch(() => false);
|
||
|
|
if (cashiersVisible) {
|
||
|
|
await cashiersBtn.click();
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
const downloadBtn = page.locator('button:has-text("Download Report")').first();
|
||
|
|
await downloadBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
const financeToggle = page.locator('.ant-modal-wrap button span:text-is("Finance")').first();
|
||
|
|
await financeToggle.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const csvOption = page.locator('text=CSV').first();
|
||
|
|
await csvOption.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
|
||
|
|
const dlBtn = page.locator('.ant-modal-wrap button span:text-is("Download")').first();
|
||
|
|
await dlBtn.click();
|
||
|
|
|
||
|
|
const download = await downloadPromise;
|
||
|
|
expect(download).toBeTruthy();
|
||
|
|
console.log('✓ Downloaded:', download.suggestedFilename());
|
||
|
|
|
||
|
|
const filePath = await download.path();
|
||
|
|
if (filePath) {
|
||
|
|
csvContent = fs.readFileSync(filePath, 'utf-8');
|
||
|
|
console.log('CSV lines:', csvContent.split('\n').length);
|
||
|
|
console.log('CSV first 300 chars:', csvContent.substring(0, 300));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Cross-check finance data against CSV', async () => {
|
||
|
|
expect(csvContent).toBeTruthy();
|
||
|
|
const csvLines = csvContent.split('\n').filter((l: string) => l.trim());
|
||
|
|
const csvDataLines = csvLines.length - 1;
|
||
|
|
console.log('CSV header:', csvLines[0]);
|
||
|
|
console.log(`Portal total rows: ${tableRows.length}`);
|
||
|
|
console.log(`CSV data rows: ${csvDataLines}`);
|
||
|
|
|
||
|
|
let matchCount = 0;
|
||
|
|
const unmatched: string[] = [];
|
||
|
|
for (const row of tableRows) {
|
||
|
|
const keyValue = row[1] || row[0];
|
||
|
|
if (keyValue && csvContent.includes(keyValue)) {
|
||
|
|
matchCount++;
|
||
|
|
} else {
|
||
|
|
unmatched.push(keyValue);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log(`Matched ${matchCount}/${tableRows.length} finance rows found in CSV`);
|
||
|
|
if (unmatched.length > 0) console.log('Unmatched:', unmatched.join(', '));
|
||
|
|
expect(matchCount).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Finance download + cross-check completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should download report from Branches tab and verify content', async ({ page }) => {
|
||
|
|
test.setTimeout(120000);
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('critical');
|
||
|
|
description('Download CSV report from Branches tab and cross-check content against the table on the portal');
|
||
|
|
|
||
|
|
const tableRows: string[][] = [];
|
||
|
|
|
||
|
|
await test.step('Switch to Branches and collect ALL data', async () => {
|
||
|
|
const branchesTab = page.locator('[role="tab"]:has-text("Branches")').first();
|
||
|
|
await branchesTab.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
|
||
|
|
// Get the visible table (Branches tab table, not the hidden Users table)
|
||
|
|
const tables = page.locator('table');
|
||
|
|
const tableCount = await tables.count();
|
||
|
|
console.log(`Tables in DOM: ${tableCount}`);
|
||
|
|
|
||
|
|
// Find the table that has branch-related headers
|
||
|
|
let branchTable = tables.first();
|
||
|
|
for (let t = 0; t < tableCount; t++) {
|
||
|
|
const hdrs = await tables.nth(t).locator('thead th').allTextContents();
|
||
|
|
const hdrText = hdrs.join(' ');
|
||
|
|
if (hdrText.includes('Branch') && hdrText.includes('Address')) {
|
||
|
|
branchTable = tables.nth(t);
|
||
|
|
console.log(`Using table ${t} with headers: ${hdrs.join(' | ')}`);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let pageNum = 1;
|
||
|
|
while (true) {
|
||
|
|
const rows = branchTable.locator('tbody tr:not(.ant-table-placeholder)');
|
||
|
|
const rowCount = await rows.count();
|
||
|
|
|
||
|
|
for (let i = 0; i < rowCount; i++) {
|
||
|
|
const cells = await rows.nth(i).locator('td').allTextContents();
|
||
|
|
const rowData = cells.map(c => c.trim());
|
||
|
|
const hasContent = rowData.some(c => c.length > 0 && !c.match(/^No data$/i));
|
||
|
|
if (hasContent) {
|
||
|
|
tableRows.push(rowData);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextBtn = page.locator('.ant-pagination-next:not(.ant-pagination-disabled)').first();
|
||
|
|
const hasNext = await nextBtn.isVisible().catch(() => false);
|
||
|
|
if (hasNext) {
|
||
|
|
await nextBtn.click();
|
||
|
|
await page.waitForTimeout(1500);
|
||
|
|
pageNum++;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Total branch rows collected across ${pageNum} pages: ${tableRows.length}`);
|
||
|
|
if (tableRows.length > 0) console.log('First row:', tableRows[0]?.join(' | '));
|
||
|
|
expect(tableRows.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
let csvContent = '';
|
||
|
|
|
||
|
|
await test.step('Download Branches CSV report', async () => {
|
||
|
|
const downloadBtn = page.locator('button:has-text("Download Report")').first();
|
||
|
|
await downloadBtn.click();
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
const csvOption = page.locator('text=CSV').first();
|
||
|
|
await csvOption.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
|
||
|
|
const dlBtn = page.locator('.ant-modal-wrap button span:text-is("Download")').first();
|
||
|
|
await dlBtn.click();
|
||
|
|
|
||
|
|
const download = await downloadPromise;
|
||
|
|
expect(download).toBeTruthy();
|
||
|
|
console.log('✓ Downloaded:', download.suggestedFilename());
|
||
|
|
|
||
|
|
const filePath = await download.path();
|
||
|
|
if (filePath) {
|
||
|
|
csvContent = fs.readFileSync(filePath, 'utf-8');
|
||
|
|
console.log('CSV lines:', csvContent.split('\n').length);
|
||
|
|
console.log('CSV first 300 chars:', csvContent.substring(0, 300));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Cross-check branches data against CSV', async () => {
|
||
|
|
expect(csvContent).toBeTruthy();
|
||
|
|
const csvLines = csvContent.split('\n').filter((l: string) => l.trim());
|
||
|
|
const csvDataLines = csvLines.length - 1;
|
||
|
|
console.log('CSV header:', csvLines[0]);
|
||
|
|
console.log(`Portal total rows: ${tableRows.length}`);
|
||
|
|
console.log(`CSV data rows: ${csvDataLines}`);
|
||
|
|
|
||
|
|
let matchCount = 0;
|
||
|
|
const unmatched: string[] = [];
|
||
|
|
for (const row of tableRows) {
|
||
|
|
const keyValue = row[1] || row[0];
|
||
|
|
if (keyValue && csvContent.includes(keyValue)) {
|
||
|
|
matchCount++;
|
||
|
|
} else {
|
||
|
|
unmatched.push(keyValue);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log(`Matched ${matchCount}/${tableRows.length} branch rows found in CSV`);
|
||
|
|
if (unmatched.length > 0) console.log('Unmatched:', unmatched.join(', '));
|
||
|
|
expect(matchCount).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Branches download + cross-check completed');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should paginate through cashier list', async ({ page }) => {
|
||
|
|
feature('Management Operations');
|
||
|
|
tag('functional');
|
||
|
|
severity('normal');
|
||
|
|
description('Test pagination controls navigate to page 2 and load a genuinely different set of cashiers');
|
||
|
|
|
||
|
|
const table = page.locator('table').first();
|
||
|
|
await table.locator('tbody tr:not([aria-hidden="true"])').first().waitFor({ state: 'visible', timeout: 25000 });
|
||
|
|
const headers = await table.locator('thead th').allTextContents();
|
||
|
|
const idIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'cashier id');
|
||
|
|
expect(idIdx, 'A Cashier ID column should exist').toBeGreaterThanOrEqual(0);
|
||
|
|
|
||
|
|
let firstPageId = '';
|
||
|
|
|
||
|
|
await test.step('Capture first row ID on page 1', async () => {
|
||
|
|
const cells = await table.locator('tbody tr:not([aria-hidden="true"])').first().locator('td').allTextContents();
|
||
|
|
firstPageId = (cells[idIdx] || '').trim();
|
||
|
|
expect(firstPageId, 'Page 1 first row should have a valid ID').toMatch(/^\d+$/);
|
||
|
|
console.log('First page first row ID:', firstPageId);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Navigate to page 2', async () => {
|
||
|
|
// Ant Design renders page numbers as <li class="ant-pagination-item-N">,
|
||
|
|
// not <button> elements - a `button:has-text("2")` selector matches
|
||
|
|
// nothing here even though the pagination (606 pages in this dataset)
|
||
|
|
// is very much real.
|
||
|
|
const page2Item = page.locator('.ant-pagination-item').filter({ hasText: /^2$/ }).first();
|
||
|
|
await expect(page2Item, 'A page 2 pagination item should exist').toBeVisible({ timeout: 10000 });
|
||
|
|
await page2Item.click();
|
||
|
|
console.log('✓ Navigated to page 2');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test.step('Verify page 2 loaded a different first row', async () => {
|
||
|
|
let secondPageId = '';
|
||
|
|
for (let attempt = 0; attempt < 20; attempt++) {
|
||
|
|
const cells = await table.locator('tbody tr:not([aria-hidden="true"])').first().locator('td').allTextContents().catch(() => [] as string[]);
|
||
|
|
secondPageId = (cells[idIdx] || '').trim();
|
||
|
|
if (secondPageId && secondPageId !== firstPageId) break;
|
||
|
|
await page.waitForTimeout(400);
|
||
|
|
}
|
||
|
|
console.log('Page 2 first row ID:', secondPageId);
|
||
|
|
|
||
|
|
expect(secondPageId, 'Page 2 first row should have a valid ID').toMatch(/^\d+$/);
|
||
|
|
expect(secondPageId, 'Page 2 should show a different first row than page 1').not.toBe(firstPageId);
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('✓ Pagination test completed');
|
||
|
|
});
|
||
|
|
});
|