refactor(security): 重构安全配置并优化测试环境

- 移除旧的测试套件和UAT测试文件
- 更新密码编码器配置使用BCrypt strength=12
- 添加用户角色关联表和相关服务
- 优化前端日期显示格式
- 清理无用资源和配置文件
- 增强测试数据管理和清理功能
This commit is contained in:
张翔
2026-03-27 13:00:22 +08:00
parent ce30893a96
commit af44c23f21
294 changed files with 16057 additions and 22601 deletions
+288
View File
@@ -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 '测试中文🎉🚀';
}
}
+283
View File
@@ -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;
}
}
}
+10
View File
@@ -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';