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