import { test, expect } from '@playwright/test'; import { login, loginAs } from './helpers/auth'; import { epic, feature, severity, description, tag } from './helpers/allure'; import { navigateToManagement } from './helpers/navigation'; // Confirmed live by actually logging in as each role: the nav menu is // restricted per role, and Terminal Manager lands on /terminals (not // /dashboard) since Dashboard isn't in its menu at all. const EXPECTED_MENUS: Record = { CASHIER: { visible: ['Dashboard', 'Transactions', 'Terminals', 'Refunds', 'Settings'], hidden: ['Discounts & fees', 'Reconciliation', 'Settlement', 'Management'], landingPath: '/dashboard', }, FINANCE: { visible: ['Dashboard', 'Transactions', 'Discounts & fees', 'Reconciliation', 'Refunds', 'Settlement', 'Settings'], hidden: ['Terminals', 'Management'], landingPath: '/dashboard', }, TERMINAL_MANAGER: { visible: ['Terminals', 'Settings'], hidden: ['Dashboard', 'Transactions', 'Discounts & fees', 'Reconciliation', 'Refunds', 'Settlement', 'Management'], landingPath: '/terminals', }, }; async function createRoleUser( page: import('@playwright/test').Page, role: keyof typeof EXPECTED_MENUS, fullName: string, identifier: string, password: string ): Promise { await navigateToManagement(page); await page.waitForTimeout(2000); const addUserBtn = page.locator('button').filter({ hasText: 'Add User' }).first(); await addUserBtn.click({ force: true }); await page.waitForTimeout(1500); await page.locator('#fullName').first().fill(fullName); await page.locator('#username').first().fill(identifier); await page.locator('#password').first().fill(password); await page.locator('#confirmPassword').first().fill(password); if (role !== 'CASHIER') { const roleSelect = page.locator('.ant-select:has(.anticon-solution)').first(); await roleSelect.locator('.ant-select-selector').click(); await page.waitForTimeout(800); const label = role === 'FINANCE' ? 'Finance' : 'Terminal Manager'; const option = page.locator('.ant-select-item-option-content', { hasText: label }).first(); await option.waitFor({ state: 'visible', timeout: 5000 }); await option.click(); // Wait for the dropdown to fully close - a leftover open dropdown here // caused the *next* select's option locator to resolve to a stale, // invisible element from this role dropdown instead. await page.waitForSelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden)', { state: 'hidden', timeout: 5000 }).catch(() => {}); await page.waitForTimeout(800); } const branchSelect = page.locator('.ant-select').filter({ hasText: /Select a branch/i }).first(); if (await branchSelect.isVisible({ timeout: 3000 }).catch(() => false)) { await branchSelect.click(); await page.waitForTimeout(800); const options = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option'); const count = await options.count(); for (let i = 0; i < count; i++) { const t = (await options.nth(i).textContent())?.trim(); if (t && t !== '-' && t.length > 1) { await options.nth(i).click(); break; } } await page.waitForTimeout(500); } if (role === 'CASHIER') { // Cashiers additionally require an unassociated Terminal Id. const terminalSelect = page.locator('.ant-select:has(.anticon-mobile)').first(); await expect(terminalSelect, 'Terminal Id select should be visible for Cashier role').toBeVisible({ timeout: 5000 }); await terminalSelect.locator('.ant-select-selector').click(); await page.waitForTimeout(1200); const dropdownOptions = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option'); const count = await dropdownOptions.count(); let selected = false; 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); selected = true; break; } } expect(selected, 'Should be able to select an unassociated terminal for the new cashier').toBe(true); } const submitBtn = page.locator('.ant-modal button:has-text("Add User")').last(); await submitBtn.click(); await page.waitForTimeout(3000); const modalStillOpen = await page.locator('.ant-modal-content').first().isVisible().catch(() => false); expect(modalStillOpen, `${role} user creation should succeed (the modal should close)`).toBe(false); } test.describe('Role-Based Access Tests', () => { test.setTimeout(120000); for (const role of Object.keys(EXPECTED_MENUS) as Array) { test(`should show correct restricted navigation for ${role} role after login`, async ({ page }) => { epic('Role-Based Access Control'); feature('Role Permissions'); tag('functional'); severity('critical'); description(`Create a ${role} user as admin, then actually log in as that user and verify their navigation menu matches expected role permissions`); console.log(`Testing ${role} role-based access...`); const timestamp = Date.now(); const fullName = `RoleTest ${role} ${timestamp}`; const identifier = role === 'CASHIER' ? `roletest${role.toLowerCase()}${timestamp}` : `roletest${role.toLowerCase().replace('_', '')}${timestamp}@test.com`; const password = 'Mav@1234'; await test.step(`Log in as admin and create a new ${role} user`, async () => { await login(page); await createRoleUser(page, role, fullName, identifier, password); console.log(`✓ Created ${role} user: ${identifier}`); }); await test.step('Log out of the admin session', async () => { // Navigating straight to the login URL while still authenticated as // admin just auto-redirects back to /dashboard - the login form // never renders, so filling it later times out. Log out explicitly. const logoutLink = page.locator('text=Logout').first(); await logoutLink.waitFor({ state: 'visible', timeout: 10000 }); await logoutLink.click(); await page.waitForURL(/\/login/, { timeout: 10000 }).catch(() => {}); await page.waitForTimeout(1500); console.log('✓ Logged out of admin session, URL:', page.url()); }); await test.step(`Log in as the new ${role} user`, async () => { // A just-created account can take a moment to become authenticatable // (backend provisioning lag) - retry the login itself rather than // failing on the first attempt landing back on the login page. let landed = false; for (let attempt = 1; attempt <= 3; attempt++) { await loginAs(page, identifier, password); console.log(`✓ Logged in as ${role}, landed on: ${page.url()}`); landed = page.url().includes(EXPECTED_MENUS[role].landingPath); if (landed) break; console.log(`Login attempt ${attempt} did not land on ${EXPECTED_MENUS[role].landingPath} - retrying...`); await page.waitForTimeout(3000); } expect(landed, `${role} should land on ${EXPECTED_MENUS[role].landingPath} after login`).toBe(true); }); await test.step('Verify the navigation menu matches expected role permissions', async () => { const { visible, hidden } = EXPECTED_MENUS[role]; for (const item of visible) { const menuItem = page.locator('button').filter({ hasText: new RegExp(`^${item}$`) }).first(); await expect(menuItem, `${role} should see "${item}" in the menu`).toBeVisible({ timeout: 10000 }); } console.log(`✓ ${role} sees all expected menu items: ${visible.join(', ')}`); for (const item of hidden) { const menuItem = page.locator('button').filter({ hasText: new RegExp(`^${item}$`) }); const count = await menuItem.count(); expect(count, `${role} should NOT see "${item}" in the menu`).toBe(0); } console.log(`✓ ${role} correctly does not see: ${hidden.join(', ')}`); }); console.log(`${role} role-based access test completed`); }); } });