Extract PassDashboard Playwright e2e suite into standalone repo
Some checks failed
E2E Tests / test (push) Has been cancelled

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>
This commit is contained in:
2026-08-03 15:50:50 +03:00
commit 7bf4343f88
23 changed files with 6568 additions and 0 deletions

836
tests/refunds.spec.ts Normal file
View File

@@ -0,0 +1,836 @@
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<string, string> = {
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<string, string> = { 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');
});
});