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

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,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');
}
}