763 lines
34 KiB
TypeScript
763 lines
34 KiB
TypeScript
|
|
import { test, expect } from '@playwright/test';
|
|||
|
|
import { login } from './helpers/auth';
|
|||
|
|
import { epic, feature, severity, description, tag } from './helpers/allure';
|
|||
|
|
import { navigateToTerminals, navigateToManagement } from './helpers/navigation';
|
|||
|
|
|
|||
|
|
test.describe('Terminal Tests', () => {
|
|||
|
|
test.setTimeout(90000);
|
|||
|
|
test.beforeEach(async ({ page }) => {
|
|||
|
|
epic('Terminal Management');
|
|||
|
|
await login(page);
|
|||
|
|
await navigateToTerminals(page);
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 2: View Terminal Details with Data Matching
|
|||
|
|
test('should view terminal details with all information', async ({ page }) => {
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('critical');
|
|||
|
|
description('View terminal details and verify Terminal Id matches and download button exists');
|
|||
|
|
|
|||
|
|
console.log('Testing terminal details view with data matching...');
|
|||
|
|
|
|||
|
|
// Variables to store data from table
|
|||
|
|
let terminalIdValue = '';
|
|||
|
|
|
|||
|
|
await test.step('Capture Terminal Id from first row', async () => {
|
|||
|
|
const firstRow = page.locator('table tbody tr:not([aria-hidden="true"])').first();
|
|||
|
|
await expect(firstRow).toBeVisible();
|
|||
|
|
|
|||
|
|
const cells = firstRow.locator('td');
|
|||
|
|
|
|||
|
|
// Extract Terminal Id (column index 5)
|
|||
|
|
terminalIdValue = (await cells.nth(5).textContent())?.trim() || '';
|
|||
|
|
|
|||
|
|
console.log('Captured Terminal Id from table:', terminalIdValue);
|
|||
|
|
expect(terminalIdValue).not.toBe('');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click Details button to view terminal details', async () => {
|
|||
|
|
const firstRow = page.locator('table tbody tr:not([aria-hidden="true"])').first();
|
|||
|
|
const detailsButton = firstRow.locator('button:has-text("Details")');
|
|||
|
|
await expect(detailsButton).toBeVisible();
|
|||
|
|
|
|||
|
|
await detailsButton.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
console.log('Details button clicked');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify terminal details panel appears', async () => {
|
|||
|
|
const detailsHeading = await page.locator('text=Terminal details').first().isVisible({ timeout: 5000 });
|
|||
|
|
console.log('Terminal details heading visible:', detailsHeading);
|
|||
|
|
expect(detailsHeading).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify Terminal Id matches in details panel', async () => {
|
|||
|
|
if (terminalIdValue && terminalIdValue.trim() !== '' && terminalIdValue !== '-') {
|
|||
|
|
const terminalIdLabel = await page.locator('text=Terminal Id :').isVisible({ timeout: 3000 });
|
|||
|
|
console.log('Terminal Id label found:', terminalIdLabel);
|
|||
|
|
expect(terminalIdLabel).toBeTruthy();
|
|||
|
|
|
|||
|
|
const terminalIdInDetails = await page.locator(`text=${terminalIdValue}`).first().isVisible({ timeout: 3000 });
|
|||
|
|
console.log('Terminal Id value found in details:', terminalIdInDetails);
|
|||
|
|
console.log('✓ Terminal Id matches:', terminalIdValue);
|
|||
|
|
expect(terminalIdInDetails).toBeTruthy();
|
|||
|
|
} else {
|
|||
|
|
console.log('Terminal Id is empty or dash, skipping verification');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify Download button exists in details panel', async () => {
|
|||
|
|
const downloadButton = page.locator('button:has-text("Download")').last();
|
|||
|
|
const isVisible = await downloadButton.isVisible({ timeout: 3000 });
|
|||
|
|
console.log('Download button in details panel visible:', isVisible);
|
|||
|
|
expect(isVisible).toBeTruthy();
|
|||
|
|
|
|||
|
|
// Verify it has the download icon
|
|||
|
|
const downloadIcon = downloadButton.locator('[data-icon="download"]');
|
|||
|
|
const iconVisible = await downloadIcon.isVisible().catch(() => false);
|
|||
|
|
console.log('Download icon visible:', iconVisible);
|
|||
|
|
|
|||
|
|
console.log('✓ Download button verified in terminal details');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('Terminal details verification completed - Terminal Id matches and download button exists!');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 3: Pagination Test
|
|||
|
|
test('should paginate through terminal list', async ({ page }) => {
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('normal');
|
|||
|
|
description('Test pagination controls navigate to page 2 and load a genuinely different set of terminals');
|
|||
|
|
|
|||
|
|
console.log('Testing terminal pagination...');
|
|||
|
|
|
|||
|
|
// Dismiss any open modals first
|
|||
|
|
await page.keyboard.press('Escape').catch(() => {});
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
|
|||
|
|
const table = page.locator('table').first();
|
|||
|
|
await table.locator('tbody tr:not([aria-hidden="true"])').first().waitFor({ state: 'visible', timeout: 15000 });
|
|||
|
|
const headers = await table.locator('thead th').allTextContents();
|
|||
|
|
// The first column is a blank icon/expander column - match "ID" exactly so
|
|||
|
|
// we don't accidentally grab that blank column or "Terminal Id"/"Cashier ID".
|
|||
|
|
const idIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'id');
|
|||
|
|
expect(idIdx, 'An ID column should exist').toBeGreaterThanOrEqual(0);
|
|||
|
|
|
|||
|
|
let firstPageId = '';
|
|||
|
|
|
|||
|
|
await test.step('Capture first terminal 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 terminal ID on page 1:', firstPageId);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click page 2 button', async () => {
|
|||
|
|
// Match the exact page-number text so we don't accidentally hit an
|
|||
|
|
// unrelated button that merely contains "2" somewhere in its label.
|
|||
|
|
const page2Button = page.locator('button').filter({ hasText: /^2$/ }).first();
|
|||
|
|
const hasPage2 = await page2Button.isVisible({ timeout: 10000 }).catch(() => false);
|
|||
|
|
test.skip(!hasPage2, 'Not enough terminals in this environment to require a second page');
|
|||
|
|
await page2Button.scrollIntoViewIfNeeded();
|
|||
|
|
await page2Button.click();
|
|||
|
|
console.log('Clicked page 2');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify page 2 loaded a different first row', async () => {
|
|||
|
|
let secondPageId = '';
|
|||
|
|
for (let attempt = 0; attempt < 30; 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('First terminal ID on page 2:', 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);
|
|||
|
|
|
|||
|
|
const rowCount = await table.locator('tbody tr:not([aria-hidden="true"])').count();
|
|||
|
|
expect(rowCount, 'Page 2 should have rows').toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Navigate back to page 1', async () => {
|
|||
|
|
const page1Button = page.locator('button').filter({ hasText: /^1$/ }).first();
|
|||
|
|
await expect(page1Button, 'A page 1 button should exist').toBeVisible({ timeout: 10000 });
|
|||
|
|
await page1Button.click({ force: true });
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
console.log('Navigated back to page 1');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('Pagination test completed');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 4: Download Report Test
|
|||
|
|
test('should download report and verify content', async ({ page }) => {
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('normal');
|
|||
|
|
description('Download terminal report in XSLX, cross-check data against portal table');
|
|||
|
|
|
|||
|
|
console.log('Testing terminal report download with cross-check...');
|
|||
|
|
|
|||
|
|
// Collect table data first
|
|||
|
|
const tableRows: string[][] = [];
|
|||
|
|
|
|||
|
|
await test.step('Collect terminal table data', async () => {
|
|||
|
|
const table = page.locator('table').first();
|
|||
|
|
const headers = await table.locator('thead th').allTextContents();
|
|||
|
|
const idIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'id');
|
|||
|
|
expect(idIdx, 'An ID column should exist').toBeGreaterThanOrEqual(0);
|
|||
|
|
|
|||
|
|
const rows = table.locator('tbody tr:not([aria-hidden="true"])');
|
|||
|
|
const rowCount = await rows.count();
|
|||
|
|
console.log(`Terminal rows: ${rowCount}`);
|
|||
|
|
for (let i = 0; i < rowCount; i++) {
|
|||
|
|
const cells = await rows.nth(i).locator('td').allTextContents();
|
|||
|
|
const cleaned = cells.map(c => c.trim());
|
|||
|
|
// Ant Design renders a "No data" placeholder row with no real ID when
|
|||
|
|
// a duplicate/sync copy of the table is present - require a genuine
|
|||
|
|
// numeric ID so that phantom row doesn't get counted as a real terminal.
|
|||
|
|
if (/^\d+$/.test(cleaned[idIdx] || '')) tableRows.push(cleaned);
|
|||
|
|
}
|
|||
|
|
console.log(`Collected ${tableRows.length} rows`);
|
|||
|
|
expect(tableRows.length).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let downloadedContent = '';
|
|||
|
|
|
|||
|
|
await test.step('Download XSLX report', async () => {
|
|||
|
|
const downloadButton = page.locator('button:has-text("Download report")').first();
|
|||
|
|
await downloadButton.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// Select XSLX — radio input with value="3"
|
|||
|
|
const xlsxRadio = page.locator('input.ant-radio-input[value="3"]').first();
|
|||
|
|
const xlsxVis = await xlsxRadio.isVisible().catch(() => false);
|
|||
|
|
if (xlsxVis) {
|
|||
|
|
await xlsxRadio.click({ force: true });
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
console.log('✓ XSLX selected via radio value=3');
|
|||
|
|
} else {
|
|||
|
|
// Fallback to wrapper
|
|||
|
|
const wrapper = page.locator('.ant-radio-button-wrapper', { hasText: 'XSLX' }).first();
|
|||
|
|
await wrapper.click();
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
console.log('✓ XSLX selected via wrapper');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
|
|||
|
|
const dlBtn = page.locator('button.bg-gradient-to-tr:has-text("Download")').first();
|
|||
|
|
await dlBtn.click();
|
|||
|
|
|
|||
|
|
const download = await downloadPromise;
|
|||
|
|
expect(download).toBeTruthy();
|
|||
|
|
const filePath = await download.path();
|
|||
|
|
console.log('✓ Downloaded:', download.suggestedFilename());
|
|||
|
|
|
|||
|
|
if (filePath) {
|
|||
|
|
const fs = await import('fs');
|
|||
|
|
const XLSX = await import('xlsx');
|
|||
|
|
const buf = fs.readFileSync(filePath);
|
|||
|
|
const wb = XLSX.read(buf, { type: 'buffer' });
|
|||
|
|
const sheet = wb.Sheets[wb.SheetNames[0]];
|
|||
|
|
const rows: any[][] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
|||
|
|
downloadedContent = JSON.stringify(rows);
|
|||
|
|
console.log(`XSLX rows: ${rows.length}`);
|
|||
|
|
console.log('Header:', JSON.stringify(rows[0])?.substring(0, 200));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Cross-check portal data against downloaded file', async () => {
|
|||
|
|
expect(downloadedContent).toBeTruthy();
|
|||
|
|
const unmatched: string[] = [];
|
|||
|
|
for (const row of tableRows) {
|
|||
|
|
const key = row[0] || row[1]; // ID or name
|
|||
|
|
if (!key || !downloadedContent.includes(key)) unmatched.push(key || '(blank)');
|
|||
|
|
}
|
|||
|
|
const matchCount = tableRows.length - unmatched.length;
|
|||
|
|
console.log(`Matched ${matchCount}/${tableRows.length} rows in XSLX`);
|
|||
|
|
expect(unmatched, `Every table row's ID should appear in the downloaded report (unmatched: ${unmatched.join(', ')})`).toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('✓ Download report + cross-check completed');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 5: Remove Cashier from Terminal
|
|||
|
|
test('should remove cashier from terminal', async ({ page }) => {
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('critical');
|
|||
|
|
description('Remove cashier from terminal and verify it now shows Unassigned in both the details panel and the terminals table');
|
|||
|
|
|
|||
|
|
console.log('Testing cashier removal from terminal...');
|
|||
|
|
|
|||
|
|
const table = page.locator('table').first();
|
|||
|
|
await table.locator('tbody tr:not([aria-hidden="true"])').first().waitFor({ state: 'visible', timeout: 15000 });
|
|||
|
|
const headers = await table.locator('thead th').allTextContents();
|
|||
|
|
const cashierColIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'cashier');
|
|||
|
|
console.log('Table headers:', headers);
|
|||
|
|
expect(cashierColIdx, 'A Cashier column should exist').toBeGreaterThanOrEqual(0);
|
|||
|
|
|
|||
|
|
let targetRow = -1;
|
|||
|
|
let cashierName = '';
|
|||
|
|
|
|||
|
|
await test.step('Find a terminal with an assigned cashier', async () => {
|
|||
|
|
const rows = table.locator('tbody tr:not([aria-hidden="true"])');
|
|||
|
|
const rowCount = await rows.count();
|
|||
|
|
console.log(`Total rows: ${rowCount}`);
|
|||
|
|
|
|||
|
|
for (let i = 0; i < rowCount; i++) {
|
|||
|
|
const cashierText = (await rows.nth(i).locator('td').nth(cashierColIdx).textContent({ timeout: 2000 }))?.trim() || '';
|
|||
|
|
console.log(`Row ${i} cashier: "${cashierText}"`);
|
|||
|
|
if (cashierText && cashierText !== 'Unassigned' && cashierText !== '-') {
|
|||
|
|
targetRow = i;
|
|||
|
|
cashierName = cashierText;
|
|||
|
|
console.log(`✓ Found terminal with assigned cashier at row ${i}: "${cashierName}"`);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
expect(targetRow, 'A terminal with an assigned cashier should exist').toBeGreaterThanOrEqual(0);
|
|||
|
|
|
|||
|
|
await rows.nth(targetRow).locator('button:has-text("Details")').click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click Remove Cashier button', async () => {
|
|||
|
|
const removeCashierButton = page.locator('button:has-text("Remove Cashier")');
|
|||
|
|
await expect(removeCashierButton, 'Remove Cashier button should be visible').toBeVisible({ timeout: 5000 });
|
|||
|
|
await removeCashierButton.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
console.log('Remove Cashier button clicked');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify the terminals table row shows Unassigned', async () => {
|
|||
|
|
// Removing the cashier closes the details panel automatically (same
|
|||
|
|
// behavior the reassign-cashier test accounts for) - dismiss defensively
|
|||
|
|
// in case that ever changes, then read the row straight from the table.
|
|||
|
|
const closeBtn = page.locator('button:has-text("Close"), button.ant-modal-close, [aria-label="Close"]').first();
|
|||
|
|
if (await closeBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|||
|
|
await closeBtn.click();
|
|||
|
|
} else {
|
|||
|
|
await page.keyboard.press('Escape').catch(() => {});
|
|||
|
|
}
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
const cashierText = (await table.locator('tbody tr:not([aria-hidden="true"])').nth(targetRow).locator('td').nth(cashierColIdx).textContent({ timeout: 5000 }))?.trim() || '';
|
|||
|
|
console.log(`Table row ${targetRow} cashier after removal: "${cashierText}" (was "${cashierName}")`);
|
|||
|
|
expect(cashierText, 'Table row should show Unassigned after removal').toBe('Unassigned');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('Remove cashier test completed');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 6: Remove cashier then reassign a different one
|
|||
|
|
test('should reassign cashier to terminal', async ({ page }) => {
|
|||
|
|
test.setTimeout(90000);
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('critical');
|
|||
|
|
description('Find terminal with cashier, remove cashier, then reassign a different cashier');
|
|||
|
|
|
|||
|
|
console.log('Testing remove + reassign different cashier...');
|
|||
|
|
|
|||
|
|
let targetTerminalRow = -1;
|
|||
|
|
let originalCashier = '';
|
|||
|
|
|
|||
|
|
// Step 1: Find a terminal with an assigned cashier
|
|||
|
|
await test.step('Find terminal with assigned cashier', async () => {
|
|||
|
|
const headers = await page.locator('table thead th').allTextContents();
|
|||
|
|
console.log('Table headers:', headers);
|
|||
|
|
|
|||
|
|
const rows = page.locator('table tbody tr:not([aria-hidden="true"])');
|
|||
|
|
const rowCount = await rows.count();
|
|||
|
|
console.log('Total rows:', rowCount);
|
|||
|
|
|
|||
|
|
// Debug: print first few rows
|
|||
|
|
if (rowCount > 0) {
|
|||
|
|
const firstRowCells = await rows.nth(0).locator('td').allTextContents();
|
|||
|
|
console.log('First row cells:', firstRowCells);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Try to find a terminal with an assigned cashier
|
|||
|
|
let cashierColIdx = -1;
|
|||
|
|
for (let i = 0; i < rowCount; i++) {
|
|||
|
|
const allCells = await rows.nth(i).locator('td').allTextContents();
|
|||
|
|
|
|||
|
|
// Look for "Unassigned" to identify the cashier column
|
|||
|
|
const unassignedIdx = allCells.findIndex(cell => cell.trim() === 'Unassigned');
|
|||
|
|
if (unassignedIdx >= 0) {
|
|||
|
|
cashierColIdx = unassignedIdx;
|
|||
|
|
console.log('Found cashier column at index:', cashierColIdx);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Find a terminal with an assigned cashier
|
|||
|
|
for (let i = 0; i < rowCount; i++) {
|
|||
|
|
const allCells = await rows.nth(i).locator('td').allTextContents();
|
|||
|
|
const cashierText = (allCells[cashierColIdx] || '').trim();
|
|||
|
|
|
|||
|
|
if (cashierText && cashierText !== 'Unassigned' && cashierText !== '-') {
|
|||
|
|
targetTerminalRow = i;
|
|||
|
|
originalCashier = cashierText;
|
|||
|
|
console.log(`✓ Found terminal with assigned cashier at row ${i}: "${originalCashier}"`);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If no assigned cashier found, use first terminal and assign one first
|
|||
|
|
if (targetTerminalRow === -1) {
|
|||
|
|
targetTerminalRow = 0;
|
|||
|
|
originalCashier = 'Unassigned';
|
|||
|
|
console.log('✓ No assigned cashier found. Will assign one to first terminal first.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
expect(targetTerminalRow).toBeGreaterThanOrEqual(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Step 2: Open details and assign cashier if needed, then remove
|
|||
|
|
await test.step('Assign cashier if unassigned, then remove', async () => {
|
|||
|
|
const row = page.locator('table tbody tr:not([aria-hidden="true"])').nth(targetTerminalRow);
|
|||
|
|
await row.locator('button:has-text("Details")').click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
// If terminal is unassigned, assign a cashier first
|
|||
|
|
if (originalCashier === 'Unassigned') {
|
|||
|
|
console.log('Terminal is unassigned. Assigning a cashier first...');
|
|||
|
|
|
|||
|
|
const reassignBtn = page.locator('button:has-text("reassignCashier")');
|
|||
|
|
const reassignVis = await reassignBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (reassignVis) {
|
|||
|
|
await reassignBtn.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// Select first available cashier. Scope to the open modal/drawer and
|
|||
|
|
// exclude read-only inputs: a separate, always-present, read-only
|
|||
|
|
// combobox elsewhere on the page (id="rc_select_0") otherwise causes
|
|||
|
|
// a strict-mode violation (2 elements matched) on the plain selector.
|
|||
|
|
const detailsPanel = page.locator('.ant-modal-content, .ant-drawer-content, [role="dialog"]').first();
|
|||
|
|
const searchInput = detailsPanel.locator('input.ant-select-selection-search-input[role="combobox"]:not([readonly])').first();
|
|||
|
|
await searchInput.waitFor({ state: 'visible', timeout: 5000 });
|
|||
|
|
await searchInput.click();
|
|||
|
|
await page.waitForTimeout(1500);
|
|||
|
|
|
|||
|
|
const option = page.locator('.ant-select-item-option:visible').first();
|
|||
|
|
const optText = await option.textContent();
|
|||
|
|
await option.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
console.log('✓ Assigned cashier:', optText?.trim());
|
|||
|
|
|
|||
|
|
// Click Done
|
|||
|
|
const doneBtn = page.locator('button.bg-green-500:has-text("Done")');
|
|||
|
|
await doneBtn.click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log('✓ Assignment confirmed');
|
|||
|
|
|
|||
|
|
// Close and reopen to see the assigned cashier
|
|||
|
|
const closeBtn = page.locator('button:has-text("Close"), button.ant-modal-close, [aria-label="Close"]').first();
|
|||
|
|
const closeVis = await closeBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
if (closeVis) {
|
|||
|
|
await closeBtn.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
} else {
|
|||
|
|
await page.keyboard.press('Escape');
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Reopen
|
|||
|
|
await row.locator('button:has-text("Details")').click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Now remove the cashier
|
|||
|
|
const removeBtn = page.locator('button:has-text("Remove Cashier")');
|
|||
|
|
const removeBtnVis = await removeBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (removeBtnVis) {
|
|||
|
|
await removeBtn.click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log('✓ Cashier removed');
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠ Remove button not found');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait and check for reassignCashier button
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
// Try different selectors for the reassign button
|
|||
|
|
let reassignBtn = await page.locator('button:has-text("reassignCashier")').isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (!reassignBtn) {
|
|||
|
|
// Try alternative selectors
|
|||
|
|
reassignBtn = await page.locator('button:has-text("Reassign")').isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!reassignBtn) {
|
|||
|
|
// Try looking for any button with "assign" in text
|
|||
|
|
reassignBtn = await page.locator('button:has-text("assign")').isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('✓ reassignCashier button visible:', reassignBtn);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Step 3: Close details, reopen to see reassignCashier button
|
|||
|
|
await test.step('Close and reopen terminal details', async () => {
|
|||
|
|
// Close the details panel
|
|||
|
|
const closeBtn = page.locator('button:has-text("Close"), button.ant-modal-close, [aria-label="Close"]').first();
|
|||
|
|
const closeVis = await closeBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
if (closeVis) {
|
|||
|
|
await closeBtn.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
} else {
|
|||
|
|
await page.keyboard.press('Escape');
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Reopen the same terminal details
|
|||
|
|
const row = page.locator('table tbody tr:not([aria-hidden="true"])').nth(targetTerminalRow);
|
|||
|
|
await row.locator('button:has-text("Details")').click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log('✓ Terminal details reopened');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click reassignCashier', async () => {
|
|||
|
|
// Try different selectors for the reassign button
|
|||
|
|
let reassignBtn = page.locator('button:has-text("reassignCashier")');
|
|||
|
|
let vis = await reassignBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (!vis) {
|
|||
|
|
reassignBtn = page.locator('button:has-text("Reassign")');
|
|||
|
|
vis = await reassignBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!vis) {
|
|||
|
|
reassignBtn = page.locator('button:has-text("assign")');
|
|||
|
|
vis = await reassignBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (vis) {
|
|||
|
|
await reassignBtn.scrollIntoViewIfNeeded().catch(() => {});
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
await reassignBtn.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
console.log('✓ reassignCashier clicked');
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠ reassignCashier button not found, trying force click');
|
|||
|
|
try {
|
|||
|
|
await reassignBtn.click({ force: true });
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
console.log('✓ reassignCashier clicked (force)');
|
|||
|
|
} catch (e) {
|
|||
|
|
console.log('⚠ Force click failed:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Select a different cashier', async () => {
|
|||
|
|
const detailsPanel = page.locator('.ant-modal-content, .ant-drawer-content, [role="dialog"]').first();
|
|||
|
|
const searchInput = detailsPanel.locator('input.ant-select-selection-search-input[role="combobox"]:not([readonly])').first();
|
|||
|
|
await searchInput.waitFor({ state: 'visible', timeout: 5000 });
|
|||
|
|
|
|||
|
|
// Click to open dropdown and see all available cashiers
|
|||
|
|
await searchInput.click();
|
|||
|
|
await page.waitForTimeout(1500);
|
|||
|
|
|
|||
|
|
// Get all available options
|
|||
|
|
const options = page.locator('.ant-select-item-option:visible');
|
|||
|
|
const optCount = await options.count();
|
|||
|
|
|
|||
|
|
if (optCount > 0) {
|
|||
|
|
// Select the first available cashier (skip if it's the original)
|
|||
|
|
const firstOption = options.first();
|
|||
|
|
const optText = await firstOption.textContent();
|
|||
|
|
|
|||
|
|
// If first option is the original cashier, try the second one
|
|||
|
|
if (optText?.trim() === originalCashier && optCount > 1) {
|
|||
|
|
const secondOption = options.nth(1);
|
|||
|
|
const secondText = await secondOption.textContent();
|
|||
|
|
await secondOption.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
console.log('✓ Selected new cashier (second option):', secondText?.trim());
|
|||
|
|
} else {
|
|||
|
|
await firstOption.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
console.log('✓ Selected new cashier (first option):', optText?.trim());
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.log('No available cashiers in dropdown');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click Done to confirm reassignment', async () => {
|
|||
|
|
// Click the Done button to confirm
|
|||
|
|
const doneBtn = page.locator('button:has-text("Done")');
|
|||
|
|
const vis = await doneBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (vis) {
|
|||
|
|
await doneBtn.click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log('✓ Done clicked - cashier reassigned');
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠ Done button not found');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify new cashier assigned in table', async () => {
|
|||
|
|
// Close details panel
|
|||
|
|
const closeBtn = page.locator('button:has-text("Close"), button.ant-modal-close, [aria-label="Close"]').first();
|
|||
|
|
const closeVis = await closeBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
if (closeVis) {
|
|||
|
|
await closeBtn.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
} else {
|
|||
|
|
await page.keyboard.press('Escape');
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check the table row — the cashier column should now show the new cashier.
|
|||
|
|
// Poll instead of a single fixed-wait read: under a full suite run the
|
|||
|
|
// backend/table refresh can lag more than in isolation, which caused an
|
|||
|
|
// intermittent false-negative here.
|
|||
|
|
const headers = await page.locator('table thead th').allTextContents();
|
|||
|
|
const cashierColIdx = headers.findIndex(h => h.toLowerCase().includes('cashier') && !h.toLowerCase().includes('id') && !h.toLowerCase().includes('username'));
|
|||
|
|
const row = page.locator('table tbody tr:not([aria-hidden="true"])').nth(targetTerminalRow);
|
|||
|
|
|
|||
|
|
let cashierText = '';
|
|||
|
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|||
|
|
cashierText = (await row.locator('td').nth(cashierColIdx >= 0 ? cashierColIdx : 3).textContent({ timeout: 5000 }).catch(() => ''))?.trim() || '';
|
|||
|
|
if (cashierText && cashierText !== 'Unassigned' && cashierText !== '-' && cashierText !== originalCashier) break;
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
}
|
|||
|
|
console.log(`Table row ${targetTerminalRow} cashier after reassign: "${cashierText}"`);
|
|||
|
|
console.log(`Original cashier was: "${originalCashier}"`);
|
|||
|
|
|
|||
|
|
const isAssigned = cashierText && cashierText !== 'Unassigned' && cashierText !== '-';
|
|||
|
|
const isDifferent = cashierText !== originalCashier;
|
|||
|
|
|
|||
|
|
console.log('Cashier is assigned:', isAssigned);
|
|||
|
|
console.log('Cashier is different from original:', isDifferent);
|
|||
|
|
|
|||
|
|
expect(isAssigned).toBe(true);
|
|||
|
|
expect(isDifferent).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('✓ Remove + reassign different cashier completed');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Test 7: Add Terminal
|
|||
|
|
test('should add a new terminal', async ({ page }) => {
|
|||
|
|
feature('Terminal Operations');
|
|||
|
|
tag('functional');
|
|||
|
|
severity('critical');
|
|||
|
|
description('Add a new terminal with Terminal ID, Provider, Terminal Model and Mada Package');
|
|||
|
|
|
|||
|
|
console.log('Testing add terminal...');
|
|||
|
|
|
|||
|
|
// Generate a unique exactly-8-digit terminal ID (10000000–99999999)
|
|||
|
|
const newTerminalId = String(10000000 + (Date.now() % 90000000)).slice(0, 8);
|
|||
|
|
console.log('New Terminal ID to create:', newTerminalId);
|
|||
|
|
|
|||
|
|
await test.step('Click Add terminal button to open form', async () => {
|
|||
|
|
// The first "Add terminal" button (anticon-plus) opens/shows the inline form
|
|||
|
|
const addTerminalBtn = page.locator('button:has(.anticon-plus):has-text("Add terminal"), button:has([aria-label="plus"]):has-text("Add terminal")').first();
|
|||
|
|
const isVisible = await addTerminalBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (isVisible) {
|
|||
|
|
await addTerminalBtn.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
console.log('✓ Add terminal form opened');
|
|||
|
|
} else {
|
|||
|
|
// Fallback: try all buttons with "Add terminal" text and pick the one with anticon-plus (not anticon-plus-square)
|
|||
|
|
const allAddBtns = page.locator('button:has-text("Add terminal")');
|
|||
|
|
const count = await allAddBtns.count();
|
|||
|
|
console.log(`Found ${count} "Add terminal" buttons`);
|
|||
|
|
|
|||
|
|
for (let i = 0; i < count; i++) {
|
|||
|
|
const btn = allAddBtns.nth(i);
|
|||
|
|
const hasPlusSquare = await btn.locator('.anticon-plus-square').isVisible().catch(() => false);
|
|||
|
|
if (!hasPlusSquare) {
|
|||
|
|
await btn.click();
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
console.log(`✓ Clicked "Add terminal" button ${i} (open form button)`);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Fill Terminal ID', async () => {
|
|||
|
|
// Scope to the open modal: the page's top-level search box has the
|
|||
|
|
// placeholder "Search by Terminal id or cashier name", which also
|
|||
|
|
// matches a loose `placeholder*="Terminal Id" i` selector and, being
|
|||
|
|
// first in DOM order, silently absorbed the fill instead of the real
|
|||
|
|
// form field - leaving every submitted terminal with a blank ID.
|
|||
|
|
const modal = page.locator('.ant-modal-content, .ant-drawer-content, [role="dialog"]').first();
|
|||
|
|
const terminalIdInput = modal.locator('input[placeholder="Terminal Id"]').first();
|
|||
|
|
await terminalIdInput.waitFor({ state: 'visible', timeout: 5000 });
|
|||
|
|
await terminalIdInput.fill(newTerminalId);
|
|||
|
|
const filledValue = await terminalIdInput.inputValue();
|
|||
|
|
expect(filledValue, 'Terminal ID field should contain the value we filled').toBe(newTerminalId);
|
|||
|
|
console.log('✓ Terminal ID filled:', newTerminalId);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Select Terminal Provider', async () => {
|
|||
|
|
const providerSelect = page.locator('.ant-select').filter({ hasText: /Select terminal provider/i }).first();
|
|||
|
|
const isVisible = await providerSelect.isVisible().catch(() => false);
|
|||
|
|
if (isVisible) {
|
|||
|
|
await providerSelect.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
const firstOption = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option').first();
|
|||
|
|
const optText = await firstOption.textContent();
|
|||
|
|
await firstOption.click({ force: true });
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
console.log('✓ Provider selected:', optText?.trim());
|
|||
|
|
} else {
|
|||
|
|
console.log('Provider dropdown not found');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Select Terminal Model', async () => {
|
|||
|
|
const modelSelect = page.locator('.ant-select').filter({ hasText: /Select terminal model/i }).first();
|
|||
|
|
const isVisible = await modelSelect.isVisible().catch(() => false);
|
|||
|
|
if (isVisible) {
|
|||
|
|
await modelSelect.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
const firstOption = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option').first();
|
|||
|
|
const optText = await firstOption.textContent();
|
|||
|
|
await firstOption.click({ force: true });
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
console.log('✓ Terminal model selected:', optText?.trim());
|
|||
|
|
} else {
|
|||
|
|
console.log('Terminal model dropdown not found');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Select Mada Package', async () => {
|
|||
|
|
const packageSelect = page.locator('.ant-select').filter({ hasText: /Select mada package/i }).first();
|
|||
|
|
const isVisible = await packageSelect.isVisible().catch(() => false);
|
|||
|
|
if (isVisible) {
|
|||
|
|
await packageSelect.click();
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
const firstOption = page.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option').first();
|
|||
|
|
const optText = await firstOption.textContent();
|
|||
|
|
await firstOption.click({ force: true });
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
console.log('✓ Mada package selected:', optText?.trim());
|
|||
|
|
} else {
|
|||
|
|
console.log('Mada package dropdown not found');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Click submit Add terminal button (anticon-plus-square)', async () => {
|
|||
|
|
// The second "Add terminal" button (anticon-plus-square) submits the form
|
|||
|
|
const submitBtn = page.locator('button:has(.anticon-plus-square):has-text("Add terminal"), button:has([aria-label="plus-square"]):has-text("Add terminal")').first();
|
|||
|
|
const isVisible = await submitBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (isVisible) {
|
|||
|
|
await submitBtn.click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log('✓ Form submitted via Add terminal (plus-square) button');
|
|||
|
|
} else {
|
|||
|
|
// Fallback: find the button with anticon-plus-square among all "Add terminal" buttons
|
|||
|
|
const allAddBtns = page.locator('button:has-text("Add terminal")');
|
|||
|
|
const count = await allAddBtns.count();
|
|||
|
|
for (let i = 0; i < count; i++) {
|
|||
|
|
const btn = allAddBtns.nth(i);
|
|||
|
|
const hasPlusSquare = await btn.locator('.anticon-plus-square').isVisible().catch(() => false);
|
|||
|
|
if (hasPlusSquare) {
|
|||
|
|
await btn.click();
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
console.log(`✓ Submitted via "Add terminal" button ${i} (plus-square)`);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify new terminal appears in table', async () => {
|
|||
|
|
// Dismiss any modals
|
|||
|
|
await page.keyboard.press('Escape').catch(() => {});
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// The list has 200+ pages and a new terminal isn't guaranteed to sort
|
|||
|
|
// onto page 1, so search for it directly instead of paging blindly.
|
|||
|
|
const searchInput = page.locator('input[placeholder*="Search by Terminal id" i]').first();
|
|||
|
|
await expect(searchInput, 'Terminal search input should be visible').toBeVisible({ timeout: 10000 });
|
|||
|
|
await searchInput.fill(newTerminalId);
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
const terminalRow = page.locator('table tbody tr').filter({ hasText: newTerminalId }).first();
|
|||
|
|
await expect(terminalRow, `New terminal ${newTerminalId} should appear in search results`).toBeVisible({ timeout: 10000 });
|
|||
|
|
console.log(`✓ New terminal "${newTerminalId}" found via search`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('Add terminal test completed');
|
|||
|
|
});
|
|||
|
|
});
|