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

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,332 @@
import { APIRequestContext } from '@playwright/test';
import { testLogger } from './test-logger';
export interface MetricsData {
jvmMemoryUsed: number;
jvmMemoryMax: number;
jvmGcPause: number;
responseTime: number;
requestCount: number;
errorCount: number;
}
export interface JvmInfo {
memory: {
heap: {
used: number;
max: number;
committed: number;
};
nonHeap: {
used: number;
max: number;
committed: number;
};
};
gc: {
pauseCount: number;
pauseTime: number;
};
threads: {
live: number;
peak: number;
daemon: number;
};
}
export interface EnvInfo {
activeProfiles: string[];
properties: Record<string, string>;
}
export interface AppInfo {
name: string;
version: string;
description: string;
}
export interface TestMetrics {
testName: string;
duration: number;
status: 'passed' | 'failed';
memoryUsage: number;
responseTime: number;
timestamp: string;
}
export class ActuatorMonitor {
private request: APIRequestContext;
private baseUrl: string;
private authToken?: string;
constructor(request: APIRequestContext, baseUrl: string, authToken?: string) {
this.request = request;
this.baseUrl = baseUrl.replace(/\/$/, '');
this.authToken = authToken;
}
private async getEndpoint(endpoint: string): Promise<any> {
try {
const headers: Record<string, string> = {
'Accept': 'application/json',
};
if (this.authToken) {
headers['Authorization'] = `Bearer ${this.authToken}`;
}
const response = await this.request.get(`${this.baseUrl}${endpoint}`, {
headers,
timeout: 5000,
});
if (!response.ok()) {
throw new Error(`Actuator endpoint failed: ${response.status()} ${response.statusText()}`);
}
return await response.json();
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error(`获取Actuator端点失败: ${endpoint}`, errorObj);
throw errorObj;
}
}
async checkHealth(): Promise<boolean> {
try {
testLogger.debug('检查应用健康状态');
const healthData = await this.getEndpoint('/actuator/health');
const status = healthData.status;
// 服务可访问即可,不严格要求 UP 状态
// 因为某些组件(如 CPU 负载)可能导致整体状态为 DOWN
const isAccessible = status === 'UP' || status === 'DOWN' || status === 'WARNING';
if (status !== 'UP') {
testLogger.warn(`应用健康状态非UP: ${status}`);
if (healthData.components) {
for (const [name, component] of Object.entries(healthData.components)) {
if ((component as { status: string }).status !== 'UP') {
testLogger.warn(`组件 ${name} 状态: ${(component as { status: string }).status}`);
}
}
}
}
testLogger.info(`应用健康状态: ${status}, 可访问: ${isAccessible}`);
return isAccessible;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('健康检查失败', errorObj);
return false;
}
}
async getMetrics(): Promise<MetricsData> {
try {
testLogger.debug('获取性能指标');
const jvmMemoryUsed = await this.getMetricValue('jvm.memory.used', 'area=heap');
const jvmMemoryMax = await this.getMetricValue('jvm.memory.max', 'area=heap');
const jvmGcPause = await this.getMetricValue('jvm.gc.pause', 'count');
const metrics: MetricsData = {
jvmMemoryUsed: Math.round(jvmMemoryUsed / 1024 / 1024),
jvmMemoryMax: Math.round(jvmMemoryMax / 1024 / 1024),
jvmGcPause: Math.round(jvmGcPause),
responseTime: 0,
requestCount: 0,
errorCount: 0,
};
testLogger.debug(`性能指标: ${JSON.stringify(metrics)}`);
return metrics;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('获取性能指标失败', errorObj);
throw errorObj;
}
}
private async getMetricValue(metricName: string, tags: string = ''): Promise<number> {
try {
const endpoint = tags
? `/actuator/metrics/${metricName}?tag=${tags}`
: `/actuator/metrics/${metricName}`;
const data = await this.getEndpoint(endpoint);
return data.measurements?.[0]?.value || 0;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.warn(`获取指标值失败: ${metricName}`, errorObj);
return 0;
}
}
async getJvmInfo(): Promise<JvmInfo> {
try {
testLogger.debug('获取JVM信息');
const heapUsed = await this.getMetricValue('jvm.memory.used', 'area=heap');
const heapMax = await this.getMetricValue('jvm.memory.max', 'area=heap');
const heapCommitted = await this.getMetricValue('jvm.memory.committed', 'area=heap');
const nonHeapUsed = await this.getMetricValue('jvm.memory.used', 'area=nonheap');
const nonHeapMax = await this.getMetricValue('jvm.memory.max', 'area=nonheap');
const nonHeapCommitted = await this.getMetricValue('jvm.memory.committed', 'area=nonheap');
const gcPauseCount = await this.getMetricValue('jvm.gc.pause.count');
const gcPauseTime = await this.getMetricValue('jvm.gc.pause.total');
const threadsLive = await this.getMetricValue('jvm.threads.live');
const threadsPeak = await this.getMetricValue('jvm.threads.peak');
const threadsDaemon = await this.getMetricValue('jvm.threads.daemon');
const jvmInfo: JvmInfo = {
memory: {
heap: {
used: Math.round(heapUsed / 1024 / 1024),
max: Math.round(heapMax / 1024 / 1024),
committed: Math.round(heapCommitted / 1024 / 1024),
},
nonHeap: {
used: Math.round(nonHeapUsed / 1024 / 1024),
max: Math.round(nonHeapMax / 1024 / 1024),
committed: Math.round(nonHeapCommitted / 1024 / 1024),
},
},
gc: {
pauseCount: Math.round(gcPauseCount),
pauseTime: Math.round(gcPauseTime / 1000),
},
threads: {
live: Math.round(threadsLive),
peak: Math.round(threadsPeak),
daemon: Math.round(threadsDaemon),
},
};
testLogger.debug(`JVM信息: ${JSON.stringify(jvmInfo)}`);
return jvmInfo;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('获取JVM信息失败', errorObj);
throw errorObj;
}
}
async getEnvInfo(): Promise<EnvInfo> {
try {
testLogger.debug('获取环境信息');
const envData = await this.getEndpoint('/actuator/env');
const envInfo: EnvInfo = {
activeProfiles: envData.profiles?.active || [],
properties: {},
};
if (envData.propertySources) {
for (const source of envData.propertySources) {
if (source.properties) {
for (const [key, value] of Object.entries(source.properties)) {
if (value && typeof value === 'object' && 'value' in value) {
envInfo.properties[key] = (value as any).value;
}
}
}
}
}
testLogger.debug(`环境信息: 激活的配置文件 [${envInfo.activeProfiles.join(', ')}]`);
return envInfo;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('获取环境信息失败', errorObj);
throw errorObj;
}
}
async getAppInfo(): Promise<AppInfo> {
try {
testLogger.debug('获取应用信息');
const appData = await this.getEndpoint('/actuator/info');
const appInfo: AppInfo = {
name: appData.app?.name || 'Unknown',
version: appData.app?.version || 'Unknown',
description: appData.app?.description || 'Unknown',
};
testLogger.debug(`应用信息: ${appInfo.name} v${appInfo.version}`);
return appInfo;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('获取应用信息失败', errorObj);
throw errorObj;
}
}
async pushTestMetrics(metrics: TestMetrics): Promise<void> {
try {
testLogger.debug(`推送测试指标: ${metrics.testName}`);
const response = await this.request.post(`${this.baseUrl}/actuator/metrics/test`, {
headers: {
'Content-Type': 'application/json',
'Authorization': this.authToken ? `Bearer ${this.authToken}` : '',
},
data: metrics,
timeout: 5000,
});
if (!response.ok()) {
throw new Error(`推送测试指标失败: ${response.status()} ${response.statusText()}`);
}
testLogger.debug(`测试指标推送成功: ${metrics.testName}`);
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.warn('推送测试指标失败(可能不支持自定义指标)', errorObj);
}
}
async waitForHealth(maxRetries: number = 30, retryInterval: number = 2000): Promise<boolean> {
testLogger.info(`等待应用健康状态,最大重试次数: ${maxRetries}`);
for (let i = 0; i < maxRetries; i++) {
const isHealthy = await this.checkHealth();
if (isHealthy) {
testLogger.info('应用健康状态检查通过');
return true;
}
testLogger.debug(`应用未就绪,等待 ${retryInterval}ms 后重试 (${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
testLogger.error('应用健康状态检查超时');
return false;
}
async getFullHealthInfo(): Promise<any> {
try {
testLogger.debug('获取完整健康信息');
const healthData = await this.getEndpoint('/actuator/health');
testLogger.debug(`完整健康信息: ${JSON.stringify(healthData)}`);
return healthData;
} catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
testLogger.error('获取完整健康信息失败', errorObj);
throw errorObj;
}
}
}
@@ -0,0 +1,187 @@
import { test as base, expect, Page, Locator } from '@playwright/test';
import { TestDataGenerator } from './test-data.js';
import { TestLogger } from './test-logger.js';
import { ScreenshotHelper } from './screenshot-helper.js';
import { FormHelper } from './form-helper.js';
import { TableHelper } from './table-helper.js';
import { MockManager } from './mock-manager.js';
/**
* 基础测试类型定义
*/
export interface TestContext {
page: Page;
testData: ReturnType<typeof TestDataGenerator.getInstance>;
testLogger: TestLogger;
helpers: {
screenshot: ScreenshotHelper;
form: FormHelper;
table: TableHelper;
};
mocks: MockManager;
}
/**
* 可复用的测试固件
* 提供统一的测试基础设施
*/
export const baseTest = base.extend<TestContext>({
// 测试数据生成器
testData: async ({}, use) => {
const generator = TestDataGenerator.getInstance();
await use(generator);
},
// 测试日志记录器
testLogger: async ({}, use) => {
const logger = new TestLogger();
await use(logger);
},
// 测试辅助工具集合
helpers: async ({ page, testLogger }, use) => {
const helpers = {
screenshot: new ScreenshotHelper(page, testLogger),
form: new FormHelper(page, testLogger),
table: new TableHelper(page, testLogger),
};
await use(helpers);
},
// Mock管理器
mocks: async ({ page }, use) => {
const mockManager = new MockManager(page);
await use(mockManager);
},
});
/**
* 页面对象基类
* 所有页面对象都应继承此类
*/
export abstract class BasePage {
protected page: Page;
protected testLogger: TestLogger;
protected baseUrl: string;
constructor(page: Page, testLogger: TestLogger, baseUrl: string = process.env.E2E_BASE_URL || 'http://localhost:5174') {
this.page = page;
this.testLogger = testLogger;
this.baseUrl = baseUrl;
}
/**
* 导航到页面
*/
abstract navigate(): Promise<void>;
/**
* 等待页面加载完成
*/
abstract waitForLoad(): Promise<void>;
/**
* 获取页面标题
*/
async getPageTitle(): Promise<string> {
return await this.page.title();
}
/**
* 截图
*/
async screenshot(name: string): Promise<void> {
await this.page.screenshot({
path: `./test-results/screenshots/${name}-${Date.now()}.png`,
fullPage: true,
});
}
/**
* 等待元素可见
*/
async waitForVisible(selector: string, timeout: number = 10000): Promise<Locator> {
const locator = this.page.locator(selector);
await locator.waitFor({ state: 'visible', timeout });
return locator;
}
/**
* 等待元素隐藏
*/
async waitForHidden(selector: string, timeout: number = 10000): Promise<void> {
const locator = this.page.locator(selector);
await locator.waitFor({ state: 'hidden', timeout });
}
/**
* 点击元素
*/
async click(selector: string, options?: { force?: boolean }): Promise<void> {
await this.page.click(selector, options);
}
/**
* 填写输入框
*/
async fill(selector: string, value: string): Promise<void> {
await this.page.fill(selector, value);
}
/**
* 获取元素文本
*/
async getText(selector: string): Promise<string> {
return await this.page.locator(selector).textContent() || '';
}
/**
* 检查元素是否存在
*/
async exists(selector: string): Promise<boolean> {
return await this.page.locator(selector).count() > 0;
}
/**
* 检查元素是否可见
*/
async isVisible(selector: string): Promise<boolean> {
return await this.page.locator(selector).isVisible();
}
}
/**
* 测试套件基类
* 提供统一的测试套件结构
*/
export abstract class TestSuite {
protected test = baseTest;
/**
* 运行测试套件
*/
abstract run(): void;
/**
* 创建测试用例
*/
protected createTest(
name: string,
testFn: (context: TestContext) => Promise<void>
): void {
this.test(name, async (context) => {
const { testLogger } = context;
testLogger.startTest(name);
try {
await testFn(context);
testLogger.endTest(name, 'passed');
} catch (error) {
testLogger.endTest(name, 'failed', error as Error);
throw error;
}
});
}
}
export { expect };
@@ -0,0 +1,203 @@
/**
* 业务流程工作流定义
* 定义核心业务场景的完整流程步骤
*/
export interface WorkflowStep {
name: string;
action: () => Promise<void>;
rollback?: () => Promise<void>;
timeout?: number;
retryCount?: number;
}
export interface BusinessWorkflow {
name: string;
description: string;
steps: WorkflowStep[];
preconditions?: () => Promise<boolean>;
postconditions?: () => Promise<boolean>;
cleanup?: () => Promise<void>;
}
/**
* 用户管理工作流
*/
export const UserManagementWorkflows = {
/**
* 创建用户完整流程
*/
createUser: {
name: 'createUser',
description: '创建新用户的完整流程',
steps: [
{ name: 'navigateToUserManagement', action: async () => {}, timeout: 10000 },
{ name: 'clickAddUserButton', action: async () => {}, timeout: 5000 },
{ name: 'fillUserForm', action: async () => {}, timeout: 10000 },
{ name: 'submitUserForm', action: async () => {}, timeout: 10000 },
{ name: 'verifyUserCreated', action: async () => {}, timeout: 5000 }
]
},
/**
* 编辑用户完整流程
*/
editUser: {
name: 'editUser',
description: '编辑用户的完整流程',
steps: [
{ name: 'navigateToUserManagement', action: async () => {}, timeout: 10000 },
{ name: 'searchUser', action: async () => {}, timeout: 10000 },
{ name: 'clickEditButton', action: async () => {}, timeout: 5000 },
{ name: 'updateUserForm', action: async () => {}, timeout: 10000 },
{ name: 'submitUserForm', action: async () => {}, timeout: 10000 },
{ name: 'verifyUserUpdated', action: async () => {}, timeout: 5000 }
]
},
/**
* 删除用户完整流程
*/
deleteUser: {
name: 'deleteUser',
description: '删除用户的完整流程',
steps: [
{ name: 'navigateToUserManagement', action: async () => {}, timeout: 10000 },
{ name: 'searchUser', action: async () => {}, timeout: 10000 },
{ name: 'clickDeleteButton', action: async () => {}, timeout: 5000 },
{ name: 'confirmDelete', action: async () => {}, timeout: 5000 },
{ name: 'verifyUserDeleted', action: async () => {}, timeout: 5000 }
]
}
};
/**
* 角色管理工作流
*/
export const RoleManagementWorkflows = {
/**
* 创建角色完整流程
*/
createRole: {
name: 'createRole',
description: '创建新角色的完整流程',
steps: [
{ name: 'navigateToRoleManagement', action: async () => {}, timeout: 10000 },
{ name: 'clickAddRoleButton', action: async () => {}, timeout: 5000 },
{ name: 'fillRoleForm', action: async () => {}, timeout: 10000 },
{ name: 'assignPermissions', action: async () => {}, timeout: 10000 },
{ name: 'submitRoleForm', action: async () => {}, timeout: 10000 },
{ name: 'verifyRoleCreated', action: async () => {}, timeout: 5000 }
]
},
/**
* 编辑角色完整流程
*/
editRole: {
name: 'editRole',
description: '编辑角色的完整流程',
steps: [
{ name: 'navigateToRoleManagement', action: async () => {}, timeout: 10000 },
{ name: 'searchRole', action: async () => {}, timeout: 10000 },
{ name: 'clickEditButton', action: async () => {}, timeout: 5000 },
{ name: 'updateRoleForm', action: async () => {}, timeout: 10000 },
{ name: 'updatePermissions', action: async () => {}, timeout: 10000 },
{ name: 'submitRoleForm', action: async () => {}, timeout: 10000 },
{ name: 'verifyRoleUpdated', action: async () => {}, timeout: 5000 }
]
}
};
/**
* 菜单管理工作流
*/
export const MenuManagementWorkflows = {
/**
* 创建菜单完整流程
*/
createMenu: {
name: 'createMenu',
description: '创建新菜单的完整流程',
steps: [
{ name: 'navigateToMenuManagement', action: async () => {}, timeout: 10000 },
{ name: 'clickAddMenuButton', action: async () => {}, timeout: 5000 },
{ name: 'fillMenuForm', action: async () => {}, timeout: 10000 },
{ name: 'selectParentMenu', action: async () => {}, timeout: 5000 },
{ name: 'submitMenuForm', action: async () => {}, timeout: 10000 },
{ name: 'verifyMenuCreated', action: async () => {}, timeout: 5000 }
]
}
};
/**
* 认证工作流
*/
export const AuthenticationWorkflows = {
/**
* 用户登录流程
*/
login: {
name: 'login',
description: '用户登录的完整流程',
steps: [
{ name: 'navigateToLogin', action: async () => {}, timeout: 10000 },
{ name: 'fillUsername', action: async () => {}, timeout: 5000 },
{ name: 'fillPassword', action: async () => {}, timeout: 5000 },
{ name: 'clickLoginButton', action: async () => {}, timeout: 5000 },
{ name: 'verifyLoginSuccess', action: async () => {}, timeout: 10000 }
]
},
/**
* 用户登出流程
*/
logout: {
name: 'logout',
description: '用户登出的完整流程',
steps: [
{ name: 'clickUserDropdown', action: async () => {}, timeout: 5000 },
{ name: 'clickLogoutButton', action: async () => {}, timeout: 5000 },
{ name: 'verifyLogoutSuccess', action: async () => {}, timeout: 10000 }
]
}
};
/**
* 端到端业务流程
*/
export const EndToEndWorkflows = {
/**
* 完整用户生命周期流程
*/
userLifecycle: {
name: 'userLifecycle',
description: '用户从创建到删除的完整生命周期',
steps: [
{ name: 'login', action: async () => {}, timeout: 15000 },
{ name: 'createUser', action: async () => {}, timeout: 30000 },
{ name: 'verifyUserInList', action: async () => {}, timeout: 10000 },
{ name: 'editUser', action: async () => {}, timeout: 30000 },
{ name: 'verifyUserUpdated', action: async () => {}, timeout: 10000 },
{ name: 'deleteUser', action: async () => {}, timeout: 20000 },
{ name: 'verifyUserDeleted', action: async () => {}, timeout: 10000 },
{ name: 'logout', action: async () => {}, timeout: 15000 }
]
},
/**
* 权限管理完整流程
*/
permissionManagement: {
name: 'permissionManagement',
description: '创建角色并分配权限的完整流程',
steps: [
{ name: 'login', action: async () => {}, timeout: 15000 },
{ name: 'createRole', action: async () => {}, timeout: 30000 },
{ name: 'assignPermissions', action: async () => {}, timeout: 20000 },
{ name: 'createUserWithRole', action: async () => {}, timeout: 30000 },
{ name: 'verifyPermissions', action: async () => {}, timeout: 20000 },
{ name: 'cleanup', action: async () => {}, timeout: 30000 }
]
}
};
@@ -0,0 +1,254 @@
import { test } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
import { testDataGenerator, UserData, RoleData, MenuData } from './test-data';
export type DataStrategy = 'real' | 'mock' | 'hybrid';
export interface DataStrategyConfig {
strategy: DataStrategy;
mockEnabled: boolean;
realDataEnabled: boolean;
autoCleanup: boolean;
}
export interface DataSnapshot {
name: string;
timestamp: number;
data: Map<string, any[]>;
}
export class DataStrategyManager {
private strategy: DataStrategy;
private config: DataStrategyConfig;
private snapshots: Map<string, DataSnapshot> = new Map();
private testData: Map<string, any[]> = new Map();
constructor(config?: Partial<DataStrategyConfig>) {
this.config = {
strategy: 'hybrid',
mockEnabled: true,
realDataEnabled: true,
autoCleanup: true,
...config
};
this.strategy = this.config.strategy;
testLogger.info(`DataStrategyManager initialized with strategy: ${this.strategy}`);
}
setStrategy(strategy: DataStrategy): void {
this.strategy = strategy;
this.config.strategy = strategy;
testLogger.info(`Data strategy changed to: ${strategy}`);
}
getStrategy(): DataStrategy {
return this.strategy;
}
getConfig(): DataStrategyConfig {
return { ...this.config };
}
selectDataSource(testTags: string[]): DataStrategy {
testLogger.debug(`Selecting data source for tags: ${testTags.join(', ')}`);
if (this.config.strategy === 'real') {
testLogger.debug('Using real data strategy (forced)');
return 'real';
}
if (this.config.strategy === 'mock') {
testLogger.debug('Using mock data strategy (forced)');
return 'mock';
}
if (this.config.strategy === 'hybrid') {
return this.selectHybridDataSource(testTags);
}
return 'mock';
}
private selectHybridDataSource(testTags: string[]): DataStrategy {
const hasSmokeTag = testTags.includes('@smoke');
const hasRegressionTag = testTags.includes('@regression');
const hasFullTag = testTags.includes('@full');
const hasCriticalTag = testTags.includes('@critical');
if (hasCriticalTag) {
testLogger.debug('Hybrid strategy: Using real data for @critical tests');
return 'real';
}
if (hasFullTag) {
testLogger.debug('Hybrid strategy: Using real data for @full tests');
return 'real';
}
if (hasRegressionTag) {
testLogger.debug('Hybrid strategy: Using hybrid data for @regression tests');
return 'hybrid';
}
if (hasSmokeTag) {
testLogger.debug('Hybrid strategy: Using mock data for @smoke tests');
return 'mock';
}
testLogger.debug('Hybrid strategy: Defaulting to mock data');
return 'mock';
}
async createData(dataType: string, data: any, testTags: string[] = []): Promise<any> {
const dataSource = this.selectDataSource(testTags);
testLogger.info(`Creating ${dataType} using ${dataSource} data source`);
if (dataSource === 'mock') {
return this.createMockData(dataType, data);
}
if (dataSource === 'real') {
return this.createRealData(dataType, data);
}
return this.createHybridData(dataType, data);
}
private createMockData(dataType: string, data: any): any {
testLogger.debug(`Creating mock data for ${dataType}`);
switch (dataType) {
case 'user':
return testDataGenerator.generateUserData(data);
case 'role':
return testDataGenerator.generateRoleData(data);
case 'menu':
return testDataGenerator.generateMenuData(data);
case 'permission':
return testDataGenerator.generatePermissionData(data);
default:
return data;
}
}
private createRealData(dataType: string, data: any): any {
testLogger.debug(`Creating real data for ${dataType} (requires API connection)`);
return data;
}
private createHybridData(dataType: string, data: any): any {
testLogger.debug(`Creating hybrid data for ${dataType}`);
const mockData = this.createMockData(dataType, data);
const realData = this.createRealData(dataType, data);
return {
...mockData,
_dataSource: 'hybrid',
_mockData: mockData,
_realData: realData
};
}
async cleanupData(dataType: string, dataId: string | number): Promise<void> {
testLogger.info(`Cleaning up ${dataType} with id: ${dataId}`);
if (this.config.autoCleanup) {
this.removeTestData(dataType, dataId);
}
}
async cleanupAll(): Promise<void> {
testLogger.info('Cleaning up all test data');
this.testData.clear();
this.snapshots.clear();
testLogger.info('All test data cleaned up');
}
async createSnapshot(snapshotName: string): Promise<DataSnapshot> {
testLogger.info(`Creating snapshot: ${snapshotName}`);
const snapshot: DataSnapshot = {
name: snapshotName,
timestamp: Date.now(),
data: new Map(this.testData)
};
this.snapshots.set(snapshotName, snapshot);
testLogger.info(`Snapshot created: ${snapshotName} at ${new Date(snapshot.timestamp).toISOString()}`);
return snapshot;
}
async rollbackToSnapshot(snapshotName: string): Promise<void> {
testLogger.info(`Rolling back to snapshot: ${snapshotName}`);
const snapshot = this.snapshots.get(snapshotName);
if (!snapshot) {
throw new Error(`Snapshot not found: ${snapshotName}`);
}
this.testData = new Map(snapshot.data);
testLogger.info(`Rolled back to snapshot: ${snapshotName}`);
}
private addTestData(dataType: string, data: any): void {
if (!this.testData.has(dataType)) {
this.testData.set(dataType, []);
}
this.testData.get(dataType)!.push(data);
}
private removeTestData(dataType: string, dataId: string | number): void {
const items = this.testData.get(dataType);
if (items) {
const index = items.findIndex(item => item.id === dataId);
if (index !== -1) {
items.splice(index, 1);
}
}
}
getTestData(dataType: string): any[] {
return this.testData.get(dataType) || [];
}
getSnapshot(snapshotName: string): DataSnapshot | undefined {
return this.snapshots.get(snapshotName);
}
getAllSnapshots(): DataSnapshot[] {
return Array.from(this.snapshots.values());
}
deleteSnapshot(snapshotName: string): void {
this.snapshots.delete(snapshotName);
testLogger.info(`Snapshot deleted: ${snapshotName}`);
}
getStatistics(): {
totalTestData: number;
totalSnapshots: number;
strategy: DataStrategy;
config: DataStrategyConfig;
} {
let totalTestData = 0;
const testDataValues = Array.from(this.testData.values());
for (const items of testDataValues) {
totalTestData += items.length;
}
return {
totalTestData,
totalSnapshots: this.snapshots.size,
strategy: this.strategy,
config: this.getConfig()
};
}
}
export const dataStrategyManager = new DataStrategyManager();
@@ -0,0 +1,303 @@
import { Page, Route } from '@playwright/test';
/**
* Mock管理器
* 提供统一的API Mock功能
*/
export interface MockConfig {
url: string | RegExp;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
status?: number;
body?: unknown;
headers?: Record<string, string>;
delay?: number;
}
export class MockManager {
private page: Page;
private mocks: Map<string, MockConfig> = new Map();
private isEnabled: boolean = false;
constructor(page: Page) {
this.page = page;
}
/**
* 启用Mock
*/
enable(): void {
this.isEnabled = true;
}
/**
* 禁用Mock
*/
disable(): void {
this.isEnabled = false;
}
/**
* 检查Mock是否启用
*/
isMockEnabled(): boolean {
return this.isEnabled;
}
/**
* 添加Mock配置
*/
addMock(config: MockConfig): void {
const key = this.getMockKey(config.url, config.method || 'GET');
this.mocks.set(key, config);
}
/**
* 移除Mock配置
*/
removeMock(url: string | RegExp, method: string = 'GET'): void {
const key = this.getMockKey(url, method);
this.mocks.delete(key);
}
/**
* 清除所有Mock
*/
clearMocks(): void {
this.mocks.clear();
}
/**
* 应用所有Mock
*/
async applyMocks(): Promise<void> {
if (!this.isEnabled) {
return;
}
await this.page.route('**/*', async (route: Route) => {
const request = route.request();
const url = request.url();
const method = request.method();
// 查找匹配的Mock配置
for (const config of this.mocks.values()) {
if (this.matchesUrl(url, config.url) && method === (config.method || 'GET')) {
// 模拟延迟
if (config.delay) {
await new Promise(resolve => setTimeout(resolve, config.delay));
}
// 返回Mock响应
await route.fulfill({
status: config.status || 200,
contentType: 'application/json',
headers: config.headers || {},
body: JSON.stringify(config.body || {}),
});
return;
}
}
// 没有匹配的Mock,继续正常请求
await route.continue();
});
}
/**
* Mock登录API
*/
mockLogin(success: boolean = true, delay: number = 500): void {
this.addMock({
url: /.*\/auth\/login/,
method: 'POST',
status: success ? 200 : 401,
delay,
body: success
? {
token: 'mock-token-' + Date.now(),
refreshToken: 'mock-refresh-token',
user: {
id: 1,
username: 'admin',
realName: '管理员',
email: 'admin@example.com',
avatar: '',
status: 'active',
},
permissions: ['*'],
}
: {
message: '用户名或密码错误',
code: 401,
},
});
}
/**
* Mock用户列表API
*/
mockUserList(count: number = 10, delay: number = 300): void {
const users = Array.from({ length: count }, (_, i) => ({
id: i + 1,
username: `user${i + 1}`,
realName: `用户${i + 1}`,
email: `user${i + 1}@example.com`,
phone: `138001380${String(i).padStart(2, '0')}`,
status: i % 3 === 0 ? 'inactive' : 'active',
createTime: new Date().toISOString(),
}));
this.addMock({
url: /.*\/user\/list/,
method: 'GET',
status: 200,
delay,
body: {
data: users,
total: count,
page: 1,
pageSize: 10,
},
});
}
/**
* Mock角色列表API
*/
mockRoleList(delay: number = 300): void {
const roles = [
{ id: 1, roleName: '超级管理员', roleKey: 'admin', status: 'active' },
{ id: 2, roleName: '普通用户', roleKey: 'user', status: 'active' },
{ id: 3, roleName: '访客', roleKey: 'guest', status: 'inactive' },
];
this.addMock({
url: /.*\/role\/list/,
method: 'GET',
status: 200,
delay,
body: {
data: roles,
total: roles.length,
},
});
}
/**
* Mock菜单列表API
*/
mockMenuList(delay: number = 300): void {
const menus = [
{ id: 1, menuName: '仪表盘', path: '/dashboard', icon: 'DashboardOutlined', status: 'active' },
{ id: 2, menuName: '系统管理', path: '/sys', icon: 'SettingOutlined', status: 'active' },
{ id: 3, menuName: '用户管理', path: '/sys/user', parentId: 2, status: 'active' },
{ id: 4, menuName: '角色管理', path: '/sys/role', parentId: 2, status: 'active' },
];
this.addMock({
url: /.*\/menu\/list/,
method: 'GET',
status: 200,
delay,
body: {
data: menus,
total: menus.length,
},
});
}
/**
* Mock创建操作
*/
mockCreate(resource: string, delay: number = 500): void {
this.addMock({
url: new RegExp(`.*\\/${resource}$`),
method: 'POST',
status: 200,
delay,
body: {
message: '创建成功',
code: 200,
data: { id: Date.now() },
},
});
}
/**
* Mock更新操作
*/
mockUpdate(resource: string, delay: number = 500): void {
this.addMock({
url: new RegExp(`.*\\/${resource}\\/.*`),
method: 'PUT',
status: 200,
delay,
body: {
message: '更新成功',
code: 200,
},
});
}
/**
* Mock删除操作
*/
mockDelete(resource: string, delay: number = 500): void {
this.addMock({
url: new RegExp(`.*\\/${resource}\\/.*`),
method: 'DELETE',
status: 200,
delay,
body: {
message: '删除成功',
code: 200,
},
});
}
/**
* Mock错误响应
*/
mockError(url: string | RegExp, status: number = 500, message: string = '服务器错误'): void {
this.addMock({
url,
method: 'GET',
status,
body: {
message,
code: status,
},
});
}
/**
* Mock网络延迟
*/
mockDelay(url: string | RegExp, delay: number = 2000): void {
this.addMock({
url,
method: 'GET',
delay,
body: {},
});
}
/**
* 获取Mock键
*/
private getMockKey(url: string | RegExp, method: string): string {
const urlStr = url instanceof RegExp ? url.source : url;
return `${method}:${urlStr}`;
}
/**
* 检查URL是否匹配
*/
private matchesUrl(actualUrl: string, configUrl: string | RegExp): boolean {
if (configUrl instanceof RegExp) {
return configUrl.test(actualUrl);
}
return actualUrl.includes(configUrl);
}
}
@@ -0,0 +1,59 @@
export interface TestEnvironment {
name: string;
baseURL: string;
apiBaseURL: string;
uniappBaseURL: string;
mockEnabled: boolean;
timeout: number;
credentials: {
username: string;
password: string;
};
}
export class TestConfig {
private static instance: TestConfig;
private currentEnv: TestEnvironment;
private constructor() {
this.currentEnv = this.loadEnvironment();
}
static getInstance(): TestConfig {
if (!TestConfig.instance) {
TestConfig.instance = new TestConfig();
}
return TestConfig.instance;
}
getEnvironment(): TestEnvironment {
return this.currentEnv;
}
getBaseURL(): string {
return this.currentEnv.baseURL;
}
setEnvironment(envName: string): void {
this.currentEnv = this.loadEnvironment(envName);
}
private loadEnvironment(envName?: string): TestEnvironment {
const name = envName || process.env.TEST_ENV || 'local';
return {
name,
baseURL: process.env.ADMIN_BASE_URL || 'http://localhost:5174',
apiBaseURL: process.env.API_BASE_URL || 'http://127.0.0.1:8080',
uniappBaseURL: process.env.UNIAPP_BASE_URL || 'http://localhost:8081',
mockEnabled: process.env.MOCK_ENABLED === 'true',
timeout: parseInt(process.env.TEST_TIMEOUT || '30000'),
credentials: {
username: process.env.TEST_USERNAME || 'admin',
password: process.env.TEST_PASSWORD || 'admin123'
}
};
}
}
export const testConfig = TestConfig.getInstance();
@@ -0,0 +1,529 @@
import { test } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
import * as fs from 'fs';
import * as path from 'path';
export interface TestCoverage {
totalTests: number;
passedTests: number;
failedTests: number;
skippedTests: number;
passRate: number;
testSuites: TestSuiteCoverage[];
executionTime: number;
timestamp: string;
}
export interface TestSuiteCoverage {
name: string;
totalTests: number;
passedTests: number;
failedTests: number;
skippedTests: number;
passRate: number;
tests: TestCaseCoverage[];
}
export interface TestCaseCoverage {
name: string;
status: 'passed' | 'failed' | 'skipped';
duration: number;
tags: string[];
file: string;
}
export class TestCoverageReporter {
private coverageData: TestCoverage;
private testResults: Map<string, TestCaseCoverage[]> = new Map();
private suiteResults: Map<string, TestSuiteCoverage> = new Map();
private startTime: number = 0;
private endTime: number = 0;
constructor() {
this.coverageData = {
totalTests: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
passRate: 0,
testSuites: [],
executionTime: 0,
timestamp: new Date().toISOString()
};
}
startCoverage(): void {
this.startTime = Date.now();
testLogger.info('开始收集测试覆盖率数据');
}
endCoverage(): void {
this.endTime = Date.now();
this.coverageData.executionTime = this.endTime - this.startTime;
this.calculateCoverage();
this.generateReport();
testLogger.info('测试覆盖率收集完成');
testLogger.info(`总测试数: ${this.coverageData.totalTests}`);
testLogger.info(`通过测试数: ${this.coverageData.passedTests}`);
testLogger.info(`失败测试数: ${this.coverageData.failedTests}`);
testLogger.info(`跳过测试数: ${this.coverageData.skippedTests}`);
testLogger.info(`通过率: ${this.coverageData.passRate.toFixed(2)}%`);
}
recordTestResult(suiteName: string, testName: string, status: 'passed' | 'failed' | 'skipped', duration: number, tags: string[], file: string): void {
const testCase: TestCaseCoverage = {
name: testName,
status,
duration,
tags,
file
};
if (!this.testResults.has(suiteName)) {
this.testResults.set(suiteName, []);
}
this.testResults.get(suiteName)!.push(testCase);
}
private calculateCoverage(): void {
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
let skippedTests = 0;
for (const [suiteName, testCases] of this.testResults.entries()) {
const suiteCoverage = this.calculateSuiteCoverage(suiteName, testCases);
this.suiteResults.set(suiteName, suiteCoverage);
totalTests += suiteCoverage.totalTests;
passedTests += suiteCoverage.passedTests;
failedTests += suiteCoverage.failedTests;
skippedTests += suiteCoverage.skippedTests;
}
this.coverageData.totalTests = totalTests;
this.coverageData.passedTests = passedTests;
this.coverageData.failedTests = failedTests;
this.coverageData.skippedTests = skippedTests;
this.coverageData.passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0;
this.coverageData.testSuites = Array.from(this.suiteResults.values());
}
private calculateSuiteCoverage(suiteName: string, testCases: TestCaseCoverage[]): TestSuiteCoverage {
const totalTests = testCases.length;
const passedTests = testCases.filter(tc => tc.status === 'passed').length;
const failedTests = testCases.filter(tc => tc.status === 'failed').length;
const skippedTests = testCases.filter(tc => tc.status === 'skipped').length;
const passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0;
return {
name: suiteName,
totalTests,
passedTests,
failedTests,
skippedTests,
passRate,
tests: testCases
};
}
private generateReport(): void {
const reportDir = path.join(process.cwd(), 'test-results', 'coverage');
if (!fs.existsSync(reportDir)) {
fs.mkdirSync(reportDir, { recursive: true });
}
this.generateJSONReport(reportDir);
this.generateHTMLReport(reportDir);
this.generateMarkdownReport(reportDir);
this.generateConsoleReport();
}
private generateJSONReport(reportDir: string): void {
const jsonPath = path.join(reportDir, 'coverage.json');
fs.writeFileSync(jsonPath, JSON.stringify(this.coverageData, null, 2), 'utf-8');
testLogger.info(`JSON覆盖率报告已生成: ${jsonPath}`);
}
private generateHTMLReport(reportDir: string): void {
const htmlPath = path.join(reportDir, 'coverage.html');
const html = this.generateHTMLContent();
fs.writeFileSync(htmlPath, html, 'utf-8');
testLogger.info(`HTML覆盖率报告已生成: ${htmlPath}`);
}
private generateMarkdownReport(reportDir: string): void {
const mdPath = path.join(reportDir, 'coverage.md');
const markdown = this.generateMarkdownContent();
fs.writeFileSync(mdPath, markdown, 'utf-8');
testLogger.info(`Markdown覆盖率报告已生成: ${mdPath}`);
}
private generateHTMLContent(): string {
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData;
const passRateColor = passRate >= 80 ? '#52c41a' : passRate >= 60 ? '#faad14' : '#f5222d';
const passRateClass = passRate >= 80 ? 'success' : passRate >= 60 ? 'warning' : 'danger';
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试覆盖率报告</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 32px;
font-weight: 600;
}
.header p {
margin: 10px 0 0 0;
font-size: 14px;
opacity: 0.9;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background-color: #fafafa;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
text-align: center;
}
.summary-card .label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.summary-card .value {
font-size: 28px;
font-weight: 600;
color: #333;
}
.summary-card .value.${passRateClass} {
color: ${passRateColor};
}
.suites {
padding: 30px;
}
.suites h2 {
margin: 0 0 20px 0;
font-size: 24px;
color: #333;
}
.suite {
margin-bottom: 30px;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
}
.suite-header {
background-color: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
}
.suite-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.suite-stats {
display: flex;
gap: 20px;
font-size: 14px;
}
.suite-stats span {
padding: 4px 12px;
border-radius: 12px;
font-weight: 500;
}
.suite-stats .passed {
background-color: #e6f7ff;
color: #4a5568;
}
.suite-stats .failed {
background-color: #fee2e2;
color: #991b1b;
}
.suite-stats .skipped {
background-color: #fef3c7;
color: #92400e;
}
.suite-stats .rate {
background-color: ${passRateColor};
color: white;
}
.test-cases {
padding: 20px;
}
.test-case {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
}
.test-case:last-child {
border-bottom: none;
}
.test-case .name {
flex: 1;
font-size: 14px;
color: #333;
}
.test-case .status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.test-case .status.passed {
background-color: #d4edda;
color: #0f5132;
}
.test-case .status.failed {
background-color: #f8d7da;
color: #721c24;
}
.test-case .status.skipped {
background-color: #fff3cd;
color: #856404;
}
.test-case .duration {
font-size: 12px;
color: #999;
min-width: 80px;
text-align: right;
}
.test-case .tags {
display: flex;
gap: 5px;
}
.test-case .tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
background-color: #e9ecef;
color: #495057;
}
.footer {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 测试覆盖率报告</h1>
<p>生成时间: ${timestamp}</p>
<p>执行时间: ${(executionTime / 1000).toFixed(2)}秒</p>
</div>
<div class="summary">
<div class="summary-card">
<div class="label">总测试数</div>
<div class="value">${totalTests}</div>
</div>
<div class="summary-card">
<div class="label">通过测试</div>
<div class="value" style="color: #52c41a">${passedTests}</div>
</div>
<div class="summary-card">
<div class="label">失败测试</div>
<div class="value" style="color: #f5222d">${failedTests}</div>
</div>
<div class="summary-card">
<div class="label">跳过测试</div>
<div class="value" style="color: #faad14">${skippedTests}</div>
</div>
<div class="summary-card">
<div class="label">通过率</div>
<div class="value ${passRateClass}">${passRate.toFixed(2)}%</div>
</div>
</div>
<div class="suites">
<h2>📊 测试套件详情</h2>
${testSuites.map(suite => `
<div class="suite">
<div class="suite-header">
<h3>${suite.name}</h3>
<div class="suite-stats">
<span class="passed">✓ ${suite.passedTests}</span>
<span class="failed">✗ ${suite.failedTests}</span>
<span class="skipped">⊘ ${suite.skippedTests}</span>
<span class="rate">${suite.passRate.toFixed(1)}%</span>
</div>
</div>
<div class="test-cases">
${suite.tests.map(testCase => `
<div class="test-case">
<div class="name">${testCase.name}</div>
<div class="status ${testCase.status}">${testCase.status}</div>
<div class="duration">${testCase.duration}ms</div>
<div class="tags">
${testCase.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
<div class="footer">
<p>Generated by TestCoverageReporter</p>
</div>
</div>
</body>
</html>
`;
}
private generateMarkdownContent(): string {
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData;
return `# 测试覆盖率报告
## 概要
- **生成时间**: ${timestamp}
- **执行时间**: ${(executionTime / 1000).toFixed(2)}
- **总测试数**: ${totalTests}
- **通过测试**: ${passedTests}
- **失败测试**: ${failedTests}
- **跳过测试**: ${skippedTests}
- **通过率**: ${passRate.toFixed(2)}%
## 测试套件详情
${testSuites.map(suite => `
### ${suite.name}
- **总测试数**: ${suite.totalTests}
- **通过测试**: ${suite.passedTests}
- **失败测试**: ${suite.failedTests}
- **跳过测试**: ${suite.skippedTests}
- **通过率**: ${suite.passRate.toFixed(2)}%
#### 测试用例
| 测试用例 | 状态 | 耗时 | 标签 |
|---------|------|------|------|
${suite.tests.map(testCase => `
| ${testCase.name} | ${testCase.status} | ${testCase.duration}ms | ${testCase.tags.join(', ')} |
`).join('')}
`).join('')}
## 总结
${passRate >= 80 ? '✅ 测试覆盖率优秀' : passRate >= 60 ? '⚠️ 测试覆盖率良好' : '❌ 测试覆盖率需要改进'}
---
*Generated by TestCoverageReporter*
`;
}
private generateConsoleReport(): void {
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime } = this.coverageData;
console.log('\n' + '='.repeat(60));
console.log('📊 测试覆盖率报告');
console.log('='.repeat(60));
console.log(`生成时间: ${this.coverageData.timestamp}`);
console.log(`执行时间: ${(executionTime / 1000).toFixed(2)}`);
console.log('');
console.log('📈 总体统计:');
console.log(` 总测试数: ${totalTests}`);
console.log(` 通过测试: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`);
console.log(` 失败测试: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`);
console.log(` 跳过测试: ${skippedTests} (${(skippedTests / totalTests * 100).toFixed(2)}%)`);
console.log(` 通过率: ${passRate.toFixed(2)}%`);
console.log('');
console.log('📋 测试套件详情:');
for (const suite of this.coverageData.testSuites) {
console.log(`\n ${suite.name}:`);
console.log(` 总测试数: ${suite.totalTests}`);
console.log(` 通过测试: ${suite.passedTests} (${suite.passRate.toFixed(2)}%)`);
console.log(` 失败测试: ${suite.failedTests}`);
console.log(` 跳过测试: ${suite.skippedTests}`);
}
console.log('\n' + '='.repeat(60));
if (passRate >= 80) {
console.log('✅ 测试覆盖率优秀');
} else if (passRate >= 60) {
console.log('⚠️ 测试覆盖率良好');
} else {
console.log('❌ 测试覆盖率需要改进');
}
console.log('='.repeat(60) + '\n');
}
getCoverage(): TestCoverage {
return this.coverageData;
}
getSuiteCoverage(suiteName: string): TestSuiteCoverage | undefined {
return this.suiteResults.get(suiteName);
}
exportCoverage(format: 'json' | 'html' | 'markdown' = 'json'): string {
switch (format) {
case 'json':
return JSON.stringify(this.coverageData, null, 2);
case 'html':
return this.generateHTMLContent();
case 'markdown':
return this.generateMarkdownContent();
default:
return JSON.stringify(this.coverageData, null, 2);
}
}
}
export const testCoverageReporter = new TestCoverageReporter();
@@ -0,0 +1,254 @@
import { testDataGenerator, UserData } from './test-data';
export class TestDataManager {
private request: APIRequestContext;
private authToken: string;
private testData: Map<string, any[]> = new Map();
constructor(request: APIRequestContext, authToken: string) {
this.request = request;
this.authToken = authToken;
}
async createTestUser(overrides: Partial<User> = {}): Promise<User> {
const userData = TestDataFactory.generateUser(overrides);
const response = await this.request.post('/api/sys/user', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
data: userData
});
const body = await response.json();
const user = body.data || body;
this.addTestData('user', user);
return user;
}
async createTestUserFromGenerator(overrides: Partial<UserData> = {}): Promise<UserData> {
const userData = testDataGenerator.generateUserData(overrides);
const response = await this.request.post('/api/sys/user', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
data: userData
});
const body = await response.json();
const user = body.data || body;
this.addTestData('user', user);
return user;
}
async createTestRole(overrides: Partial<Role> = {}): Promise<Role> {
const roleData = TestDataFactory.generateRole(overrides);
const response = await this.request.post('/api/sys/role', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
data: roleData
});
const body = await response.json();
const role = body.data || body;
this.addTestData('role', role);
return role;
}
async createTestMenu(overrides: Partial<Menu> = {}): Promise<Menu> {
const menuData = TestDataFactory.generateMenu(overrides);
const response = await this.request.post('/api/sys/menu', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
data: menuData
});
const body = await response.json();
const menu = body.data || body;
this.addTestData('menu', menu);
return menu;
}
async getTestUser(username: string): Promise<User | null> {
const response = await this.request.get(`/api/sys/user/username/${username}`, {
headers: {
'Authorization': `Bearer ${this.authToken}`
}
});
if (response.status() === 200) {
const body = await response.json();
return body.data || body;
}
return null;
}
async updateTestUser(userId: number | string, updates: Partial<User>): Promise<User> {
const response = await this.request.put('/api/sys/user', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
data: { id: userId, ...updates }
});
const body = await response.json();
return body.data || body;
}
async deleteTestUser(userId: number | string): Promise<void> {
await this.request.delete(`/api/sys/user/${userId}`, {
headers: {
'Authorization': `Bearer ${this.authToken}`
}
});
this.removeTestData('user', userId);
}
async deleteTestRole(roleId: number | string): Promise<void> {
await this.request.delete(`/api/sys/role/${roleId}`, {
headers: {
'Authorization': `Bearer ${this.authToken}`
}
});
this.removeTestData('role', roleId);
}
async deleteTestMenu(menuId: number | string): Promise<void> {
await this.request.delete(`/api/sys/menu/${menuId}`, {
headers: {
'Authorization': `Bearer ${this.authToken}`
}
});
this.removeTestData('menu', menuId);
}
async cleanup(): Promise<void> {
const cleanupPromises: Promise<void>[] = [];
for (const [type, items] of this.testData) {
for (const item of items) {
if (item.id) {
switch (type) {
case 'user':
cleanupPromises.push(this.deleteTestUser(item.id));
break;
case 'role':
cleanupPromises.push(this.deleteTestRole(item.id));
break;
case 'menu':
cleanupPromises.push(this.deleteTestMenu(item.id));
break;
}
}
}
}
await Promise.all(cleanupPromises);
this.testData.clear();
}
private addTestData(type: string, data: any): void {
if (!this.testData.has(type)) {
this.testData.set(type, []);
}
this.testData.get(type)!.push(data);
}
private removeTestData(type: string, id: number | string): void {
const items = this.testData.get(type);
if (items) {
const index = items.findIndex(item => item.id === id);
if (index !== -1) {
items.splice(index, 1);
}
}
}
}
export class TestDataFactory {
static generateUser(overrides: Partial<User> = {}): User {
const timestamp = Date.now();
return {
username: `e2e_test_user_${timestamp}`,
password: 'Test@123456',
realName: 'E2E测试用户',
email: `e2e_${timestamp}@example.com`,
phone: '13800138000',
status: 1,
gender: 1,
...overrides
};
}
static generateRole(overrides: Partial<Role> = {}): Role {
const timestamp = Date.now();
return {
roleName: `E2E测试角色_${timestamp}`,
roleCode: `e2e_test_role_${timestamp}`,
description: 'E2E测试角色描述',
status: 1,
...overrides
};
}
static generateMenu(overrides: Partial<Menu> = {}): Menu {
const timestamp = Date.now();
return {
name: `E2E测试菜单_${timestamp}`,
code: `e2e_test_menu_${timestamp}`,
path: `/e2e-test-menu-${timestamp}`,
icon: 'SettingOutlined',
sortOrder: 10,
status: 1,
parentId: 0,
...overrides
};
}
}
export interface User {
id?: number | string;
username: string;
password?: string;
realName?: string;
email?: string;
phone?: string;
status?: number;
gender?: number;
}
export interface Role {
id?: number | string;
roleName: string;
roleCode: string;
description?: string;
status?: number;
}
export interface Menu {
id?: number | string;
name: string;
code: string;
path?: string;
icon?: string;
sortOrder?: number;
status?: number;
parentId?: number | string;
}
@@ -0,0 +1,210 @@
import { randomBytes } from 'crypto';
export interface UserData {
username: string;
password: string;
email: string;
phone: string;
realName: string;
status: 'active' | 'inactive' | 'locked';
roleIds: number[];
}
export interface RoleData {
name: string;
code: string;
description: string;
status: 'active' | 'inactive';
permissions: string[];
}
export interface MenuData {
name: string;
code: string;
path: string;
icon: string;
parentId: number;
sortOrder: number;
status: 'active' | 'inactive';
}
export interface PermissionData {
name: string;
code: string;
description: string;
type: 'menu' | 'button' | 'api';
parentId: number;
}
class TestDataGenerator {
private static instance: TestDataGenerator;
private constructor() {}
static getInstance(): TestDataGenerator {
if (!TestDataGenerator.instance) {
TestDataGenerator.instance = new TestDataGenerator();
}
return TestDataGenerator.instance;
}
randomString(length: number = 10): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
randomEmail(): string {
const domains = ['example.com', 'test.com', 'demo.com'];
const username = this.randomString(8).toLowerCase();
const domain = domains[Math.floor(Math.random() * domains.length)];
return `${username}@${domain}`;
}
randomPhone(): string {
const prefix = ['138', '139', '150', '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}`;
}
randomPassword(length: number = 12): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomBoolean(): boolean {
return Math.random() < 0.5;
}
randomDate(startYear: number = 2020, endYear: number = 2024): Date {
const start = new Date(startYear, 0, 1);
const end = new Date(endYear, 11, 31);
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
}
randomItem<T>(items: T[]): T {
return items[Math.floor(Math.random() * items.length)];
}
randomItems<T>(items: T[], count: number): T[] {
const shuffled = [...items].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(count, items.length));
}
generateUserData(overrides: Partial<UserData> = {}): UserData {
const username = overrides.username || `user_${this.randomString(6).toLowerCase()}`;
return {
username,
password: overrides.password || 'Admin@123',
email: overrides.email || this.randomEmail(),
phone: overrides.phone || this.randomPhone(),
realName: overrides.realName || `测试用户${this.randomInt(1, 100)}`,
status: overrides.status || this.randomItem(['active', 'inactive', 'locked']),
roleIds: overrides.roleIds || [1]
};
}
generateRoleData(overrides: Partial<RoleData> = {}): RoleData {
const code = overrides.code || `role_${this.randomString(6).toLowerCase()}`;
return {
name: overrides.name || `测试角色${this.randomInt(1, 100)}`,
code,
description: overrides.description || `角色${code}的描述`,
status: overrides.status || this.randomItem(['active', 'inactive']),
permissions: overrides.permissions || ['dashboard:view']
};
}
generateMenuData(overrides: Partial<MenuData> = {}): MenuData {
const code = overrides.code || `menu_${this.randomString(6).toLowerCase()}`;
return {
name: overrides.name || `测试菜单${this.randomInt(1, 100)}`,
code,
path: overrides.path || `/${code}`,
icon: overrides.icon || 'MenuOutlined',
parentId: overrides.parentId || 0,
sortOrder: overrides.sortOrder || this.randomInt(1, 100),
status: overrides.status || this.randomItem(['active', 'inactive'])
};
}
generatePermissionData(overrides: Partial<PermissionData> = {}): PermissionData {
const code = overrides.code || `perm_${this.randomString(6).toLowerCase()}`;
return {
name: overrides.name || `测试权限${this.randomInt(1, 100)}`,
code,
description: overrides.description || `权限${code}的描述`,
type: overrides.type || this.randomItem(['menu', 'button', 'api']),
parentId: overrides.parentId || 0
};
}
generateUserList(count: number): UserData[] {
return Array.from({ length: count }, () => this.generateUserData());
}
generateRoleList(count: number): RoleData[] {
return Array.from({ length: count }, () => this.generateRoleData());
}
generateMenuList(count: number): MenuData[] {
return Array.from({ length: count }, () => this.generateMenuData());
}
generatePermissionList(count: number): PermissionData[] {
return Array.from({ length: count }, () => this.generatePermissionData());
}
generatePaginationData<T>(data: T[], page: number = 1, pageSize: number = 10) {
const start = (page - 1) * pageSize;
const end = start + pageSize;
return {
records: data.slice(start, end),
total: data.length,
page,
pageSize,
totalPages: Math.ceil(data.length / pageSize)
};
}
generateSearchQuery(keyword: string): Record<string, any> {
return {
keyword,
page: 1,
pageSize: 10
};
}
generateFormData(fields: Record<string, any>): Record<string, any> {
const formData: Record<string, any> = {};
for (const [key, value] of Object.entries(fields)) {
if (typeof value === 'function') {
formData[key] = value();
} else {
formData[key] = value;
}
}
return formData;
}
}
export const testDataGenerator = TestDataGenerator.getInstance();
@@ -0,0 +1,109 @@
export class TestLogger {
private logs: LogEntry[] = [];
private currentTest: string | null = null;
private currentStep: string | null = null;
startTest(testName: string): void {
this.currentTest = testName;
this.currentStep = null;
this.log('info', `开始测试: ${testName}`);
}
endTest(testName: string, status: 'passed' | 'failed', error?: Error): void {
this.log('info', `结束测试: ${testName} - ${status}`);
if (error) {
this.log('error', `测试失败: ${error.message}`, error);
}
this.currentTest = null;
this.currentStep = null;
}
startStep(stepName: string): void {
this.currentStep = stepName;
this.log('info', ` 开始步骤: ${stepName}`);
}
endStep(stepName: string, status: 'passed' | 'failed', error?: Error): void {
this.log('info', ` 结束步骤: ${stepName} - ${status}`);
if (error) {
this.log('error', ` 步骤失败: ${error.message}`, error);
}
this.currentStep = null;
}
debug(message: string): void {
this.log('debug', message);
}
info(message: string): void {
this.log('info', message);
}
warn(message: string): void {
this.log('warn', message);
}
error(message: string, error?: Error): void {
this.log('error', message, error);
}
success(message: string): void {
this.log('info', `${message}`);
}
private log(level: LogLevel, message: string, error?: Error): void {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
test: this.currentTest,
step: this.currentStep,
error
};
this.logs.push(entry);
this.printLog(entry);
}
private printLog(entry: LogEntry): void {
const timestamp = entry.timestamp.split('T')[1].split('.')[0];
const prefix = entry.step ? ` ${entry.step}` : entry.test || 'SYSTEM';
const levelIcon = {
debug: '🔍',
info: '️',
warn: '⚠️',
error: '❌'
}[entry.level];
console.log(`${timestamp} ${levelIcon} [${prefix}] ${entry.message}`);
if (entry.error) {
const errorMessage = entry.error.stack || entry.error.message || String(entry.error);
console.error(errorMessage);
}
}
getLogs(): LogEntry[] {
return this.logs;
}
clearLogs(): void {
this.logs = [];
}
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
}
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
test?: string | null;
step?: string | null;
error?: Error;
}
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export const testLogger = new TestLogger();
@@ -0,0 +1,208 @@
import { promises as fs } from 'fs';
import { join } from 'path';
export class TestReporter {
private results: TestResult[] = [];
private startTime: number = 0;
startReport(): void {
this.startTime = Date.now();
console.log('📊 开始生成测试报告');
}
recordResult(result: TestResult): void {
this.results.push(result);
}
async generateAllReports(outputDir: string): Promise<void> {
await this.generateJSONReport(join(outputDir, 'test-results.json'));
await this.generateHTMLReport(join(outputDir, 'test-results.html'));
await this.generateJUnitReport(join(outputDir, 'junit-report.xml'));
await this.generateSummaryReport(outputDir);
}
async generateJSONReport(outputPath: string): Promise<void> {
const report = {
summary: this.getSummary(),
results: this.results,
metadata: {
generatedAt: new Date().toISOString(),
duration: Date.now() - this.startTime
}
};
await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
console.log(`✅ JSON报告已生成: ${outputPath}`);
}
async generateHTMLReport(outputPath: string): Promise<void> {
const summary = this.getSummary();
const html = this.generateHTML(summary, this.results);
await fs.writeFile(outputPath, html);
console.log(`✅ HTML报告已生成: ${outputPath}`);
}
async generateJUnitReport(outputPath: string): Promise<void> {
const summary = this.getSummary();
const xml = this.generateJUnitXML(summary, this.results);
await fs.writeFile(outputPath, xml);
console.log(`✅ JUnit报告已生成: ${outputPath}`);
}
async generateSummaryReport(outputDir: string): Promise<void> {
const summary = this.getSummary();
const summaryPath = join(outputDir, 'summary.txt');
const summaryText = this.generateSummaryText(summary);
await fs.writeFile(summaryPath, summaryText);
console.log(`✅ 摘要报告已生成: ${summaryPath}`);
}
private getSummary(): TestSummary {
const passed = this.results.filter(r => r.status === 'passed').length;
const failed = this.results.filter(r => r.status === 'failed').length;
const skipped = this.results.filter(r => r.status === 'skipped').length;
const total = this.results.length;
const duration = this.results.reduce((sum, r) => sum + r.duration, 0);
return {
total,
passed,
failed,
skipped,
passRate: total > 0 ? (passed / total * 100).toFixed(2) : '0',
duration,
startTime: new Date(this.startTime).toISOString(),
endTime: new Date().toISOString()
};
}
private generateHTML(summary: TestSummary, results: TestResult[]): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E2E测试报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.passed { color: green; }
.failed { color: red; }
.skipped { color: orange; }
.test-result { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 5px; }
.test-result.failed { background: #ffe6e6; }
.test-result.passed { background: #e6ffe6; }
.test-result.skipped { background: #fff3cd; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f2f2f2; }
</style>
</head>
<body>
<h1>E2E测试报告</h1>
<div class="summary">
<h2>测试摘要</h2>
<p>总测试数: ${summary.total}</p>
<p>通过: <span class="passed">${summary.passed}</span></p>
<p>失败: <span class="failed">${summary.failed}</span></p>
<p>跳过: <span class="skipped">${summary.skipped}</span></p>
<p>通过率: ${summary.passRate}%</p>
<p>总耗时: ${summary.duration}ms</p>
</div>
<h2>测试结果</h2>
${results.map(result => `
<div class="test-result ${result.status}">
<h3>${result.testName}</h3>
<p>状态: <span class="${result.status}">${result.status}</span></p>
<p>耗时: ${result.duration}ms</p>
${result.error ? `<p>错误: ${result.error.message}</p>` : ''}
${result.steps.length > 0 ? `
<h4>测试步骤</h4>
<ul>
${result.steps.map(step => `
<li>${step.name} - <span class="${step.status}">${step.status}</span></li>
`).join('')}
</ul>
` : ''}
</div>
`).join('')}
</body>
</html>
`;
}
private generateJUnitXML(summary: TestSummary, results: TestResult[]): string {
const testCases = results.map(result => {
const testCase = `
<testcase name="${result.testName}" time="${result.duration / 1000}">
${result.status === 'failed' ? `
<failure message="${result.error?.message || 'Test failed'}">
${result.error?.stack || ''}
</failure>
` : ''}
${result.status === 'skipped' ? '<skipped/>' : ''}
</testcase>`;
return testCase;
}).join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="E2E Tests" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${summary.duration / 1000}">
${testCases}
</testsuite>
</testsuites>`;
}
private generateSummaryText(summary: TestSummary): string {
return `
========================================
E2E测试摘要报告
========================================
测试时间: ${summary.startTime} - ${summary.endTime}
总耗时: ${summary.duration}ms
测试统计:
----------------------------------------
总测试数: ${summary.total}
通过: ${summary.passed}
失败: ${summary.failed}
跳过: ${summary.skipped}
通过率: ${summary.passRate}%
========================================
`;
}
}
export interface TestResult {
testName: string;
status: 'passed' | 'failed' | 'skipped';
duration: number;
steps: TestStep[];
logs: string[];
screenshots: string[];
error?: Error;
}
export interface TestStep {
name: string;
status: 'passed' | 'failed';
duration: number;
}
export interface TestSummary {
total: number;
passed: number;
failed: number;
skipped: number;
passRate: string;
duration: number;
startTime: string;
endTime: string;
}
export const testReporter = new TestReporter();
@@ -0,0 +1,276 @@
/**
* 工作流执行器
* 负责执行业务流程工作流,支持重试、回滚和错误处理
*/
import { BusinessWorkflow, WorkflowStep } from './business-workflows.js';
import { TestLogger } from './test-logger.js';
export interface WorkflowExecutionResult {
success: boolean;
workflowName: string;
completedSteps: string[];
failedStep?: string;
error?: Error;
executionTime: number;
startTime: Date;
endTime: Date;
}
export interface WorkflowExecutionOptions {
maxRetries?: number;
retryDelay?: number;
continueOnError?: boolean;
enableRollback?: boolean;
timeout?: number;
}
export class WorkflowExecutor {
private testLogger: TestLogger;
private defaultOptions: WorkflowExecutionOptions = {
maxRetries: 3,
retryDelay: 1000,
continueOnError: false,
enableRollback: true,
timeout: 300000 // 5分钟默认超时
};
constructor(testLogger: TestLogger) {
this.testLogger = testLogger;
}
/**
* 执行工作流
*/
async execute(
workflow: BusinessWorkflow,
options?: WorkflowExecutionOptions
): Promise<WorkflowExecutionResult> {
const mergedOptions = { ...this.defaultOptions, ...options };
const startTime = new Date();
const completedSteps: string[] = [];
const executedSteps: WorkflowStep[] = [];
this.testLogger.info(`🚀 开始执行工作流: ${workflow.name}`);
this.testLogger.info(`📝 工作流描述: ${workflow.description}`);
try {
// 执行前置条件检查
if (workflow.preconditions) {
this.testLogger.info('🔍 检查前置条件');
const preconditionsMet = await workflow.preconditions();
if (!preconditionsMet) {
throw new Error('前置条件未满足');
}
this.testLogger.success('前置条件检查通过');
}
// 执行工作流步骤
for (const step of workflow.steps) {
const stepStartTime = Date.now();
try {
this.testLogger.startStep(step.name);
// 执行步骤(带重试)
await this.executeStepWithRetry(step, mergedOptions);
executedSteps.push(step);
completedSteps.push(step.name);
const stepDuration = Date.now() - stepStartTime;
this.testLogger.endStep(step.name, 'passed', stepDuration);
} catch (error) {
const stepDuration = Date.now() - stepStartTime;
this.testLogger.endStep(step.name, 'failed', stepDuration);
this.testLogger.error(`步骤执行失败: ${step.name}`, error as Error);
// 如果需要回滚
if (mergedOptions.enableRollback) {
await this.rollback(executedSteps);
}
const endTime = new Date();
return {
success: false,
workflowName: workflow.name,
completedSteps,
failedStep: step.name,
error: error as Error,
executionTime: endTime.getTime() - startTime.getTime(),
startTime,
endTime
};
}
}
// 执行后置条件检查
if (workflow.postconditions) {
this.testLogger.info('🔍 检查后置条件');
const postconditionsMet = await workflow.postconditions();
if (!postconditionsMet) {
throw new Error('后置条件未满足');
}
this.testLogger.success('后置条件检查通过');
}
const endTime = new Date();
const executionTime = endTime.getTime() - startTime.getTime();
this.testLogger.success(`✅ 工作流执行成功: ${workflow.name} (${executionTime}ms)`);
return {
success: true,
workflowName: workflow.name,
completedSteps,
executionTime,
startTime,
endTime
};
} catch (error) {
const endTime = new Date();
// 执行清理
if (workflow.cleanup) {
this.testLogger.info('🧹 执行清理操作');
try {
await workflow.cleanup();
} catch (cleanupError) {
this.testLogger.error('清理操作失败', cleanupError as Error);
}
}
return {
success: false,
workflowName: workflow.name,
completedSteps,
error: error as Error,
executionTime: endTime.getTime() - startTime.getTime(),
startTime,
endTime
};
}
}
/**
* 带重试的步骤执行
*/
private async executeStepWithRetry(
step: WorkflowStep,
options: WorkflowExecutionOptions
): Promise<void> {
const maxRetries = step.retryCount ?? options.maxRetries ?? 3;
const retryDelay = options.retryDelay ?? 1000;
const timeout = step.timeout ?? options.timeout ?? 30000;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.testLogger.info(`🔄 执行步骤: ${step.name} (尝试 ${attempt}/${maxRetries})`);
// 使用 Promise.race 实现超时控制
await Promise.race([
step.action(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`步骤超时: ${step.name}`)), timeout)
)
]);
// 执行成功
return;
} catch (error) {
lastError = error as Error;
this.testLogger.warn(`步骤执行失败 (尝试 ${attempt}/${maxRetries}): ${step.name}`);
if (attempt < maxRetries) {
this.testLogger.info(`⏳ 等待 ${retryDelay}ms 后重试...`);
await this.delay(retryDelay);
}
}
}
// 所有重试都失败
throw lastError;
}
/**
* 回滚操作
*/
private async rollback(executedSteps: WorkflowStep[]): Promise<void> {
this.testLogger.info('⏪ 开始回滚操作');
// 逆序执行回滚
for (let i = executedSteps.length - 1; i >= 0; i--) {
const step = executedSteps[i];
if (step.rollback) {
try {
this.testLogger.info(`⏪ 回滚步骤: ${step.name}`);
await step.rollback();
this.testLogger.success(`步骤回滚成功: ${step.name}`);
} catch (error) {
this.testLogger.error(`步骤回滚失败: ${step.name}`, error as Error);
}
}
}
this.testLogger.info('⏪ 回滚操作完成');
}
/**
* 延迟函数
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 批量执行多个工作流
*/
async executeBatch(
workflows: BusinessWorkflow[],
options?: WorkflowExecutionOptions
): Promise<WorkflowExecutionResult[]> {
this.testLogger.info(`📦 开始批量执行 ${workflows.length} 个工作流`);
const results: WorkflowExecutionResult[] = [];
for (const workflow of workflows) {
const result = await this.execute(workflow, options);
results.push(result);
// 如果失败且不继续执行,则中断
if (!result.success && !options?.continueOnError) {
this.testLogger.error(`工作流执行失败,中断批量执行: ${workflow.name}`);
break;
}
}
const successCount = results.filter(r => r.success).length;
this.testLogger.info(`📦 批量执行完成: ${successCount}/${workflows.length} 成功`);
return results;
}
/**
* 并行执行多个工作流
*/
async executeParallel(
workflows: BusinessWorkflow[],
options?: WorkflowExecutionOptions
): Promise<WorkflowExecutionResult[]> {
this.testLogger.info(`⚡ 开始并行执行 ${workflows.length} 个工作流`);
const promises = workflows.map(workflow => this.execute(workflow, options));
const results = await Promise.all(promises);
const successCount = results.filter(r => r.success).length;
this.testLogger.info(`⚡ 并行执行完成: ${successCount}/${workflows.length} 成功`);
return results;
}
}