Files
Frontend_automation/tests/terminals.spec.ts
Rediet Wogayehu 7bf4343f88
Some checks failed
E2E Tests / test (push) Has been cancelled
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

763 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (1000000099999999)
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');
});
});