import { Page, Locator } from '@playwright/test'; import { testLogger } from '../shared/utils/test-logger'; export interface TableColumn { name: string; selector: string; type: 'text' | 'number' | 'date' | 'boolean' | 'action'; sortable?: boolean; filterable?: boolean; } export interface TableRow { index: number; data: Record; cells: Map; } export interface TableOperation { name: string; selector: string; type: 'edit' | 'delete' | 'view' | 'custom'; } export class TableHelper { private page: Page; private tableSelector: string; private columns: Map = new Map(); private operations: Map = new Map(); constructor(page: Page, tableSelector: string = 'table') { this.page = page; this.tableSelector = tableSelector; testLogger.info(`TableHelper initialized for table: ${tableSelector}`); } setColumn(column: TableColumn): void { this.columns.set(column.name, column); testLogger.debug(`Column added: ${column.name}`); } setColumns(columns: TableColumn[]): void { columns.forEach(column => this.setColumn(column)); testLogger.debug(`${columns.length} columns added`); } setOperation(operation: TableOperation): void { this.operations.set(operation.name, operation); testLogger.debug(`Operation added: ${operation.name}`); } setOperations(operations: TableOperation[]): void { operations.forEach(operation => this.setOperation(operation)); testLogger.debug(`${operations.length} operations added`); } getColumn(name: string): TableColumn | undefined { return this.columns.get(name); } getAllColumns(): TableColumn[] { return Array.from(this.columns.values()); } async getRowCount(): Promise { testLogger.info('Getting row count'); const rowsLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`); const count = await rowsLocator.count(); testLogger.debug(`Row count: ${count}`); return count; } async getColumnCount(): Promise { testLogger.info('Getting column count'); const headersLocator = this.page.locator(`${this.tableSelector} thead th, ${this.tableSelector} .table-header th`); const count = await headersLocator.count(); testLogger.debug(`Column count: ${count}`); return count; } async getRow(index: number): Promise { testLogger.info(`Getting row: ${index}`); const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(index); const cells: Map = new Map(); const data: Record = {}; const columnEntries = Array.from(this.columns.entries()); for (const [columnName, column] of columnEntries) { const cellSelector = `td:nth-child(${this.getColumnIndex(columnName) + 1}), .table-cell:nth-child(${this.getColumnIndex(columnName) + 1})`; const cellLocator = rowLocator.locator(cellSelector); cells.set(columnName, cellLocator); let cellValue: string; switch (column.type) { case 'text': cellValue = await cellLocator.textContent() || ''; break; case 'number': cellValue = await cellLocator.textContent() || '0'; break; case 'date': cellValue = await cellLocator.textContent() || ''; break; case 'boolean': const checkboxLocator = cellLocator.locator('input[type="checkbox"]'); cellValue = String(await checkboxLocator.isChecked()); break; case 'action': cellValue = 'action'; break; default: cellValue = await cellLocator.textContent() || ''; } data[columnName] = cellValue.trim(); } const row: TableRow = { index, data, cells }; testLogger.debug(`Row retrieved: ${index}`); return row; } async getAllRows(): Promise { testLogger.info('Getting all rows'); const rowCount = await this.getRowCount(); const rows: TableRow[] = []; for (let i = 0; i < rowCount; i++) { rows.push(await this.getRow(i)); } testLogger.info(`All rows retrieved: ${rows.length}`); return rows; } async getTableData(): Promise[]> { testLogger.info('Getting table data'); const rows = await this.getAllRows(); const data: Record[] = rows.map(row => row.data); testLogger.debug(`Table data retrieved: ${JSON.stringify(data)}`); return data; } async findRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Finding row by column: ${columnName} = ${value}`); const rows = await this.getAllRows(); const foundRow = rows.find(row => row.data[columnName] === value); if (foundRow) { testLogger.info(`Row found: ${foundRow.index}`); } else { testLogger.warn(`Row not found: ${columnName} = ${value}`); } return foundRow; } async findRowsByColumn(columnName: string, value: string): Promise { testLogger.info(`Finding rows by column: ${columnName} = ${value}`); const rows = await this.getAllRows(); const foundRows = rows.filter(row => row.data[columnName] === value); testLogger.info(`Rows found: ${foundRows.length}`); return foundRows; } async filterByColumn(columnName: string, value: string): Promise { testLogger.info(`Filtering by column: ${columnName} = ${value}`); const column = this.columns.get(columnName); if (!column) { throw new Error(`Column not found: ${columnName}`); } if (!column.filterable) { throw new Error(`Column is not filterable: ${columnName}`); } const filterSelector = `${this.tableSelector} thead th:nth-child(${this.getColumnIndex(columnName) + 1}) .filter-input, ${this.tableSelector} .table-header th:nth-child(${this.getColumnIndex(columnName) + 1}) .filter-input`; const filterLocator = this.page.locator(filterSelector); await filterLocator.waitFor({ state: 'visible' }); await filterLocator.fill(value); await this.page.waitForTimeout(500); testLogger.info(`Filter applied: ${columnName} = ${value}`); } async clearFilter(columnName: string): Promise { testLogger.info(`Clearing filter: ${columnName}`); const column = this.columns.get(columnName); if (!column) { throw new Error(`Column not found: ${columnName}`); } if (!column.filterable) { throw new Error(`Column is not filterable: ${columnName}`); } const filterSelector = `${this.tableSelector} thead th:nth-child(${this.getColumnIndex(columnName) + 1}) .filter-input, ${this.tableSelector} .table-header th:nth-child(${this.getColumnIndex(columnName) + 1}) .filter-input`; const filterLocator = this.page.locator(filterSelector); await filterLocator.clear(); await this.page.waitForTimeout(500); testLogger.info(`Filter cleared: ${columnName}`); } async clearAllFilters(): Promise { testLogger.info('Clearing all filters'); const columnKeys = Array.from(this.columns.keys()); for (const columnName of columnKeys) { const column = this.columns.get(columnName); if (column?.filterable) { try { await this.clearFilter(columnName); } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.warn(`Failed to clear filter: ${columnName}`, { error: errorObj.message }); } } } testLogger.info('All filters cleared'); } async sortByColumn(columnName: string, order: 'asc' | 'desc' = 'asc'): Promise { testLogger.info(`Sorting by column: ${columnName} (${order})`); const column = this.columns.get(columnName); if (!column) { throw new Error(`Column not found: ${columnName}`); } if (!column.sortable) { throw new Error(`Column is not sortable: ${columnName}`); } const sortSelector = `${this.tableSelector} thead th:nth-child(${this.getColumnIndex(columnName) + 1}) .sort-icon, ${this.tableSelector} .table-header th:nth-child(${this.getColumnIndex(columnName) + 1}) .sort-icon`; const sortLocator = this.page.locator(sortSelector); await sortLocator.waitFor({ state: 'visible' }); await sortLocator.click(); await this.page.waitForTimeout(500); const currentOrder = await this.getSortOrder(columnName); if (currentOrder !== order) { await sortLocator.click(); await this.page.waitForTimeout(500); } testLogger.info(`Sorted by: ${columnName} (${order})`); } async getSortOrder(columnName: string): Promise<'asc' | 'desc' | null> { const column = this.columns.get(columnName); if (!column) { throw new Error(`Column not found: ${columnName}`); } const sortSelector = `${this.tableSelector} thead th:nth-child(${this.getColumnIndex(columnName) + 1})`; const sortLocator = this.page.locator(sortSelector); const classList = await sortLocator.getAttribute('class') || ''; if (classList.includes('asc')) { return 'asc'; } else if (classList.includes('desc')) { return 'desc'; } return null; } async clickRow(index: number): Promise { testLogger.info(`Clicking row: ${index}`); const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(index); await rowLocator.waitFor({ state: 'visible' }); await rowLocator.click(); testLogger.info(`Row clicked: ${index}`); } async clickRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Clicking row by column: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(row.index); await rowLocator.click(); testLogger.info(`Row clicked: ${row.index}`); } async performOperation(rowIndex: number, operationName: string): Promise { testLogger.info(`Performing operation: ${operationName} on row: ${rowIndex}`); const operation = this.operations.get(operationName); if (!operation) { throw new Error(`Operation not found: ${operationName}`); } const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(rowIndex); const operationLocator = rowLocator.locator(operation.selector); await operationLocator.waitFor({ state: 'visible' }); await operationLocator.click(); testLogger.info(`Operation performed: ${operationName} on row: ${rowIndex}`); } async performOperationByColumn(columnName: string, value: string, operationName: string): Promise { testLogger.info(`Performing operation: ${operationName} on row: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } await this.performOperation(row.index, operationName); } async editRow(rowIndex: number): Promise { testLogger.info(`Editing row: ${rowIndex}`); const editOperation = this.operations.get('edit'); if (!editOperation) { throw new Error('Edit operation not found'); } await this.performOperation(rowIndex, 'edit'); testLogger.info(`Row edited: ${rowIndex}`); } async editRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Editing row by column: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } await this.editRow(row.index); } async deleteRow(rowIndex: number): Promise { testLogger.info(`Deleting row: ${rowIndex}`); const deleteOperation = this.operations.get('delete'); if (!deleteOperation) { throw new Error('Delete operation not found'); } await this.performOperation(rowIndex, 'delete'); testLogger.info(`Row deleted: ${rowIndex}`); } async deleteRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Deleting row by column: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } await this.deleteRow(row.index); } async viewRow(rowIndex: number): Promise { testLogger.info(`Viewing row: ${rowIndex}`); const viewOperation = this.operations.get('view'); if (!viewOperation) { throw new Error('View operation not found'); } await this.performOperation(rowIndex, 'view'); testLogger.info(`Row viewed: ${rowIndex}`); } async viewRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Viewing row by column: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } await this.viewRow(row.index); } async selectRow(rowIndex: number): Promise { testLogger.info(`Selecting row: ${rowIndex}`); const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(rowIndex); const checkboxLocator = rowLocator.locator('input[type="checkbox"]'); await checkboxLocator.waitFor({ state: 'visible' }); await checkboxLocator.check(); testLogger.info(`Row selected: ${rowIndex}`); } async selectRowByColumn(columnName: string, value: string): Promise { testLogger.info(`Selecting row by column: ${columnName} = ${value}`); const row = await this.findRowByColumn(columnName, value); if (!row) { throw new Error(`Row not found: ${columnName} = ${value}`); } await this.selectRow(row.index); } async selectAllRows(): Promise { testLogger.info('Selecting all rows'); const selectAllLocator = this.page.locator(`${this.tableSelector} thead input[type="checkbox"], ${this.tableSelector} .table-header input[type="checkbox"]`); await selectAllLocator.waitFor({ state: 'visible' }); await selectAllLocator.check(); testLogger.info('All rows selected'); } async deselectRow(rowIndex: number): Promise { testLogger.info(`Deselecting row: ${rowIndex}`); const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(rowIndex); const checkboxLocator = rowLocator.locator('input[type="checkbox"]'); await checkboxLocator.waitFor({ state: 'visible' }); await checkboxLocator.uncheck(); testLogger.info(`Row deselected: ${rowIndex}`); } async deselectAllRows(): Promise { testLogger.info('Deselecting all rows'); const selectAllLocator = this.page.locator(`${this.tableSelector} thead input[type="checkbox"], ${this.tableSelector} .table-header input[type="checkbox"]`); await selectAllLocator.waitFor({ state: 'visible' }); await selectAllLocator.uncheck(); testLogger.info('All rows deselected'); } async getSelectedRows(): Promise { testLogger.info('Getting selected rows'); const rows = await this.getAllRows(); const selectedRows: TableRow[] = []; for (const row of rows) { const checkboxLocator = this.page.locator(`${this.tableSelector} tbody tr:nth-child(${row.index + 1}) input[type="checkbox"], ${this.tableSelector} .table-row:nth-child(${row.index + 1}) input[type="checkbox"]`); if (await checkboxLocator.isChecked()) { selectedRows.push(row); } } testLogger.info(`Selected rows: ${selectedRows.length}`); return selectedRows; } async goToPage(pageNumber: number): Promise { testLogger.info(`Going to page: ${pageNumber}`); const pageSelector = `${this.tableSelector} .pagination .page-item[data-page="${pageNumber}"], ${this.tableSelector} .pagination button[data-page="${pageNumber}"]`; const pageLocator = this.page.locator(pageSelector); await pageLocator.waitFor({ state: 'visible' }); await pageLocator.click(); await this.page.waitForTimeout(500); testLogger.info(`Page changed: ${pageNumber}`); } async nextPage(): Promise { testLogger.info('Going to next page'); const nextSelector = `${this.tableSelector} .pagination .next, ${this.tableSelector} .pagination button[aria-label="Next"]`; const nextLocator = this.page.locator(nextSelector); await nextLocator.waitFor({ state: 'visible' }); await nextLocator.click(); await this.page.waitForTimeout(500); testLogger.info('Next page loaded'); } async previousPage(): Promise { testLogger.info('Going to previous page'); const prevSelector = `${this.tableSelector} .pagination .prev, ${this.tableSelector} .pagination button[aria-label="Previous"]`; const prevLocator = this.page.locator(prevSelector); await prevLocator.waitFor({ state: 'visible' }); await prevLocator.click(); await this.page.waitForTimeout(500); testLogger.info('Previous page loaded'); } async getCurrentPage(): Promise { testLogger.info('Getting current page'); const activePageSelector = `${this.tableSelector} .pagination .page-item.active, ${this.tableSelector} .pagination button.active`; const activePageLocator = this.page.locator(activePageSelector); const pageNumber = await activePageLocator.getAttribute('data-page'); testLogger.debug(`Current page: ${pageNumber}`); return parseInt(pageNumber || '1', 10); } async getTotalPages(): Promise { testLogger.info('Getting total pages'); const pagesSelector = `${this.tableSelector} .pagination .page-item, ${this.tableSelector} .pagination button`; const pagesLocator = this.page.locator(pagesSelector); const count = await pagesLocator.count(); testLogger.debug(`Total pages: ${count}`); return count; } async waitForData(timeout: number = 5000): Promise { testLogger.info(`Waiting for table data (${timeout}ms)`); const rowsLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).first(); await rowsLocator.waitFor({ state: 'visible', timeout }); testLogger.info('Table data loaded'); } async isEmpty(): Promise { testLogger.info('Checking if table is empty'); const rowCount = await this.getRowCount(); const isEmpty = rowCount === 0; testLogger.debug(`Table is empty: ${isEmpty}`); return isEmpty; } async hasData(): Promise { const isEmpty = await this.isEmpty(); return !isEmpty; } async refresh(): Promise { testLogger.info('Refreshing table'); const refreshSelector = `${this.tableSelector} .refresh-button, ${this.tableSelector} button[aria-label="Refresh"]`; const refreshLocator = this.page.locator(refreshSelector); if (await refreshLocator.isVisible()) { await refreshLocator.click(); await this.page.waitForTimeout(500); testLogger.info('Table refreshed'); } else { testLogger.warn('Refresh button not found'); } } private getColumnIndex(columnName: string): number { const columns = Array.from(this.columns.keys()); return columns.indexOf(columnName); } }