Files
Frontend_automation/tests/management.spec.ts

1207 lines
49 KiB
TypeScript
Raw Normal View History

Extract PassDashboard Playwright e2e suite into standalone repo Copies the Playwright end-to-end suite out of the PassDashboard app repo so it can be run and deployed independently. Nothing was removed from the app repo - this is a copy. Carried over verbatim: tests/ (10 spec files, 4 helper modules, README) plus playwright.config.ts and .env.test.example. The suite drives the deployed app over HTTP and imports nothing from src/, which is what makes it standalone. New for this repo: - package.json with only the deps the suite actually uses (@playwright/test, allure-playwright, xlsx, typescript, @types/node) and the e2e scripts. - tsconfig.json covering tests/ - the app repo's tsconfig.app.json only included src/, so these files were never typechecked before. - .gitea/workflows/e2e-tests.yml for Gitea Actions. The app repo derived ENV from github.ref_name; that doesn't apply here since this repo has no per-environment branches, so the target is an explicit workflow_dispatch input and scheduled/push runs default to develop. Reports are uploaded as artifacts rather than deleted. - .gitignore and README covering local setup and the required CI secrets. The Jest unit tests under src/__tests__/ were deliberately left in the app repo: they import application source directly (AuthContext, ProtectedRoute, cryptoUtils, api) and cannot run without it. Verified: npx tsc --noEmit is clean and playwright collects all 43 tests across all 10 spec files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:50:50 +03:00
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');
});
});