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 = 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 { 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): Promise { 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 { 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 { 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 { 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> { testLogger.info('Getting form data'); const data: Record = {}; 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 { 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 { 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 { 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 { 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> { testLogger.info('Getting error messages'); const errors: Record = {}; 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 { const errors = await this.getErrorMessages(); return Object.keys(errors).length > 0; } async waitForValidation(timeout: number = 5000): Promise { 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 { 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 { 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 { const field = this.fields.get(name); if (!field) { throw new Error(`Field not found: ${name}`); } return field.required; } async focusField(name: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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'); } }