import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import { login } from './helpers/auth'; import { epic, feature, severity, description, tag } from './helpers/allure'; import { navigateToRefunds } from './helpers/navigation'; // Parses either an ISO picker-cell title (YYYY-MM-DD) or a display-format // table date ("Apr 26, 2026") into a comparable Date. const toDate = (s: string | null | undefined): Date | null => { if (!s) return null; const iso = s.match(/(\d{4})-(\d{2})-(\d{2})/); if (iso) return new Date(`${iso[1]}-${iso[2]}-${iso[3]}T00:00:00`); const disp = s.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),\s*(\d{4})/); if (disp) { const months: Record = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12', }; return new Date(`${disp[3]}-${months[disp[1]]}-${disp[2].padStart(2, '0')}T00:00:00`); } return null; }; const toISO = (d: Date): string => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; test.describe('Refund Tests', () => { test.setTimeout(90000); test.beforeEach(async ({ page }) => { epic('Refund Management'); await login(page); await navigateToRefunds(page); await page.waitForTimeout(2000); }); test('should have filter or search functionality', async ({ page }) => { feature('Refund Display'); severity('normal'); description('Verify that filter or search controls are available and can search by RRN'); console.log('Checking for filter/search functionality...'); let rrnValue = ''; await test.step('Get RRN from refunds (check multiple pages)', async () => { // Wait for table to load properly await page.waitForTimeout(2000); // Check pages 1, 2, and 3 for RRN for (let pageNum = 1; pageNum <= 3 && !rrnValue; pageNum++) { if (pageNum > 1) { console.log(`Navigating to page ${pageNum}...`); const pageButton = page.locator(`button:has-text("${pageNum}")`).first(); const isVisible = await pageButton.isVisible().catch(() => false); if (isVisible) { await pageButton.click(); await page.waitForTimeout(3000); console.log(`✓ Navigated to page ${pageNum}`); } else { console.log(`Page ${pageNum} button not found`); break; } } // Get all table rows const rows = page.locator('table tbody tr'); const rowCount = await rows.count(); console.log(`Page ${pageNum}: ${rowCount} rows`); // Check each row for RRN column (usually 9th column based on structure) for (let i = 0; i < Math.min(rowCount, 15); i++) { try { const row = rows.nth(i); const cells = row.locator('td'); const cellCount = await cells.count(); // RRN is typically in column 8 (0-indexed) based on: ID, Amount, Net, Status, Date, Terminal Id, Type, Card Type, RRN if (cellCount >= 9) { const rrnCell = cells.nth(8); const rrnText = await rrnCell.textContent({ timeout: 1000 }); if (rrnText && rrnText.trim() !== '-' && /^\d{12}$/.test(rrnText.trim())) { rrnValue = rrnText.trim(); console.log(`Found RRN on page ${pageNum}, row ${i + 1}:`, rrnValue); break; } } } catch (e) { continue; } } } // Navigate back to page 1 for search if (rrnValue) { console.log('Navigating back to page 1 to perform search...'); const page1Button = page.locator('button:has-text("1")').first(); const page1Visible = await page1Button.isVisible().catch(() => false); if (page1Visible) { await page1Button.click(); await page.waitForTimeout(3000); console.log('✓ Back on page 1'); } } else { console.log('Could not extract RRN from pages 1-3, will skip search test'); } }); await test.step('Verify search input exists', async () => { const searchInput = await page.locator('input[type="search"], input[placeholder*="search" i]').first().isVisible().catch(() => false); const filterButton = await page.locator('button:has-text("Filter"), [class*="filter" i]').first().isVisible().catch(() => false); console.log('Search input visible:', searchInput); console.log('Filter button visible:', filterButton); expect(searchInput || filterButton).toBeTruthy(); }); await test.step(`Search by RRN: ${rrnValue}`, async () => { if (!rrnValue) { console.log('No RRN value to search, skipping search test'); return; } const searchInput = page.locator('input[type="search"], input[placeholder*="search" i]').first(); const isVisible = await searchInput.isVisible().catch(() => false); if (isVisible) { console.log(`Entering RRN: ${rrnValue} in search field...`); await searchInput.clear(); await searchInput.fill(rrnValue); await page.waitForTimeout(1000); await page.keyboard.press('Enter'); await page.waitForTimeout(3000); console.log('Search triggered'); const resultCount = await page.locator('table tbody tr').count(); console.log('Search result count:', resultCount); const rrnFound = await page.locator(`text=${rrnValue}`).first().isVisible().catch(() => false); console.log(`RRN ${rrnValue} found in results:`, rrnFound); if (rrnFound) { console.log('✓ Search by RRN successful'); expect(rrnFound).toBeTruthy(); } else if (resultCount > 0) { console.log('⚠ Results found but RRN not visible (may be in different format)'); expect(resultCount).toBeGreaterThan(0); } else { console.log(`⚠ No results found for RRN ${rrnValue}`); } } else { console.log('Search input not found'); } }); console.log('Filter/search functionality verified'); }); // Functional Tests test('should filter refunds by date range', async ({ page }) => { test.setTimeout(90000); feature('Refund Operations'); tag('functional'); severity('critical'); description('Test filtering refunds by selecting a valid date range, then assert every returned row falls within that range'); console.log('Testing date range filter...'); let initialRowCount = 0; let filteredRowCount = 0; let startTitle = ''; let endTitle = ''; await test.step('Count initial refunds', async () => { initialRowCount = await page.locator('table tbody tr').count(); console.log('Initial refund count:', initialRowCount); }); await test.step('Open filter controls', async () => { const filterButton = page.locator('button:has-text("Filter"), [class*="filter" i]').first(); const isVisible = await filterButton.isVisible().catch(() => false); if (isVisible) { await filterButton.click(); await page.waitForTimeout(1000); console.log('Filter controls opened'); } }); await test.step('Click date range picker to open calendar', async () => { const datePickerSelectors = [ 'input[placeholder="startDate"]', 'input[date-range="start"]', '.ant-picker input[date-range="start"]', 'input[type="date"]', 'input[placeholder*="date" i]' ]; let calendarOpened = false; for (const selector of datePickerSelectors) { const dateField = page.locator(selector).first(); const isVisible = await dateField.isVisible().catch(() => false); if (isVisible) { console.log(`Found date picker with selector: ${selector}`); await dateField.click(); await page.waitForTimeout(1500); const calendarVisible = await page.locator('.ant-picker-dropdown:not(.ant-picker-dropdown-hidden)').isVisible().catch(() => false); console.log('Calendar opened:', calendarVisible); if (calendarVisible) { calendarOpened = true; break; } } } expect(calendarOpened, 'Date range calendar should open').toBe(true); }); await test.step('Select a valid ordered date range (start <= end)', async () => { // Collect all selectable in-view day cells and sort their dates ascending const cells = page.locator('.ant-picker-cell-in-view:not(.ant-picker-cell-disabled)'); const titles: string[] = (await cells.evaluateAll( (els) => els.map((e) => e.getAttribute('title')).filter((t): t is string => !!t) )); const unique = Array.from(new Set(titles)).sort(); // ISO strings sort chronologically expect(unique.length, 'There should be selectable date cells').toBeGreaterThan(1); // Pick a forward range: earliest available as start, a later date as end startTitle = unique[0]; endTitle = unique[Math.min(unique.length - 1, 20)]; // Guarantee start <= end if (toDate(startTitle)! > toDate(endTitle)!) { [startTitle, endTitle] = [endTitle, startTitle]; } console.log(`Range -> start: ${startTitle}, end: ${endTitle}`); // Click start then end by exact title await page.locator(`.ant-picker-cell-in-view[title="${startTitle}"]`).first().click(); await page.waitForTimeout(800); await page.locator(`.ant-picker-cell-in-view[title="${endTitle}"]`).first().click(); await page.waitForTimeout(1000); console.log('✓ Start and end dates selected'); // Assert the range is valid and ordered const startDate = toDate(startTitle); const endDate = toDate(endTitle); expect(startDate, 'Start date should be valid').not.toBeNull(); expect(endDate, 'End date should be valid').not.toBeNull(); expect(startDate!.getTime(), 'Start should be on/before end').toBeLessThanOrEqual(endDate!.getTime()); }); await test.step('Apply filter', async () => { const applyButton = page.locator('button:has-text("Apply"), button:has-text("Search"), button[type="submit"]').first(); const isVisible = await applyButton.isVisible().catch(() => false); if (isVisible) { await applyButton.click(); await page.waitForTimeout(3000); console.log('Filter applied'); } // Close any lingering date-picker dropdown so it doesn't interfere with reads await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(500); }); await test.step('Assert every returned row date is valid and within range', async () => { const startDate = toDate(startTitle)!; const endDate = toDate(endTitle)!; // Make the end inclusive through the whole day endDate.setHours(23, 59, 59, 999); // Scope to the refunds data table (the "RRN" header is unique to it, // which avoids matching the date-picker calendar tables). const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const headers = await dataTable.locator('thead th').allTextContents(); const dateIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'date'); console.log(`Date column index: ${dateIdx} (headers: ${headers.join(' | ')})`); expect(dateIdx, 'A date column should exist').toBeGreaterThanOrEqual(0); const rows = dataTable.locator('tbody tr.ant-table-row'); const rowCount = await rows.count(); filteredRowCount = rowCount; console.log(`Validating ${rowCount} rows are within ${startTitle} .. ${endTitle}`); let checked = 0; for (let i = 0; i < rowCount; i++) { const cellTexts = await rows.nth(i).locator('td').allTextContents(); const cellText = cellTexts[dateIdx] || ''; const rowDate = toDate(cellText); // Each row must expose a valid, parseable date expect(rowDate, `Row ${i + 1} should have a valid date (got "${cellText}")`).not.toBeNull(); // And it must fall within the selected range const t = rowDate!.getTime(); expect( t >= startDate.getTime() && t <= endDate.getTime(), `Row ${i + 1} date ${cellText} should be within ${startTitle}..${endTitle}` ).toBe(true); checked++; } console.log(`✓ All ${checked} returned rows fall within the selected range`); }); console.log('Date range filter test completed'); }); test('should view refund details', async ({ page }) => { test.setTimeout(90000); feature('Refund Operations'); tag('functional'); severity('critical'); description('Test clicking on Receipt button to view refund details and verify data matches'); console.log('Testing refund details view...'); let refundId = ''; let refundAmount = ''; let refundStatus = ''; await test.step('Capture refund data from table row', async () => { await page.waitForTimeout(2000); // Scope to the refunds data table via its unique "RRN" header const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const headers = await dataTable.locator('thead th').allTextContents(); const idIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'id'); const amtIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'amount'); const statusIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'status'); console.log(`Columns -> id:${idIdx} amount:${amtIdx} status:${statusIdx}`); const noData = await dataTable.locator('tbody', { hasText: 'No data' }).first().isVisible({ timeout: 5000 }).catch(() => false); test.skip(noData, 'No refund records available in this environment to view details for'); // First real data row that has a Receipt button const firstRow = dataTable.locator('tbody tr.ant-table-row:has(button:has-text("Receipt"))').first(); await expect(firstRow, 'A refund row with a Receipt button should exist').toBeVisible({ timeout: 10000 }); const cellTexts = await firstRow.locator('td').allTextContents(); refundId = (cellTexts[idIdx] || '').replace(/\D/g, '').trim(); const amtMatch = (cellTexts[amtIdx] || '').match(/(\d+\.\d{2})/); refundAmount = amtMatch ? amtMatch[1] : ''; refundStatus = (cellTexts[statusIdx] || '').trim(); console.log(`Captured -> ID: ${refundId}, Amount: ${refundAmount}, Status: ${refundStatus}`); // Assert we actually captured meaningful row data expect(refundId, 'Refund ID should be captured').toMatch(/^\d+$/); expect(refundAmount, 'Refund amount should be captured').toMatch(/^\d+\.\d{2}$/); expect(refundStatus.length, 'Refund status should be captured').toBeGreaterThan(0); }); await test.step('Click on Receipt button', async () => { const receiptButton = page.locator('table tbody tr button:has-text("Receipt")').first(); await expect(receiptButton, 'Receipt button should be visible').toBeVisible({ timeout: 10000 }); console.log('Clicking Receipt button...'); await receiptButton.click(); await page.waitForTimeout(2500); console.log('✓ Receipt button clicked'); }); await test.step('Verify refund details match captured row', async () => { // The receipt opens in a modal/drawer/dialog const panel = page.locator('.ant-modal-content, .ant-drawer-content, [role="dialog"]').first(); await expect(panel, 'Refund details panel should open').toBeVisible({ timeout: 10000 }); const panelText = (await panel.textContent()) || ''; console.log('Panel text (first 300):', panelText.substring(0, 300)); // Cross-check the captured values appear in the opened details expect(panelText, `Details panel should contain captured ID ${refundId}`).toContain(refundId); expect(panelText, `Details panel should contain captured amount ${refundAmount}`).toContain(refundAmount); // Status may be rendered differently; surface as a soft check if (refundStatus) { expect.soft(panelText, `Details panel should contain status ${refundStatus}`).toContain(refundStatus); } console.log('✓ Details panel matches captured row data'); }); console.log('Refund details view test completed'); }); test('should export refunds', async ({ page }) => { test.setTimeout(90000); feature('Refund Operations'); tag('functional'); severity('normal'); description('Export refunds as CSV for a date range and verify the file contains the refund transactions shown in the table'); console.log('Testing refund export (CSV) + content cross-check...'); let downloadSuccessful = false; let fileName = ''; let startTitle = ''; let endTitle = ''; const monthMap: Record = { Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12' }; const expectedIds: string[] = []; const toISO = (s: string): string | null => { const m = s.match(/([A-Za-z]{3})\s+(\d{1,2}),\s*(\d{4})/); if (!m) return null; return `${m[3]}-${monthMap[m[1]]}-${m[2].padStart(2, '0')}`; }; // Determine the date range from the first table row, then capture the IDs // of all rows that fall within that range so we can verify them in the CSV. await test.step('Capture all refund rows in range across pages', async () => { const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const noData = await dataTable.locator('tbody', { hasText: 'No data' }).first().isVisible({ timeout: 5000 }).catch(() => false); test.skip(noData, 'No refund records available in this environment to export'); // Wait for the refunds table to actually render its rows await dataTable.locator('tbody tr.ant-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); const headers = await dataTable.locator('thead th').allTextContents(); const idIdx = headers.findIndex(h => h.trim().toLowerCase() === 'id'); const dateIdx = headers.findIndex(h => h.trim().toLowerCase() === 'date'); expect(idIdx, 'ID column should exist').toBeGreaterThanOrEqual(0); expect(dateIdx, 'Date column should exist').toBeGreaterThanOrEqual(0); // Use the first (newest) row's date to define a same-month range: 1st -> that day const firstRowCells = await dataTable.locator('tbody tr.ant-table-row').first().locator('td').allTextContents(); const firstIso = toISO(firstRowCells[dateIdx] || ''); expect(firstIso, 'First row should have a parseable date').not.toBeNull(); const [yy, mm, dd] = firstIso!.split('-'); startTitle = `${yy}-${mm}-01`; endTitle = `${yy}-${mm}-${dd}`; console.log(`Target range: ${startTitle} .. ${endTitle}`); // Log the total results indicator if present ("... of N results") const totalText = await page.locator('text=/of\\s+[\\d,]+\\s+results/i').first().textContent().catch(() => null); console.log(`Results indicator: ${totalText ? totalText.trim() : 'not found'}`); // Paginate (list is newest-first) collecting every in-range refund ID. // Stop when a row older than the range start appears, or no next page. let currentPage = 1; let keepPaging = true; const maxPages = 50; // safety cap while (keepPaging && currentPage <= maxPages) { const rows = dataTable.locator('tbody tr.ant-table-row'); await rows.first().waitFor({ state: 'visible', timeout: 10000 }); const rowCount = await rows.count(); const firstCellsForKey = await rows.first().locator('td').allTextContents(); const firstRowId = (firstCellsForKey[idIdx] || '').replace(/\D/g, '').trim(); let sawOlder = false; let inRangeThisPage = 0; const pageDates: string[] = []; for (let i = 0; i < rowCount; i++) { const cells = await rows.nth(i).locator('td').allTextContents(); const iso = toISO(cells[dateIdx] || ''); const id = (cells[idIdx] || '').replace(/\D/g, '').trim(); if (!iso) continue; pageDates.push(iso); if (iso < startTitle) { sawOlder = true; continue; } // older than range if (iso > endTitle) continue; // newer than range (skip) if (id) { expectedIds.push(id); inRangeThisPage++; } } const minDate = pageDates.length ? pageDates.reduce((a, b) => (a < b ? a : b)) : 'n/a'; const maxDate = pageDates.length ? pageDates.reduce((a, b) => (a > b ? a : b)) : 'n/a'; console.log(`Page ${currentPage}: ${rowCount} total rows, dates ${minDate}..${maxDate}, ${inRangeThisPage} in-range`); // Once we hit dates older than the range start, no later pages can be in range if (sawOlder) { keepPaging = false; break; } // Move to the next page if it exists. Pagination number buttons carry // the distinctive "border-2 rounded-lg font-medium" classes. const nextPage = currentPage + 1; const allPageBtns = await page.locator('button.border-2.rounded-lg.font-medium').allTextContents(); console.log(`Pagination buttons present: [${allPageBtns.join(', ')}]`); const nextBtn = page.locator('button.border-2.rounded-lg.font-medium') .filter({ hasText: new RegExp(`^${nextPage}$`) }) .first(); if (!(await nextBtn.isVisible({ timeout: 2000 }).catch(() => false))) { console.log(`No page ${nextPage} button - reached last page`); keepPaging = false; break; } await nextBtn.click(); await page.waitForTimeout(2000); // give the next page's data time to load // Poll until the first row's ID actually changes (confirms page advanced) let changed = false; for (let w = 0; w < 30; w++) { const nkCells = await dataTable.locator('tbody tr.ant-table-row').first().locator('td').allTextContents().catch(() => [] as string[]); const nk = (nkCells[idIdx] || '').replace(/\D/g, '').trim(); if (nk && nk !== firstRowId) { changed = true; break; } await page.waitForTimeout(400); } if (!changed) { console.log(`Page did not advance to ${nextPage} - stopping`); keepPaging = false; break; } await page.waitForTimeout(500); // let all rows on the new page render currentPage = nextPage; } // De-duplicate just in case const unique = Array.from(new Set(expectedIds)); expectedIds.length = 0; expectedIds.push(...unique); console.log(`Captured ${expectedIds.length} unique refund IDs in range across ${currentPage} page(s)`); expect(expectedIds.length, 'At least one refund row should fall in range').toBeGreaterThan(0); // Return to page 1 so the export modal opens from a consistent state const page1Btn = page.locator('button.border-2.rounded-lg.font-medium') .filter({ hasText: /^1$/ }) .first(); if (await page1Btn.isVisible({ timeout: 2000 }).catch(() => false)) { await page1Btn.click(); await page.waitForTimeout(1000); } }); await test.step('Open Download report modal', async () => { const downloadButton = page.locator('button:has-text("Download report")').first(); await expect(downloadButton, 'Download report button should be visible').toBeVisible({ timeout: 5000 }); await downloadButton.click(); await page.waitForTimeout(1500); console.log('✓ Download report modal opened'); }); await test.step('Select CSV file format', async () => { const csvOption = page.locator('label:has-text("CSV"), .ant-radio-wrapper:has-text("CSV")').first(); const isVisible = await csvOption.isVisible().catch(() => false); if (isVisible) { await csvOption.click(); } else { // Fallback: click the radio input next to the CSV text await page.getByText('CSV', { exact: true }).click(); } await page.waitForTimeout(500); console.log('✓ Selected CSV format'); }); await test.step('Select date range matching refund data', async () => { const dateField = page.locator('[role="dialog"] input[date-range="start"], input[date-range="start"]').first(); await expect(dateField, 'Date range input should be visible').toBeVisible(); await dateField.click(); await page.waitForTimeout(1500); // Navigate the calendar back until the start cell is visible for (let i = 0; i < 15; i++) { const startCell = page.locator(`.ant-picker-cell[title="${startTitle}"]`).first(); if (await startCell.isVisible().catch(() => false)) break; await page.locator('.ant-picker-header-prev-btn').first().click(); await page.waitForTimeout(400); } await page.locator(`.ant-picker-cell[title="${startTitle}"]`).first().click(); await page.waitForTimeout(500); console.log('✓ Start date:', startTitle); await page.locator(`.ant-picker-cell[title="${endTitle}"]`).first().click(); await page.waitForTimeout(800); console.log('✓ End date:', endTitle); }); await test.step('Download CSV and verify contents against refund rows', async () => { const downloadPromise = page.waitForEvent('download', { timeout: 30000 }).catch(() => null); const modalDownloadButton = page.locator('[role="dialog"] button:has-text("Download")').first(); await expect(modalDownloadButton, 'Modal Download button should be visible').toBeVisible(); console.log('Clicking download button...'); await modalDownloadButton.click(); const download = await downloadPromise; expect(download, 'A file download should be triggered').not.toBeNull(); fileName = download!.suggestedFilename(); console.log('✓ Download captured! File name:', fileName); downloadSuccessful = true; // Save and read the CSV content const savePath = `test-results/${fileName}`; await download!.saveAs(savePath); const csv = fs.readFileSync(savePath, 'utf-8'); console.log(`CSV size: ${csv.length} chars; first line: ${csv.split('\n')[0]?.substring(0, 120)}`); expect(csv.length, 'CSV should not be empty').toBeGreaterThan(0); // Every refund ID shown in the table (within range) must be present in the CSV const missing = expectedIds.filter(id => !csv.includes(id)); console.log(`Verifying ${expectedIds.length} IDs in CSV; missing: ${missing.length ? missing.join(', ') : 'none'}`); expect(missing, `All table refund IDs should appear in the CSV (missing: ${missing.join(', ')})`).toHaveLength(0); console.log('✓ CSV contains all refund transactions from the table for the selected range'); }); console.log('Refund export test completed'); }); test('should paginate through refund list', async ({ page }) => { test.setTimeout(90000); feature('Refund Operations'); tag('functional'); severity('normal'); description('Test pagination controls navigate to page 2 and load a genuinely different set of rows'); console.log('Testing pagination...'); // Scope to the refunds data table via its unique "RRN" header, consistent // with the other tests in this file. const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const noData = await dataTable.locator('tbody', { hasText: 'No data' }).first().isVisible({ timeout: 5000 }).catch(() => false); test.skip(noData, 'No refund records available in this environment to paginate through'); await dataTable.locator('tbody tr.ant-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); const headers = await dataTable.locator('thead th').allTextContents(); const idIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'id'); expect(idIdx, 'ID column should exist').toBeGreaterThanOrEqual(0); let firstPageId = ''; await test.step('Capture first row ID on page 1', async () => { const cells = await dataTable.locator('tbody tr.ant-table-row').first().locator('td').allTextContents(); firstPageId = (cells[idIdx] || '').replace(/\D/g, '').trim(); expect(firstPageId, 'Page 1 first row should have a valid ID').toMatch(/^\d+$/); console.log('First row ID on page 1:', firstPageId); }); await test.step('Click page 2 button', async () => { // Page-number buttons carry the distinctive "border-2 rounded-lg font-medium" // classes; matching exact text avoids accidentally hitting unrelated buttons // that merely contain "2" (e.g. a date like "2026"). const page2Button = page.locator('button.border-2.rounded-lg.font-medium').filter({ hasText: /^2$/ }).first(); const hasPage2 = await page2Button.isVisible({ timeout: 10000 }).catch(() => false); test.skip(!hasPage2, 'Not enough refunds in this environment to require a second page'); await page2Button.scrollIntoViewIfNeeded(); console.log('Clicking page 2 button...'); await page2Button.click(); }); await test.step('Verify page 2 loaded a different first row', async () => { let secondPageId = ''; for (let attempt = 0; attempt < 30; attempt++) { const cells = await dataTable.locator('tbody tr.ant-table-row').first().locator('td').allTextContents().catch(() => [] as string[]); secondPageId = (cells[idIdx] || '').replace(/\D/g, '').trim(); if (secondPageId && secondPageId !== firstPageId) break; await page.waitForTimeout(400); } console.log('First row 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); }); console.log('Pagination test completed'); }); test('should filter refunds by Approved status and date range', async ({ page }) => { test.setTimeout(90000); feature('Refund Operations'); tag('functional'); severity('normal'); description('Test filtering refunds by Approved status and date range'); console.log('Testing status and date filter...'); let initialRowCount = 0; let startTitle = ''; let endTitle = ''; await test.step('Count initial refunds', async () => { initialRowCount = await page.locator('table tbody tr').count(); console.log('Initial refund count:', initialRowCount); }); await test.step('Determine target date range from the latest refund', async () => { // Anchor the range on the newest row actually in the table (same-month // 1st -> that day), rather than "today": this dev dataset's last refund // can be weeks old, so a range ending "today" often covers zero rows. const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const noData = await dataTable.locator('tbody', { hasText: 'No data' }).first().isVisible({ timeout: 5000 }).catch(() => false); test.skip(noData, 'No refund records available in this environment to filter by status/date'); await dataTable.locator('tbody tr.ant-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); const headers = await dataTable.locator('thead th').allTextContents(); const dateIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'date'); expect(dateIdx, 'A date column should exist').toBeGreaterThanOrEqual(0); const firstRowCells = await dataTable.locator('tbody tr.ant-table-row').first().locator('td').allTextContents(); const latestDate = toDate(firstRowCells[dateIdx] || ''); expect(latestDate, 'The newest row should have a parseable date').not.toBeNull(); startTitle = toISO(new Date(latestDate!.getFullYear(), latestDate!.getMonth(), 1)); endTitle = toISO(latestDate!); console.log(`Target range from latest refund (${firstRowCells[dateIdx]}): ${startTitle}..${endTitle}`); }); await test.step('Open filter controls', async () => { const filterButton = page.locator('button:has-text("Filter")').first(); const isVisible = await filterButton.isVisible().catch(() => false); if (isVisible) { await filterButton.click(); await page.waitForTimeout(1000); console.log('✓ Filter controls opened'); } else { console.log('Filter button not found'); } }); await test.step('Select Approved status only', async () => { // Every status checkbox (Initilized, Pending, Approved, Canceled, ...) is // checked by default, i.e. "show all statuses". To filter to Approved-only // we must uncheck every OTHER status and leave Approved checked - simply // clicking the Approved label toggles it OFF and leaves everything else // showing, which is the opposite of what this test needs. const statusSection = page.locator('article:has-text("Transaction Status")').locator('xpath=following-sibling::div[1]'); await expect(statusSection, 'Transaction Status section should be visible').toBeVisible({ timeout: 10000 }); const statusLabels = statusSection.locator('label.ant-checkbox-wrapper'); const count = await statusLabels.count(); expect(count, 'There should be status checkboxes').toBeGreaterThan(0); for (let i = 0; i < count; i++) { const label = statusLabels.nth(i); const text = (await label.innerText()).trim(); const isChecked = await label.locator('input[type="checkbox"]').isChecked(); if (text === 'Approved') { if (!isChecked) { console.log('Approved was unchecked, checking it...'); await label.click(); await page.waitForTimeout(300); } } else if (isChecked) { console.log(`Unchecking status: ${text}`); await label.click(); await page.waitForTimeout(300); } } console.log('✓ Only Approved status left checked'); }); await test.step('Select the target date range (anchored on the latest refund)', async () => { const dateField = page.locator('input[date-range="start"]').first(); await expect(dateField, 'Date range start input should be visible').toBeVisible({ timeout: 10000 }); await dateField.click(); await page.waitForTimeout(1500); const calendarVisible = await page.locator('.ant-picker-dropdown:not(.ant-picker-dropdown-hidden)').isVisible().catch(() => false); expect(calendarVisible, 'Date range calendar should open').toBe(true); // Navigate the calendar backward (bounded) until the target start date // is actually in view - the picker defaults to the current month, which // may be well after this dev dataset's latest refund. const prevBtn = page.locator('.ant-picker-header-prev-btn').first(); let found = false; for (let attempt = 0; attempt < 60; attempt++) { found = await page.locator(`.ant-picker-cell-in-view[title="${startTitle}"]:not(.ant-picker-cell-disabled)`).first().isVisible().catch(() => false); if (found) break; await prevBtn.click(); await page.waitForTimeout(300); } expect(found, `Target start date ${startTitle} should become available in the calendar`).toBe(true); console.log(`Range -> start: ${startTitle}, end: ${endTitle}`); await page.locator(`.ant-picker-cell-in-view[title="${startTitle}"]`).first().click(); await page.waitForTimeout(800); await page.locator(`.ant-picker-cell-in-view[title="${endTitle}"]`).first().click(); await page.waitForTimeout(1000); console.log('✓ Start and end dates selected'); }); await test.step('Apply filter', async () => { const applyButton = page.locator('button:has-text("Apply"), button:has-text("Search"), button[type="submit"]').first(); const isVisible = await applyButton.isVisible().catch(() => false); if (isVisible) { await applyButton.click(); await page.waitForTimeout(3000); console.log('✓ Filter applied'); } await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(500); }); await test.step('Assert every returned row is Approved and within the date range', async () => { const startDate = toDate(startTitle)!; const endDate = toDate(endTitle)!; endDate.setHours(23, 59, 59, 999); const dataTable = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); const headers = await dataTable.locator('thead th').allTextContents(); const statusIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'status'); const dateIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'date'); console.log(`Columns -> status:${statusIdx} date:${dateIdx} (headers: ${headers.join(' | ')})`); expect(statusIdx, 'A status column should exist').toBeGreaterThanOrEqual(0); expect(dateIdx, 'A date column should exist').toBeGreaterThanOrEqual(0); const rows = dataTable.locator('tbody tr.ant-table-row'); const rowCount = await rows.count(); console.log(`Validating ${rowCount} rows are Approved and within ${startTitle}..${endTitle}`); for (let i = 0; i < rowCount; i++) { const cellTexts = await rows.nth(i).locator('td').allTextContents(); const statusText = (cellTexts[statusIdx] || '').trim(); expect(statusText.toLowerCase(), `Row ${i + 1} status should be Approved (got "${statusText}")`).toBe('approved'); const rowDate = toDate(cellTexts[dateIdx] || ''); expect(rowDate, `Row ${i + 1} should have a valid date`).not.toBeNull(); const t = rowDate!.getTime(); expect( t >= startDate.getTime() && t <= endDate.getTime(), `Row ${i + 1} date ${cellTexts[dateIdx]} should be within ${startTitle}..${endTitle}` ).toBe(true); } console.log(`✓ All ${rowCount} returned rows are Approved and within range`); }); console.log('Status and date filter test completed'); }); });