183 lines
8.3 KiB
TypeScript
183 lines
8.3 KiB
TypeScript
|
|
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<string, { visible: string[]; hidden: string[]; landingPath: string }> = {
|
||
|
|
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<void> {
|
||
|
|
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<keyof typeof EXPECTED_MENUS>) {
|
||
|
|
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`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|