Files
everything-is-suitable/everything-is-suitable-test/e2e/helpers/table-helper.ts
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

623 lines
20 KiB
TypeScript

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<string, string>;
cells: Map<string, Locator>;
}
export interface TableOperation {
name: string;
selector: string;
type: 'edit' | 'delete' | 'view' | 'custom';
}
export class TableHelper {
private page: Page;
private tableSelector: string;
private columns: Map<string, TableColumn> = new Map();
private operations: Map<string, TableOperation> = 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<number> {
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<number> {
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<TableRow> {
testLogger.info(`Getting row: ${index}`);
const rowLocator = this.page.locator(`${this.tableSelector} tbody tr, ${this.tableSelector} .table-row`).nth(index);
const cells: Map<string, Locator> = new Map();
const data: Record<string, string> = {};
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<TableRow[]> {
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<Record<string, string>[]> {
testLogger.info('Getting table data');
const rows = await this.getAllRows();
const data: Record<string, string>[] = rows.map(row => row.data);
testLogger.debug(`Table data retrieved: ${JSON.stringify(data)}`);
return data;
}
async findRowByColumn(columnName: string, value: string): Promise<TableRow | undefined> {
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<TableRow[]> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<TableRow[]> {
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<void> {
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<void> {
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<void> {
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<number> {
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<number> {
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<void> {
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<boolean> {
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<boolean> {
const isEmpty = await this.isEmpty();
return !isEmpty;
}
async refresh(): Promise<void> {
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);
}
}