refactor(frontend): 重命名前端项目为 gym-manage-web

This commit is contained in:
张翔
2026-04-17 18:37:45 +08:00
parent deb961c427
commit 45bb89fc7f
140 changed files with 2 additions and 2 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)
)
]);
}
}
+221
View File
@@ -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);
}
}
}
+255
View File
@@ -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;
}
}
}
+159
View File
@@ -0,0 +1,159 @@
import { APIRequestContext } from '@playwright/test';
export class ApiClient {
private request: APIRequestContext;
private baseURL: string;
constructor(request: APIRequestContext, baseURL: string = 'http://localhost:8084') {
this.request = request;
this.baseURL = baseURL;
}
async login(username: string, password: string): Promise<{ token: string; userId: number }> {
const response = await this.request.post(`${this.baseURL}/api/auth/login`, {
data: {
username,
password,
},
});
if (!response.ok()) {
throw new Error(`Login failed: ${response.status()}`);
}
const data = await response.json();
return {
token: data.token,
userId: data.userId,
};
}
async logout(token: string): Promise<void> {
await this.request.post(`${this.baseURL}/api/auth/logout`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
async getUsers(token: string): Promise<any[]> {
const response = await this.request.get(`${this.baseURL}/api/users`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok()) {
throw new Error(`Get users failed: ${response.status()}`);
}
return await response.json();
}
async createUser(token: string, userData: any): Promise<any> {
const response = await this.request.post(`${this.baseURL}/api/users`, {
headers: {
Authorization: `Bearer ${token}`,
},
data: userData,
});
if (!response.ok()) {
throw new Error(`Create user failed: ${response.status()}`);
}
return await response.json();
}
async updateUser(token: string, userId: number, userData: any): Promise<any> {
const response = await this.request.put(`${this.baseURL}/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
data: userData,
});
if (!response.ok()) {
throw new Error(`Update user failed: ${response.status()}`);
}
return await response.json();
}
async deleteUser(token: string, userId: number): Promise<void> {
const response = await this.request.delete(`${this.baseURL}/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok()) {
throw new Error(`Delete user failed: ${response.status()}`);
}
}
async getRoles(token: string): Promise<any[]> {
const response = await this.request.get(`${this.baseURL}/api/roles`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok()) {
throw new Error(`Get roles failed: ${response.status()}`);
}
return await response.json();
}
async createRole(token: string, roleData: any): Promise<any> {
const response = await this.request.post(`${this.baseURL}/api/roles`, {
headers: {
Authorization: `Bearer ${token}`,
},
data: roleData,
});
if (!response.ok()) {
throw new Error(`Create role failed: ${response.status()}`);
}
return await response.json();
}
async deleteRole(token: string, roleId: number): Promise<void> {
const response = await this.request.delete(`${this.baseURL}/api/roles/${roleId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok()) {
throw new Error(`Delete role failed: ${response.status()}`);
}
}
async getMenus(token: string): Promise<any[]> {
const response = await this.request.get(`${this.baseURL}/api/menus`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok()) {
throw new Error(`Get menus failed: ${response.status()}`);
}
return await response.json();
}
async healthCheck(): Promise<{ status: string }> {
const response = await this.request.get(`${this.baseURL}/actuator/health`);
if (!response.ok()) {
throw new Error(`Health check failed: ${response.status()}`);
}
return await response.json();
}
}
+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';
+181
View File
@@ -0,0 +1,181 @@
import { APIRequestContext } from '@playwright/test';
export interface TestUser {
username: string;
nickname?: string;
email: string;
phone: string;
password: string;
roleIds?: number[];
}
export interface TestRole {
roleName: string;
roleKey: string;
roleSort: string;
status: string;
remark?: string;
}
export class TestDataManager {
private static testData: Map<string, any> = new Map();
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static generateTimestamp(): string {
return Date.now().toString();
}
static generateTestUser(override?: Partial<TestUser>): TestUser {
const timestamp = this.generateTimestamp();
return {
username: `testuser_${timestamp}`,
nickname: `测试用户${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
roleIds: [],
...override,
};
}
static generateTestRole(override?: Partial<TestRole>): TestRole {
const timestamp = this.generateTimestamp();
return {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `测试角色备注_${timestamp}`,
...override,
};
}
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
data: userData,
});
if (!response.ok()) {
throw new Error(`Failed to create test user: ${await response.text()}`);
}
const result = await response.json();
const userId = result.data?.id || result.id;
this.testData.set(`user_${userData.username}`, {
id: userId,
...userData,
});
return result;
}
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
data: roleData,
});
if (!response.ok()) {
throw new Error(`Failed to create test role: ${await response.text()}`);
}
const result = await response.json();
const roleId = result.data?.id || result.id;
this.testData.set(`role_${roleData.roleKey}`, {
id: roleId,
...roleData,
});
return result;
}
static async deleteTestUser(request: APIRequestContext, username: string): Promise<void> {
const userData = this.testData.get(`user_${username}`);
if (!userData || !userData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
}
this.testData.delete(`user_${username}`);
}
static async deleteTestRole(request: APIRequestContext, roleKey: string): Promise<void> {
const roleData = this.testData.get(`role_${roleKey}`);
if (!roleData || !roleData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
}
this.testData.delete(`role_${roleKey}`);
}
static async cleanupTestData(request: APIRequestContext): Promise<void> {
const cleanupPromises: Promise<void>[] = [];
const entries = Array.from(this.testData.entries());
for (const [key, data] of entries) {
if (key.startsWith('user_')) {
cleanupPromises.push(this.deleteTestUser(request, data.username));
} else if (key.startsWith('role_')) {
cleanupPromises.push(this.deleteTestRole(request, data.roleKey));
}
}
await Promise.allSettled(cleanupPromises);
this.testData.clear();
}
static getTestData(key: string): any {
return this.testData.get(key);
}
static getAllTestData(): Map<string, any> {
return new Map(this.testData);
}
static clearTestData(): void {
this.testData.clear();
}
}
export class DatabaseHelper {
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static async resetDatabase(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/reset-database`);
if (!response.ok()) {
throw new Error(`Failed to reset database: ${await response.text()}`);
}
}
static async clearTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/clear-test-data`);
if (!response.ok()) {
throw new Error(`Failed to clear test data: ${await response.text()}`);
}
}
static async seedTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/seed-test-data`);
if (!response.ok()) {
throw new Error(`Failed to seed test data: ${await response.text()}`);
}
}
}
+263
View File
@@ -0,0 +1,263 @@
import { Page, expect } from '@playwright/test';
export class TestHelper {
static async waitForPageLoad(page: Page, timeout: number = 30000): Promise<void> {
await page.waitForLoadState('networkidle', { timeout });
await page.waitForLoadState('domcontentloaded', { timeout });
}
static async waitForElementVisible(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toBeVisible({ timeout });
}
static async waitForElementHidden(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toBeHidden({ timeout });
}
static async waitForTextContent(
page: Page,
selector: string,
text: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toContainText(text, { timeout });
}
static async clickElement(page: Page, selector: string, timeout: number = 10000): Promise<void> {
await page.click(selector, { timeout });
}
static async fillInput(
page: Page,
selector: string,
value: string,
timeout: number = 10000
): Promise<void> {
await page.fill(selector, value, { timeout });
}
static async selectOption(
page: Page,
selector: string,
value: string,
timeout: number = 10000
): Promise<void> {
await page.selectOption(selector, value, { timeout });
}
static async checkCheckbox(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await page.check(selector, { timeout });
}
static async uncheckCheckbox(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await page.uncheck(selector, { timeout });
}
static async uploadFile(
page: Page,
selector: string,
filePath: string,
timeout: number = 10000
): Promise<void> {
await page.setInputFiles(selector, filePath, { timeout });
}
static async takeScreenshot(
page: Page,
filename: string,
fullPage: boolean = false
): Promise<void> {
await page.screenshot({
path: `test-results/screenshots/${filename}`,
fullPage,
});
}
static async waitForUrl(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<void> {
await page.waitForURL(urlPattern, { timeout });
}
static async reloadPage(page: Page, timeout: number = 30000): Promise<void> {
await page.reload({ waitUntil: 'networkidle', timeout });
}
static async navigateTo(page: Page, url: string, timeout: number = 30000): Promise<void> {
await page.goto(url, { waitUntil: 'networkidle', timeout });
}
static async waitForDialog(page: Page, timeout: number = 10000): Promise<void> {
await page.waitForEvent('dialog', { timeout });
}
static async handleDialog(page: Page, accept: boolean = true): Promise<void> {
page.on('dialog', async (dialog) => {
if (accept) {
await dialog.accept();
} else {
await dialog.dismiss();
}
});
}
static async waitForToast(
page: Page,
message: string,
timeout: number = 5000
): Promise<void> {
await expect(page.locator('.el-message')).toContainText(message, { timeout });
}
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<void> {
await expect(page.locator('.el-message--success')).toBeVisible({ timeout });
}
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<void> {
await expect(page.locator('.el-message--error')).toBeVisible({ timeout });
}
static async getElementText(page: Page, selector: string): Promise<string> {
const text = await page.textContent(selector);
return text || '';
}
static async getElementCount(page: Page, selector: string): Promise<number> {
return await page.locator(selector).count();
}
static async isElementVisible(page: Page, selector: string): Promise<boolean> {
return await page.locator(selector).isVisible();
}
static async isElementEnabled(page: Page, selector: string): Promise<boolean> {
return await page.locator(selector).isEnabled();
}
static async scrollToElement(page: Page, selector: string): Promise<void> {
await page.locator(selector).scrollIntoViewIfNeeded();
}
static async hoverElement(page: Page, selector: string): Promise<void> {
await page.hover(selector);
}
static async doubleClickElement(page: Page, selector: string): Promise<void> {
await page.dblclick(selector);
}
static async rightClickElement(page: Page, selector: string): Promise<void> {
await page.click(selector, { button: 'right' });
}
static async waitForApiResponse(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<void> {
await page.waitForResponse(
(response) => !!response.url().match(urlPattern),
{ timeout }
);
}
static async getApiResponse(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<any> {
const response = await page.waitForResponse(
(response) => !!response.url().match(urlPattern),
{ timeout }
);
return await response.json();
}
static async mockApiResponse(
page: Page,
urlPattern: string | RegExp,
mockData: any
): Promise<void> {
await page.route(urlPattern, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockData),
});
});
}
static async executeScript(page: Page, script: string): Promise<any> {
return await page.evaluate(script);
}
static async setLocalStorage(page: Page, key: string, value: string): Promise<void> {
await page.evaluate(
({ key, value }) => {
localStorage.setItem(key, value);
},
{ key, value }
);
}
static async getLocalStorage(page: Page, key: string): Promise<string | null> {
return await page.evaluate((key) => localStorage.getItem(key), key);
}
static async clearLocalStorage(page: Page): Promise<void> {
await page.evaluate(() => localStorage.clear());
}
static async setSessionStorage(page: Page, key: string, value: string): Promise<void> {
await page.evaluate(
({ key, value }) => {
sessionStorage.setItem(key, value);
},
{ key, value }
);
}
static async clearSessionStorage(page: Page): Promise<void> {
await page.evaluate(() => sessionStorage.clear());
}
static async clearCookies(page: Page): Promise<void> {
await page.context().clearCookies();
}
static async clearAllStorage(page: Page): Promise<void> {
await this.clearLocalStorage(page);
await this.clearSessionStorage(page);
await this.clearCookies(page);
}
static async getAuthToken(page: Page): Promise<string> {
const token = await this.getLocalStorage(page, 'token');
if (!token) {
const user = await this.getLocalStorage(page, 'user');
if (user) {
const userData = JSON.parse(user);
return userData.token || '';
}
}
return token || '';
}
}