feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user