feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,308 @@
import { APIRequestContext, APIResponse } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
export interface APIRequestOptions {
headers?: Record<string, string>;
params?: Record<string, string | number>;
data?: any;
timeout?: number;
}
export interface APIResponseData<T = any> {
success: boolean;
code: string;
message: string;
data: T;
timestamp: number;
}
export class APIHelper {
private apiContext: APIRequestContext;
private baseURL: string;
private token: string | null = null;
private tokenType: string = 'Bearer';
constructor(apiContext: APIRequestContext, baseURL: string) {
this.apiContext = apiContext;
this.baseURL = baseURL;
testLogger.info(`APIHelper initialized with baseURL: ${baseURL}`);
}
setToken(token: string, tokenType: string = 'Bearer'): void {
this.token = token;
this.tokenType = tokenType;
testLogger.info('Token set successfully');
}
clearToken(): void {
this.token = null;
testLogger.info('Token cleared');
}
private buildHeaders(options?: APIRequestOptions): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options?.headers
};
if (this.token) {
headers['Authorization'] = `${this.tokenType} ${this.token}`;
}
return headers;
}
private buildURL(endpoint: string, params?: Record<string, string | number>): string {
let url = `${this.baseURL}${endpoint}`;
if (params && Object.keys(params).length > 0) {
const queryString = new URLSearchParams(
Object.entries(params).reduce((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {} as Record<string, string>)
).toString();
url += `?${queryString}`;
}
return url;
}
private async handleResponse<T>(response: APIResponse, endpoint: string): Promise<APIResponseData<T>> {
const statusCode = response.status();
const contentType = response.headers()['content-type'] || '';
testLogger.debug(`API Response: ${endpoint} - Status: ${statusCode}, ContentType: ${contentType}`);
if (!response.ok()) {
const errorText = await response.text();
testLogger.error(`API Error: ${endpoint} - Status: ${statusCode}`, new Error(errorText));
throw new Error(`API request failed: ${statusCode} - ${errorText}`);
}
let responseData: any;
if (contentType.includes('application/json')) {
responseData = await response.json();
} else {
responseData = await response.text();
}
return responseData;
}
async get<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`GET ${url}`);
const startTime = Date.now();
const response = await this.apiContext.get(url, {
headers,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`GET ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async post<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`POST ${url}`);
const startTime = Date.now();
const response = await this.apiContext.post(url, {
headers,
data: options?.data,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`POST ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async put<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`PUT ${url}`);
const startTime = Date.now();
const response = await this.apiContext.put(url, {
headers,
data: options?.data,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`PUT ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async delete<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`DELETE ${url}`);
const startTime = Date.now();
const response = await this.apiContext.delete(url, {
headers,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`DELETE ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async patch<T = any>(endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`PATCH ${url}`);
const startTime = Date.now();
const response = await this.apiContext.fetch(url, {
method: 'PATCH',
headers,
data: options?.data,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`PATCH ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async upload<T = any>(endpoint: string, formData: any, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers: Record<string, string> = {
...options?.headers
};
if (this.token) {
headers['Authorization'] = `${this.tokenType} ${this.token}`;
}
testLogger.info(`UPLOAD ${url}`);
const startTime = Date.now();
const response = await this.apiContext.post(url, {
headers,
multipart: formData,
timeout: options?.timeout || 60000
});
const duration = Date.now() - startTime;
testLogger.info(`UPLOAD ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async download(endpoint: string, options?: APIRequestOptions): Promise<Buffer> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`DOWNLOAD ${url}`);
const startTime = Date.now();
const response = await this.apiContext.get(url, {
headers,
timeout: options?.timeout || 60000
});
const duration = Date.now() - startTime;
testLogger.info(`DOWNLOAD ${url} - ${duration}ms`);
if (!response.ok()) {
throw new Error(`Download failed: ${response.status()}`);
}
return response.body();
}
async request<T = any>(method: string, endpoint: string, options?: APIRequestOptions): Promise<APIResponseData<T>> {
const url = this.buildURL(endpoint, options?.params);
const headers = this.buildHeaders(options);
testLogger.info(`${method.toUpperCase()} ${url}`);
const startTime = Date.now();
const response = await this.apiContext.fetch(url, {
method,
headers,
data: options?.data,
timeout: options?.timeout || 30000
});
const duration = Date.now() - startTime;
testLogger.info(`${method.toUpperCase()} ${url} - ${duration}ms`);
return this.handleResponse<T>(response, endpoint);
}
async login(username: string, password: string): Promise<APIResponseData<{ token: string }>> {
testLogger.info(`Login attempt for user: ${username}`);
const response = await this.post('/auth/login', {
data: { username, password }
});
if (response.success && response.data.token) {
this.setToken(response.data.token);
testLogger.info('Login successful');
}
return response;
}
async logout(): Promise<APIResponseData<any>> {
testLogger.info('Logout');
const response = await this.post('/auth/logout');
this.clearToken();
testLogger.info('Logout successful');
return response;
}
async refresh(): Promise<APIResponseData<{ token: string }>> {
testLogger.info('Token refresh');
if (!this.token) {
throw new Error('No token to refresh');
}
const response = await this.post('/auth/refresh');
if (response.success && response.data.token) {
this.setToken(response.data.token);
testLogger.info('Token refreshed successfully');
}
return response;
}
verifyToken(): boolean {
return this.token !== null && this.token.length > 0;
}
getToken(): string | null {
return this.token;
}
getBaseURL(): string {
return this.baseURL;
}
}
@@ -0,0 +1,157 @@
import { expect } from '@playwright/test';
export class AssertionHelper {
async assertElementVisible(page: Page, selector: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toBeVisible();
}
async assertElementHidden(page: Page, selector: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toBeHidden();
}
async assertElementText(page: Page, selector: string, expectedText: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toHaveText(expectedText);
}
async assertElementContainsText(page: Page, selector: string, expectedText: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toContainText(expectedText);
}
async assertElementValue(page: Page, selector: string, expectedValue: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toHaveValue(expectedValue);
}
async assertElementEnabled(page: Page, selector: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toBeEnabled();
}
async assertElementDisabled(page: Page, selector: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toBeDisabled();
}
async assertElementChecked(page: Page, selector: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toBeChecked();
}
async assertElementCount(page: Page, selector: string, expectedCount: number, message?: string): Promise<void> {
const elements = page.locator(selector);
const count = await elements.count();
expect(count, message).toBe(expectedCount);
}
async assertElementCountGreaterThan(page: Page, selector: string, minCount: number, message?: string): Promise<void> {
const elements = page.locator(selector);
const count = await elements.count();
expect(count, message).toBeGreaterThan(minCount);
}
async assertElementCountLessThan(page: Page, selector: string, maxCount: number, message?: string): Promise<void> {
const elements = page.locator(selector);
const count = await elements.count();
expect(count, message).toBeLessThan(maxCount);
}
async assertURL(page: Page, expectedURL: string | RegExp, message?: string): Promise<void> {
await expect(page, message).toHaveURL(expectedURL);
}
async assertTitle(page: Page, expectedTitle: string, message?: string): Promise<void> {
await expect(page, message).toHaveTitle(expectedTitle);
}
async assertAttributeValue(page: Page, selector: string, attribute: string, expectedValue: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toHaveAttribute(attribute, expectedValue);
}
async assertCSSClass(page: Page, selector: string, className: string, message?: string): Promise<void> {
const element = page.locator(selector);
await expect(element, message).toHaveClass(new RegExp(className));
}
async assertAPISuccess(response: APIResponse, message?: string): Promise<void> {
expect(response.status(), message).toBe(200);
const body = await response.json();
expect(body.code, message).toBe('200');
}
async assertAPIResponseCode(response: APIResponse, expectedCode: string, message?: string): Promise<void> {
const body = await response.json();
expect(body.code, message).toBe(expectedCode);
}
async assertAPIResponseData(response: APIResponse, expectedData: any, message?: string): Promise<void> {
const body = await response.json();
expect(body.data, message).toEqual(expectedData);
}
async assertTableData(page: Page, tableSelector: string, expectedData: any[][], message?: string): Promise<void> {
const tableHelper = new TableHelper(page);
const isValid = await tableHelper.validateTableData(tableSelector, expectedData);
expect(isValid, message).toBe(true);
}
async assertFormValid(page: Page, message?: string): Promise<void> {
const formHelper = new FormHelper(page);
const isValid = await formHelper.validateForm();
expect(isValid, message).toBe(true);
}
async assertFormInvalid(page: Page, message?: string): Promise<void> {
const formHelper = new FormHelper(page);
const isValid = await formHelper.validateForm();
expect(isValid, message).toBe(false);
}
async assertSuccessMessage(page: Page, message?: string): Promise<void> {
const successElement = page.locator('.success-message, .ant-message-success');
await expect(successElement, message).toBeVisible();
}
async assertErrorMessage(page: Page, expectedMessage?: string, message?: string): Promise<void> {
const errorElement = page.locator('.error-message, .ant-message-error');
await expect(errorElement, message).toBeVisible();
if (expectedMessage) {
await expect(errorElement, message).toContainText(expectedMessage);
}
}
async assertLoading(page: Page, message?: string): Promise<void> {
const loadingElement = page.locator('.loading, .ant-spin, .loading-indicator');
await expect(loadingElement, message).toBeVisible();
}
async assertNotLoading(page: Page, message?: string): Promise<void> {
const loadingElement = page.locator('.loading, .ant-spin, .loading-indicator');
await expect(loadingElement, message).toBeHidden();
}
async assertModalVisible(page: Page, message?: string): Promise<void> {
const modalElement = page.locator('.modal, .ant-modal, .dialog');
await expect(modalElement, message).toBeVisible();
}
async assertModalHidden(page: Page, message?: string): Promise<void> {
const modalElement = page.locator('.modal, .ant-modal, .dialog');
await expect(modalElement, message).toBeHidden();
}
async assertToastVisible(page: Page, message?: string): Promise<void> {
const toastElement = page.locator('.toast, .ant-message');
await expect(toastElement, message).toBeVisible();
}
async assertToastHidden(page: Page, message?: string): Promise<void> {
const toastElement = page.locator('.toast, .ant-message');
await expect(toastElement, message).toBeHidden();
}
}
@@ -0,0 +1,516 @@
import { Page, Locator } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
export interface FormField {
name: string;
selector: string;
type: 'text' | 'password' | 'email' | 'number' | 'date' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'file';
required: boolean;
value?: any;
options?: Array<{ label: string; value: string }>;
}
export interface FormValidation {
field: string;
valid: boolean;
message?: string;
}
export class FormHelper {
private page: Page;
private formSelector: string;
private fields: Map<string, FormField> = new Map();
constructor(page: Page, formSelector: string = 'form') {
this.page = page;
this.formSelector = formSelector;
testLogger.info(`FormHelper initialized for form: ${formSelector}`);
}
setField(field: FormField): void {
this.fields.set(field.name, field);
testLogger.debug(`Field added: ${field.name}`);
}
setFields(fields: FormField[]): void {
fields.forEach(field => this.setField(field));
testLogger.debug(`${fields.length} fields added`);
}
getField(name: string): FormField | undefined {
return this.fields.get(name);
}
getAllFields(): FormField[] {
return Array.from(this.fields.values());
}
async fillField(name: string, value: any): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
testLogger.info(`Filling field: ${name} with value: ${value}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.waitFor({ state: 'visible' });
await locator.scrollIntoViewIfNeeded();
switch (field.type) {
case 'text':
case 'email':
case 'password':
case 'number':
case 'date':
await locator.fill(String(value));
break;
case 'textarea':
await locator.fill(String(value));
break;
case 'select':
await locator.selectOption(value);
break;
case 'checkbox':
if (value) {
await locator.check();
} else {
await locator.uncheck();
}
break;
case 'radio':
const radioLocator = this.page.locator(`${selector}[value="${value}"]`);
await radioLocator.check();
break;
case 'file':
await locator.setInputFiles(value);
break;
default:
throw new Error(`Unsupported field type: ${field.type}`);
}
testLogger.debug(`Field filled: ${name}`);
}
async fillForm(data: Record<string, any>): Promise<void> {
testLogger.info(`Filling form with ${Object.keys(data).length} fields`);
for (const [name, value] of Object.entries(data)) {
await this.fillField(name, value);
}
testLogger.info('Form filled successfully');
}
async clearField(name: string): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
testLogger.info(`Clearing field: ${name}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.clear();
testLogger.debug(`Field cleared: ${name}`);
}
async clearForm(): Promise<void> {
testLogger.info('Clearing form');
const fieldEntries = Array.from(this.fields.entries());
for (const [name] of fieldEntries) {
try {
await this.clearField(name);
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.warn(`Failed to clear field: ${name}`, { error: errorObj.message });
}
}
testLogger.info('Form cleared successfully');
}
async getFieldValue(name: string): Promise<string> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.waitFor({ state: 'visible' });
let value: string;
switch (field.type) {
case 'text':
case 'email':
case 'password':
case 'number':
case 'date':
case 'textarea':
value = await locator.inputValue();
break;
case 'select':
value = await locator.inputValue();
break;
case 'checkbox':
value = String(await locator.isChecked());
break;
case 'radio':
const radioLocator = this.page.locator(`${selector}:checked`);
value = await radioLocator.inputValue();
break;
case 'file':
value = await locator.inputValue();
break;
default:
throw new Error(`Unsupported field type: ${field.type}`);
}
testLogger.debug(`Field value retrieved: ${name} = ${value}`);
return value;
}
async getFormData(): Promise<Record<string, string>> {
testLogger.info('Getting form data');
const data: Record<string, string> = {};
const fieldEntries = Array.from(this.fields.entries());
for (const [name] of fieldEntries) {
try {
data[name] = await this.getFieldValue(name);
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.warn(`Failed to get field value: ${name}`, { error: errorObj.message });
}
}
testLogger.debug(`Form data retrieved: ${JSON.stringify(data)}`);
return data;
}
async submit(): Promise<void> {
testLogger.info('Submitting form');
const submitButton = this.page.locator(`${this.formSelector} button[type="submit"], ${this.formSelector} input[type="submit"]`);
await submitButton.waitFor({ state: 'visible' });
await submitButton.scrollIntoViewIfNeeded();
await submitButton.click();
testLogger.info('Form submitted');
}
async reset(): Promise<void> {
testLogger.info('Resetting form');
const resetButton = this.page.locator(`${this.formSelector} button[type="reset"], ${this.formSelector} input[type="reset"]`);
if (await resetButton.isVisible()) {
await resetButton.click();
testLogger.info('Form reset');
} else {
await this.clearForm();
testLogger.info('Form cleared (no reset button)');
}
}
async validate(): Promise<FormValidation[]> {
testLogger.info('Validating form');
const validations: FormValidation[] = [];
const fieldEntries = Array.from(this.fields.entries());
for (const [name, field] of fieldEntries) {
const validation = await this.validateField(name);
validations.push(validation);
}
const invalidFields = validations.filter(v => !v.valid);
if (invalidFields.length > 0) {
testLogger.warn(`Form validation failed: ${invalidFields.length} fields invalid`);
} else {
testLogger.info('Form validation passed');
}
return validations;
}
async validateField(name: string): Promise<FormValidation> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
const validation: FormValidation = {
field: name,
valid: true,
message: undefined
};
const selector = field.selector;
const locator = this.page.locator(selector);
if (field.required) {
const value = await locator.inputValue();
if (!value || value.trim() === '') {
validation.valid = false;
validation.message = 'Field is required';
}
}
if (field.type === 'email') {
const value = await locator.inputValue();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
validation.valid = false;
validation.message = 'Invalid email format';
}
}
if (field.type === 'number') {
const value = await locator.inputValue();
if (value && isNaN(Number(value))) {
validation.valid = false;
validation.message = 'Invalid number format';
}
}
testLogger.debug(`Field validation: ${name} - ${validation.valid ? 'valid' : 'invalid'}`);
return validation;
}
async getErrorMessages(): Promise<Record<string, string>> {
testLogger.info('Getting error messages');
const errors: Record<string, string> = {};
const fieldEntries = Array.from(this.fields.entries());
for (const [name, field] of fieldEntries) {
const errorSelector = `${field.selector} + .error-message, ${field.selector} ~ .error-message, ${field.selector}[aria-invalid="true"]`;
const errorLocator = this.page.locator(errorSelector);
if (await errorLocator.isVisible()) {
errors[name] = await errorLocator.textContent() || '';
}
}
testLogger.debug(`Error messages retrieved: ${JSON.stringify(errors)}`);
return errors;
}
async hasErrors(): Promise<boolean> {
const errors = await this.getErrorMessages();
return Object.keys(errors).length > 0;
}
async waitForValidation(timeout: number = 5000): Promise<void> {
testLogger.info(`Waiting for validation (${timeout}ms)`);
await this.page.waitForTimeout(timeout);
const validations = await this.validate();
const hasInvalidFields = validations.some(v => !v.valid);
if (hasInvalidFields) {
throw new Error('Form validation failed');
}
testLogger.info('Validation passed');
}
async isFieldVisible(name: string): Promise<boolean> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
const selector = field.selector;
const locator = this.page.locator(selector);
return await locator.isVisible();
}
async isFieldEnabled(name: string): Promise<boolean> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
const selector = field.selector;
const locator = this.page.locator(selector);
return await locator.isEnabled();
}
async isFieldRequired(name: string): Promise<boolean> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
return field.required;
}
async focusField(name: string): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
testLogger.info(`Focusing field: ${name}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.focus();
testLogger.debug(`Field focused: ${name}`);
}
async blurField(name: string): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
testLogger.info(`Blurring field: ${name}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.blur();
testLogger.debug(`Field blurred: ${name}`);
}
async selectOption(name: string, option: string): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
if (field.type !== 'select') {
throw new Error(`Field is not a select: ${name}`);
}
testLogger.info(`Selecting option: ${name} = ${option}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.selectOption({ label: option });
testLogger.debug(`Option selected: ${name} = ${option}`);
}
async checkCheckbox(name: string, checked: boolean = true): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
if (field.type !== 'checkbox') {
throw new Error(`Field is not a checkbox: ${name}`);
}
testLogger.info(`Checking checkbox: ${name} = ${checked}`);
const selector = field.selector;
const locator = this.page.locator(selector);
if (checked) {
await locator.check();
} else {
await locator.uncheck();
}
testLogger.debug(`Checkbox checked: ${name} = ${checked}`);
}
async isCheckboxChecked(name: string): Promise<boolean> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
if (field.type !== 'checkbox') {
throw new Error(`Field is not a checkbox: ${name}`);
}
const selector = field.selector;
const locator = this.page.locator(selector);
return await locator.isChecked();
}
async uploadFile(name: string, filePath: string): Promise<void> {
const field = this.fields.get(name);
if (!field) {
throw new Error(`Field not found: ${name}`);
}
if (field.type !== 'file') {
throw new Error(`Field is not a file input: ${name}`);
}
testLogger.info(`Uploading file: ${name} = ${filePath}`);
const selector = field.selector;
const locator = this.page.locator(selector);
await locator.setInputFiles(filePath);
testLogger.debug(`File uploaded: ${name} = ${filePath}`);
}
async waitForFormReady(timeout: number = 5000): Promise<void> {
testLogger.info(`Waiting for form to be ready (${timeout}ms)`);
const formLocator = this.page.locator(this.formSelector);
await formLocator.waitFor({ state: 'visible', timeout });
const fieldEntries = Array.from(this.fields.entries());
for (const [name, field] of fieldEntries) {
const fieldLocator = this.page.locator(field.selector);
try {
await fieldLocator.waitFor({ state: 'visible', timeout: 1000 });
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.warn(`Field not visible: ${name}`, { error: errorObj.message });
}
}
testLogger.info('Form is ready');
}
}
@@ -0,0 +1,393 @@
import { Page, Locator, PageScreenshotOptions } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
import * as path from 'path';
import * as fs from 'fs';
export interface ScreenshotConfig {
outputDir: string;
filename: string;
fullPage: boolean;
quality?: number;
type: 'png' | 'jpeg';
timeout: number;
}
export interface ScreenshotMetadata {
filename: string;
path: string;
timestamp: string;
testName?: string;
description?: string;
}
export class ScreenshotHelper {
private page: Page;
private outputDir: string;
private defaultConfig: Partial<ScreenshotConfig>;
private screenshots: Map<string, ScreenshotMetadata> = new Map();
constructor(page: Page, outputDir: string = 'test-results/screenshots') {
this.page = page;
this.outputDir = outputDir;
this.defaultConfig = {
outputDir,
fullPage: false,
type: 'png',
timeout: 5000
};
this.ensureOutputDir();
testLogger.info(`ScreenshotHelper initialized with output dir: ${outputDir}`);
}
setDefaultConfig(config: Partial<ScreenshotConfig>): void {
this.defaultConfig = { ...this.defaultConfig, ...config };
testLogger.debug('Default screenshot config updated');
}
async capture(config: Partial<ScreenshotConfig> = {}): Promise<string> {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
testLogger.info(`Capturing screenshot: ${filename}`);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
fullPage: finalConfig.fullPage,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Screenshot captured: ${filePath}`);
return filePath;
}
async captureElement(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
testLogger.info(`Capturing element screenshot: ${filename}`);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await locator.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Element screenshot captured: ${filePath}`);
return filePath;
}
async captureFullPage(config: Partial<ScreenshotConfig> = {}): Promise<string> {
return this.capture({ ...config, fullPage: true });
}
async captureViewport(config: Partial<ScreenshotConfig> = {}): Promise<string> {
return this.capture({ ...config, fullPage: false });
}
async captureOnFailure(testName: string, error?: Error): Promise<string> {
const filename = `failure-${testName}-${Date.now()}`;
const filePath = await this.captureFullPage({ filename });
testLogger.error(`Screenshot captured on failure: ${testName}`, error);
if (error) {
const errorLogPath = path.join(this.outputDir, `failure-${testName}-${Date.now()}.log`);
const errorLog = `
Test Name: ${testName}
Timestamp: ${new Date().toISOString()}
Error: ${error.message}
Stack Trace:
${error.stack}
`.trim();
fs.writeFileSync(errorLogPath, errorLog);
testLogger.info(`Error log saved: ${errorLogPath}`);
}
return filePath;
}
async captureWithDescription(description: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `${description}-${Date.now()}`;
const filePath = await this.capture({ ...config, filename });
const metadata = this.screenshots.get(filename);
if (metadata) {
metadata.description = description;
this.screenshots.set(filename, metadata);
}
return filePath;
}
async captureBeforeAction(actionName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `before-${actionName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureAfterAction(actionName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `after-${actionName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureStep(stepName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `step-${stepName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureMultiple(configs: Partial<ScreenshotConfig>[]): Promise<string[]> {
testLogger.info(`Capturing ${configs.length} screenshots`);
const filePaths: string[] = [];
for (let i = 0; i < configs.length; i++) {
const filePath = await this.capture(configs[i]);
filePaths.push(filePath);
}
testLogger.info(`Captured ${filePaths.length} screenshots`);
return filePaths;
}
async captureWithDelay(delay: number, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info(`Waiting ${delay}ms before capturing screenshot`);
await this.page.waitForTimeout(delay);
return this.capture(config);
}
async captureOnHover(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing screenshot on hover');
await locator.hover();
await this.page.waitForTimeout(300);
return this.capture(config);
}
async captureOnFocus(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing screenshot on focus');
await locator.focus();
await this.page.waitForTimeout(300);
return this.capture(config);
}
async captureVisibleArea(config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing visible area screenshot');
const viewportSize = this.page.viewportSize();
if (viewportSize) {
const clip = {
x: 0,
y: 0,
width: viewportSize.width,
height: viewportSize.height
};
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
clip,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Visible area screenshot captured: ${filePath}`);
return filePath;
}
return this.captureViewport(config);
}
async captureElementBounds(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing element bounds screenshot');
const box = await locator.boundingBox();
if (box) {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
clip: box,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Element bounds screenshot captured: ${filePath}`);
return filePath;
}
return this.captureElement(locator, config);
}
getScreenshotPath(filename: string): string | undefined {
const metadata = this.screenshots.get(filename);
return metadata?.path;
}
getAllScreenshots(): ScreenshotMetadata[] {
return Array.from(this.screenshots.values());
}
getScreenshotCount(): number {
return this.screenshots.size;
}
clearScreenshots(): void {
this.screenshots.clear();
testLogger.info('Screenshots cleared');
}
deleteScreenshot(filename: string): boolean {
const metadata = this.screenshots.get(filename);
if (metadata && fs.existsSync(metadata.path)) {
fs.unlinkSync(metadata.path);
this.screenshots.delete(filename);
testLogger.info(`Screenshot deleted: ${filename}`);
return true;
}
return false;
}
deleteAllScreenshots(): void {
const screenshotValues = Array.from(this.screenshots.values());
for (const metadata of screenshotValues) {
if (fs.existsSync(metadata.path)) {
fs.unlinkSync(metadata.path);
}
}
this.screenshots.clear();
testLogger.info('All screenshots deleted');
}
async compareScreenshots(beforePath: string, afterPath: string): Promise<boolean> {
testLogger.info(`Comparing screenshots: ${beforePath} vs ${afterPath}`);
const beforeExists = fs.existsSync(beforePath);
const afterExists = fs.existsSync(afterPath);
if (!beforeExists || !afterExists) {
testLogger.warn('Screenshot comparison failed: files not found');
return false;
}
const beforeStats = fs.statSync(beforePath);
const afterStats = fs.statSync(afterPath);
const areEqual = beforeStats.size === afterStats.size;
testLogger.info(`Screenshot comparison result: ${areEqual}`);
return areEqual;
}
async createScreenshotReport(): Promise<string> {
testLogger.info('Creating screenshot report');
const reportPath = path.join(this.outputDir, 'screenshot-report.md');
const screenshots = this.getAllScreenshots();
let report = '# Screenshot Report\n\n';
report += `Generated at: ${new Date().toISOString()}\n`;
report += `Total screenshots: ${screenshots.length}\n\n`;
report += '## Screenshots\n\n';
for (const screenshot of screenshots) {
report += `### ${screenshot.filename}\n`;
report += `- **Path**: ${screenshot.path}\n`;
report += `- **Timestamp**: ${screenshot.timestamp}\n`;
if (screenshot.description) {
report += `- **Description**: ${screenshot.description}\n`;
}
report += '\n';
}
fs.writeFileSync(reportPath, report);
testLogger.info(`Screenshot report created: ${reportPath}`);
return reportPath;
}
private generateFilename(filename?: string): string {
if (filename) {
return `${filename}.${this.defaultConfig.type || 'png'}`;
}
return `screenshot-${Date.now()}.${this.defaultConfig.type || 'png'}`;
}
private ensureOutputDir(): void {
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
testLogger.info(`Output directory created: ${this.outputDir}`);
}
}
}
@@ -0,0 +1,622 @@
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);
}
}