refactor(frontend): 重命名前端项目为 gym-manage-web
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,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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 || '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user