refactor(security): 重构安全配置并优化测试环境
- 移除旧的测试套件和UAT测试文件 - 更新密码编码器配置使用BCrypt strength=12 - 添加用户角色关联表和相关服务 - 优化前端日期显示格式 - 清理无用资源和配置文件 - 增强测试数据管理和清理功能
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
export class RetryHelper {
|
||||
static async retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
backoff?: boolean;
|
||||
onRetry?: (attempt: number, error: Error) => void;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 1000,
|
||||
backoff = true,
|
||||
onRetry
|
||||
} = options;
|
||||
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (onRetry) {
|
||||
onRetry(attempt, lastError);
|
||||
}
|
||||
|
||||
const currentDelay = backoff ? delay * attempt : delay;
|
||||
await this.sleep(currentDelay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
static async retryWithCondition<T>(
|
||||
fn: () => Promise<T>,
|
||||
condition: (result: T) => boolean,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
timeout?: number;
|
||||
onRetry?: (attempt: number, lastResult: T) => void;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 10,
|
||||
delay = 500,
|
||||
timeout = 10000,
|
||||
onRetry
|
||||
} = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastResult: T | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
lastResult = await fn();
|
||||
|
||||
if (condition(lastResult)) {
|
||||
return lastResult;
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Timeout after ${timeout}ms waiting for condition to be met`);
|
||||
}
|
||||
|
||||
if (onRetry && lastResult !== undefined) {
|
||||
onRetry(attempt, lastResult);
|
||||
}
|
||||
|
||||
await this.sleep(delay);
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Timeout after ${timeout}ms: ${error}`);
|
||||
}
|
||||
|
||||
await this.sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Condition not met after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
static async retryElementAction<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
ignoreErrors?: string[];
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 1000,
|
||||
ignoreErrors = ['Timeout', 'Element not found', 'Element not visible']
|
||||
} = options;
|
||||
|
||||
return this.retry(fn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
const shouldIgnore = ignoreErrors.some(ignoredError =>
|
||||
error.message.includes(ignoredError)
|
||||
);
|
||||
|
||||
if (shouldIgnore) {
|
||||
console.log(`Attempt ${attempt} failed with ignorable error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryNetworkRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
retryableStatuses?: number[];
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 2000,
|
||||
retryableStatuses = [408, 429, 500, 502, 503, 504]
|
||||
} = options;
|
||||
|
||||
return this.retry(fn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Network request attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryClick(
|
||||
clickFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 500 } = options;
|
||||
|
||||
return this.retry(clickFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Click attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryFill(
|
||||
fillFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 500 } = options;
|
||||
|
||||
return this.retry(fillFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Fill attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryNavigation(
|
||||
navigateFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 1000 } = options;
|
||||
|
||||
return this.retry(navigateFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Navigation attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryAssertion<T>(
|
||||
assertionFn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { maxAttempts = 5, delay = 500 } = options;
|
||||
|
||||
return this.retry(assertionFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Assertion attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
static createRetryPolicy<T>(
|
||||
fn: () => Promise<T>,
|
||||
policy: {
|
||||
maxAttempts: number;
|
||||
initialDelay: number;
|
||||
maxDelay?: number;
|
||||
backoffMultiplier?: number;
|
||||
retryCondition?: (error: Error) => boolean;
|
||||
}
|
||||
): () => Promise<T> {
|
||||
const {
|
||||
maxAttempts,
|
||||
initialDelay,
|
||||
maxDelay = 30000,
|
||||
backoffMultiplier = 2,
|
||||
retryCondition
|
||||
} = policy;
|
||||
|
||||
return async () => {
|
||||
let currentDelay = initialDelay;
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (retryCondition && !retryCondition(lastError)) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
|
||||
await this.sleep(currentDelay);
|
||||
currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
}
|
||||
|
||||
static async retryWithTimeout<T>(
|
||||
fn: () => Promise<T>,
|
||||
timeout: number,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { maxAttempts = 3, delay = 1000 } = options;
|
||||
|
||||
return Promise.race([
|
||||
this.retry(fn, { maxAttempts, delay }),
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeout}ms`)), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export class TestDataCleanup {
|
||||
readonly page: Page;
|
||||
private createdUsers: string[] = [];
|
||||
private createdRoles: string[] = [];
|
||||
private createdMenus: string[] = [];
|
||||
private createdDictTypes: string[] = [];
|
||||
private createdDictData: string[] = [];
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
trackUser(username: string) {
|
||||
this.createdUsers.push(username);
|
||||
}
|
||||
|
||||
trackRole(roleName: string) {
|
||||
this.createdRoles.push(roleName);
|
||||
}
|
||||
|
||||
trackMenu(menuName: string) {
|
||||
this.createdMenus.push(menuName);
|
||||
}
|
||||
|
||||
trackDictType(dictType: string) {
|
||||
this.createdDictTypes.push(dictType);
|
||||
}
|
||||
|
||||
trackDictData(dictData: string) {
|
||||
this.createdDictData.push(dictData);
|
||||
}
|
||||
|
||||
async cleanupAll() {
|
||||
await this.cleanupUsers();
|
||||
await this.cleanupRoles();
|
||||
await this.cleanupMenus();
|
||||
await this.cleanupDictTypes();
|
||||
await this.cleanupDictData();
|
||||
}
|
||||
|
||||
async cleanupUsers() {
|
||||
for (const username of this.createdUsers) {
|
||||
try {
|
||||
await this.deleteUser(username);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete user ${username}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdUsers = [];
|
||||
}
|
||||
|
||||
async cleanupRoles() {
|
||||
for (const roleName of this.createdRoles) {
|
||||
try {
|
||||
await this.deleteRole(roleName);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete role ${roleName}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdRoles = [];
|
||||
}
|
||||
|
||||
async cleanupMenus() {
|
||||
for (const menuName of this.createdMenus) {
|
||||
try {
|
||||
await this.deleteMenu(menuName);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete menu ${menuName}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdMenus = [];
|
||||
}
|
||||
|
||||
async cleanupDictTypes() {
|
||||
for (const dictType of this.createdDictTypes) {
|
||||
try {
|
||||
await this.deleteDictType(dictType);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict type ${dictType}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdDictTypes = [];
|
||||
}
|
||||
|
||||
async cleanupDictData() {
|
||||
for (const dictData of this.createdDictData) {
|
||||
try {
|
||||
await this.deleteDictData(dictData);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict data ${dictData}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdDictData = [];
|
||||
}
|
||||
|
||||
private async deleteUser(username: string) {
|
||||
try {
|
||||
await this.page.goto('/users');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const searchInput = this.page.locator('input[placeholder*="搜索"], input[name*="keyword"], .el-input__inner').first();
|
||||
await searchInput.fill(username);
|
||||
|
||||
const searchButton = this.page.getByRole('button', { name: '搜索' }).or(this.page.locator('button:has-text("搜索")'));
|
||||
await searchButton.click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
const userRow = this.page.locator('tbody tr').filter({ hasText: username });
|
||||
const rowCount = await userRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = userRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete user ${username}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteRole(roleName: string) {
|
||||
try {
|
||||
await this.page.goto('/roles');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const searchInput = this.page.locator('input[placeholder*="搜索"], input[name*="keyword"], .el-input__inner').first();
|
||||
await searchInput.fill(roleName);
|
||||
|
||||
const searchButton = this.page.getByRole('button', { name: '搜索' }).or(this.page.locator('button:has-text("搜索")'));
|
||||
await searchButton.click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
const roleRow = this.page.locator('tbody tr').filter({ hasText: roleName });
|
||||
const rowCount = await roleRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = roleRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete role ${roleName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteMenu(menuName: string) {
|
||||
try {
|
||||
await this.page.goto('/menus');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const menuRow = this.page.locator('tbody tr').filter({ hasText: menuName });
|
||||
const rowCount = await menuRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = menuRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete menu ${menuName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDictType(dictType: string) {
|
||||
try {
|
||||
await this.page.goto('/dict');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const dictRow = this.page.locator('.dict-type-table tbody tr').filter({ hasText: dictType });
|
||||
const rowCount = await dictRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = dictRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict type ${dictType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDictData(dictData: string) {
|
||||
try {
|
||||
await this.page.goto('/dict');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const dictRow = this.page.locator('.dict-data-table tbody tr').filter({ hasText: dictData });
|
||||
const rowCount = await dictRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = dictRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict data ${dictData}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
export interface UserData {
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export interface RoleData {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface MenuData {
|
||||
menuName: string;
|
||||
menuType?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
permission?: string;
|
||||
sort?: number;
|
||||
visible?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface DictTypeData {
|
||||
dictName: string;
|
||||
dictType: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface DictDataData {
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
dictType: string;
|
||||
status: string;
|
||||
sort?: number;
|
||||
}
|
||||
|
||||
export class TestDataFactory {
|
||||
static generateTimestamp(): string {
|
||||
return Date.now().toString();
|
||||
}
|
||||
|
||||
static generateRandomString(length: number = 8): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static generateValidEmail(username: string): string {
|
||||
return `${username}@example.com`;
|
||||
}
|
||||
|
||||
static generateValidPhone(): string {
|
||||
const prefix = ['138', '139', '150', '151', '186', '188'];
|
||||
const selectedPrefix = prefix[Math.floor(Math.random() * prefix.length)];
|
||||
const suffix = Math.floor(Math.random() * 100000000).toString().padStart(8, '0');
|
||||
return selectedPrefix + suffix;
|
||||
}
|
||||
|
||||
static generateValidPassword(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
static createUser(suffix?: string): UserData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
username: `testuser_${uniqueSuffix}_${timestamp}`,
|
||||
nickname: `测试用户_${uniqueSuffix}_${timestamp}`,
|
||||
email: this.generateValidEmail(`testuser_${uniqueSuffix}_${timestamp}`),
|
||||
phone: this.generateValidPhone(),
|
||||
password: this.generateValidPassword(),
|
||||
confirmPassword: this.generateValidPassword()
|
||||
};
|
||||
}
|
||||
|
||||
static createAdminUser(): UserData {
|
||||
return {
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'admin123',
|
||||
confirmPassword: 'admin123'
|
||||
};
|
||||
}
|
||||
|
||||
static createRole(suffix?: string): RoleData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
roleName: `testrole_${uniqueSuffix}_${timestamp}`,
|
||||
roleKey: `test_role_${uniqueSuffix}_${timestamp}`,
|
||||
roleSort: 1,
|
||||
status: '1'
|
||||
};
|
||||
}
|
||||
|
||||
static createAdminRole(): RoleData {
|
||||
return {
|
||||
roleName: '管理员',
|
||||
roleKey: 'admin',
|
||||
roleSort: 1,
|
||||
status: '1'
|
||||
};
|
||||
}
|
||||
|
||||
static createMenu(suffix?: string, parentId?: string): MenuData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
menuName: `测试菜单_${uniqueSuffix}_${timestamp}`,
|
||||
menuType: 'M',
|
||||
path: `/testmenu_${uniqueSuffix}_${timestamp}`,
|
||||
component: `TestMenu${uniqueSuffix}`,
|
||||
permission: `system:testmenu:${uniqueSuffix}:${timestamp}`,
|
||||
sort: 1,
|
||||
visible: '0',
|
||||
status: '0'
|
||||
};
|
||||
}
|
||||
|
||||
static createSubMenu(parentId: string, suffix?: string): MenuData {
|
||||
const menuData = this.createMenu(suffix);
|
||||
menuData.menuType = 'C';
|
||||
menuData.path = `${menuData.path}/submenu`;
|
||||
return menuData;
|
||||
}
|
||||
|
||||
static createDictType(suffix?: string): DictTypeData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
dictName: `测试字典类型_${uniqueSuffix}_${timestamp}`,
|
||||
dictType: `test_dict_type_${uniqueSuffix}_${timestamp}`,
|
||||
status: '0',
|
||||
remark: `测试字典类型备注_${uniqueSuffix}_${timestamp}`
|
||||
};
|
||||
}
|
||||
|
||||
static createDictData(dictType: string, suffix?: string): DictDataData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
dictLabel: `测试字典数据_${uniqueSuffix}_${timestamp}`,
|
||||
dictValue: `test_dict_value_${uniqueSuffix}_${timestamp}`,
|
||||
dictType: dictType,
|
||||
status: '0',
|
||||
sort: 1
|
||||
};
|
||||
}
|
||||
|
||||
static createBatchUsers(count: number): UserData[] {
|
||||
const users: UserData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
users.push(this.createUser(`batch_${i}`));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
static createBatchRoles(count: number): RoleData[] {
|
||||
const roles: RoleData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
roles.push(this.createRole(`batch_${i}`));
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
static createBatchMenus(count: number): MenuData[] {
|
||||
const menus: MenuData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
menus.push(this.createMenu(`batch_${i}`));
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
|
||||
static createBatchDictTypes(count: number): DictTypeData[] {
|
||||
const dictTypes: DictTypeData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
dictTypes.push(this.createDictType(`batch_${i}`));
|
||||
}
|
||||
return dictTypes;
|
||||
}
|
||||
|
||||
static createBatchDictData(dictType: string, count: number): DictDataData[] {
|
||||
const dictData: DictDataData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
dictData.push(this.createDictData(dictType, `batch_${i}`));
|
||||
}
|
||||
return dictData;
|
||||
}
|
||||
|
||||
static createInvalidUser(): UserData {
|
||||
return {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: 'invalid-email',
|
||||
phone: 'invalid-phone',
|
||||
password: 'weak',
|
||||
confirmPassword: 'different'
|
||||
};
|
||||
}
|
||||
|
||||
static createInvalidRole(): RoleData {
|
||||
return {
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: -1,
|
||||
status: 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
static createInvalidMenu(): MenuData {
|
||||
return {
|
||||
menuName: '',
|
||||
menuType: 'invalid',
|
||||
path: '',
|
||||
component: '',
|
||||
permission: '',
|
||||
sort: -1,
|
||||
visible: 'invalid',
|
||||
status: 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
static createLongString(length: number = 1000): string {
|
||||
return this.generateRandomString(length);
|
||||
}
|
||||
|
||||
static createSpecialCharsString(): string {
|
||||
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
}
|
||||
|
||||
static createUnicodeString(): string {
|
||||
return '测试中文🎉🚀';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TestHelpers {
|
||||
static async waitForElementVisible(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForElementHidden(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'hidden', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeClick(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.click();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe click failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeFill(locator: Locator, value: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.clear();
|
||||
await locator.fill(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe fill failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeSelect(locator: Locator, value: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.selectOption(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe select failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async retryOperation<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 1000
|
||||
): Promise<T | null> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (attempt === maxRetries) {
|
||||
console.error(`Operation failed after ${maxRetries} attempts:`, error);
|
||||
return null;
|
||||
}
|
||||
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async waitForNetworkIdle(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('networkidle', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('Network idle timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForNavigation(page: Page, urlPattern: RegExp, timeout: number = 10000): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForURL(urlPattern, { timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async handleDialog(page: Page, action: 'accept' | 'dismiss' = 'accept'): Promise<void> {
|
||||
page.on('dialog', async dialog => {
|
||||
if (action === 'accept') {
|
||||
await dialog.accept();
|
||||
} else {
|
||||
await dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async getTableData(table: Locator): Promise<string[][]> {
|
||||
const rows = await table.locator('tbody tr').all();
|
||||
const data: string[][] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const cells = await row.locator('td').allTextContents();
|
||||
data.push(cells);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async findTableRowByContent(table: Locator, content: string): Promise<Locator | null> {
|
||||
const rows = await table.locator('tbody tr').all();
|
||||
|
||||
for (const row of rows) {
|
||||
const textContent = await row.textContent();
|
||||
if (textContent && textContent.includes(content)) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async scrollToElement(page: Page, locator: Locator): Promise<void> {
|
||||
await locator.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
static async waitForAnimation(locator: Locator): Promise<void> {
|
||||
await locator.waitFor({ state: 'attached' });
|
||||
await locator.evaluate(el => {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async takeScreenshot(page: Page, name: string): Promise<void> {
|
||||
await page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
static async waitForPageLoad(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('load', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('Page load timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForDOMContent(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('DOM content load timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async isElementVisible(locator: Locator): Promise<boolean> {
|
||||
try {
|
||||
return await locator.isVisible({ timeout: 1000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async isElementEnabled(locator: Locator): Promise<boolean> {
|
||||
try {
|
||||
return await locator.isEnabled({ timeout: 1000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getElementText(locator: Locator): Promise<string | null> {
|
||||
try {
|
||||
return await locator.textContent({ timeout: 5000 });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getElementCount(locator: Locator): Promise<number> {
|
||||
try {
|
||||
return await locator.count();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForTextContent(locator: Locator, expectedText: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
const text = await locator.textContent();
|
||||
return text !== null && text.includes(expectedText);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async clearInput(locator: Locator): Promise<void> {
|
||||
await locator.click();
|
||||
await locator.fill('');
|
||||
await locator.press('Control+A');
|
||||
await locator.press('Backspace');
|
||||
}
|
||||
|
||||
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const successMessage = page.locator('.el-message--success, .success-message, [class*="success"]');
|
||||
try {
|
||||
await successMessage.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const errorMessage = page.locator('.el-message--error, .error-message, [class*="error"]');
|
||||
try {
|
||||
await errorMessage.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForLoadingComplete(page: Page, timeout: number = 10000): Promise<void> {
|
||||
const loadingSpinner = page.locator('.el-loading-mask, .loading, [class*="loading"]');
|
||||
|
||||
try {
|
||||
await loadingSpinner.waitFor({ state: 'visible', timeout: 2000 });
|
||||
await loadingSpinner.waitFor({ state: 'hidden', timeout });
|
||||
} catch {
|
||||
console.log('No loading spinner found or already hidden');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForModal(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const modal = page.locator('.el-dialog, .modal, [role="dialog"]');
|
||||
try {
|
||||
await modal.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async closeModal(page: Page): Promise<boolean> {
|
||||
const closeButton = page.locator('.el-dialog__close, .modal-close, button[aria-label="Close"]');
|
||||
try {
|
||||
await closeButton.click();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForSelectDropdown(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const dropdown = page.locator('.el-select-dropdown, .select-dropdown');
|
||||
try {
|
||||
await dropdown.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async selectFromDropdown(page: Page, value: string): Promise<boolean> {
|
||||
const option = page.locator('.el-select-dropdown__item, .select-option').filter({ hasText: value });
|
||||
try {
|
||||
await option.click();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { TestDataCleanup } from './TestDataCleanup';
|
||||
export { TestDataFactory } from './TestDataFactory';
|
||||
export { RetryHelper } from './RetryHelper';
|
||||
export type {
|
||||
UserData,
|
||||
RoleData,
|
||||
MenuData,
|
||||
DictTypeData,
|
||||
DictDataData
|
||||
} from './TestDataFactory';
|
||||
Reference in New Issue
Block a user