/** * 测试运行器 * 提供命令行接口执行端到端测试 */ import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; export interface TestRunnerOptions { testPattern?: string; browser?: 'chromium' | 'firefox' | 'webkit' | 'all'; headed?: boolean; debug?: boolean; mock?: boolean; mockMode?: 'full' | 'partial' | 'none'; report?: boolean; parallel?: boolean; workers?: number; retries?: number; timeout?: number; grep?: string; } export class TestRunner { private options: TestRunnerOptions; private baseDir: string; constructor(options: TestRunnerOptions = {}) { this.options = { browser: 'chromium', headed: false, debug: false, mock: false, mockMode: 'none', report: true, parallel: true, workers: undefined, retries: 2, timeout: 300000, ...options }; this.baseDir = process.cwd(); } /** * 运行测试 */ async run(): Promise { console.log('🚀 启动端到端测试...'); console.log(`📋 测试配置: ${JSON.stringify(this.options, null, 2)}`); try { // 确保测试目录存在 this.ensureTestDirectories(); // 构建命令 const command = this.buildCommand(); // 设置环境变量 this.setupEnvironment(); console.log(`⚡ 执行命令: ${command}`); // 执行测试 execSync(command, { cwd: this.baseDir, stdio: 'inherit', env: process.env }); console.log('✅ 测试执行完成'); // 生成报告 if (this.options.report) { this.openReport(); } } catch (error) { console.error('❌ 测试执行失败:', error); process.exit(1); } } /** * 构建Playwright命令 */ private buildCommand(): string { const parts: string[] = ['npx', 'playwright', 'test']; // 测试模式 if (this.options.testPattern) { parts.push(this.options.testPattern); } else { parts.push('e2e/business-flows/'); } // 浏览器选择 if (this.options.browser && this.options.browser !== 'all') { parts.push(`--project=${this.options.browser}`); } // 有头模式 if (this.options.headed) { parts.push('--headed'); } // 调试模式 if (this.options.debug) { parts.push('--debug'); } // 并行执行 if (this.options.parallel) { if (this.options.workers) { parts.push(`--workers=${this.options.workers}`); } } else { parts.push('--workers=1'); } // 重试次数 if (this.options.retries !== undefined) { parts.push(`--retries=${this.options.retries}`); } // 超时设置 if (this.options.timeout) { parts.push(`--timeout=${this.options.timeout}`); } // 过滤测试 if (this.options.grep) { parts.push(`--grep="${this.options.grep}"`); } // 报告器 if (this.options.report) { parts.push('--reporter=html,json,line'); } return parts.join(' '); } /** * 设置环境变量 */ private setupEnvironment(): void { // Mock配置 if (this.options.mock) { process.env.E2E_MOCK_ENABLED = 'true'; process.env.E2E_MOCK_MODE = this.options.mockMode || 'full'; } else { process.env.E2E_MOCK_ENABLED = 'false'; process.env.E2E_MOCK_MODE = 'none'; } // 浏览器配置 process.env.E2E_BROWSER = this.options.browser || 'chromium'; // 基础URL if (!process.env.E2E_BASE_URL) { process.env.E2E_BASE_URL = 'http://localhost:5174'; } console.log('🔧 环境变量已设置'); } /** * 确保测试目录存在 */ private ensureTestDirectories(): void { const dirs = [ 'test-results', 'test-results/screenshots', 'test-results/videos', 'test-results/traces', 'test-results/reports' ]; for (const dir of dirs) { const fullPath = path.join(this.baseDir, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } } } /** * 打开测试报告 */ private openReport(): void { try { execSync('npx playwright show-report', { cwd: this.baseDir, stdio: 'ignore' }); } catch (error) { console.warn('⚠️ 无法自动打开报告'); } } /** * 运行特定测试套件 */ async runSuite(suiteName: string): Promise { this.options.testPattern = `e2e/business-flows/${suiteName}.spec.ts`; await this.run(); } /** * 运行冒烟测试 */ async runSmokeTests(): Promise { this.options.grep = '@smoke'; this.options.parallel = false; this.options.workers = 1; await this.run(); } /** * 运行回归测试 */ async runRegressionTests(): Promise { this.options.grep = '@regression'; this.options.parallel = true; await this.run(); } /** * 运行特定工作流测试 */ async runWorkflow(workflowName: string): Promise { this.options.grep = workflowName; await this.run(); } } /** * 命令行参数解析 */ function parseArgs(): TestRunnerOptions { const args = process.argv.slice(2); const options: TestRunnerOptions = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--pattern': case '-p': options.testPattern = args[++i]; break; case '--browser': case '-b': options.browser = args[++i] as any; break; case '--headed': case '-h': options.headed = true; break; case '--debug': case '-d': options.debug = true; break; case '--mock': case '-m': options.mock = true; options.mockMode = (args[++i] as any) || 'full'; break; case '--no-report': options.report = false; break; case '--no-parallel': options.parallel = false; break; case '--workers': case '-w': options.workers = parseInt(args[++i], 10); break; case '--retries': case '-r': options.retries = parseInt(args[++i], 10); break; case '--timeout': case '-t': options.timeout = parseInt(args[++i], 10); break; case '--grep': case '-g': options.grep = args[++i]; break; case '--help': case '-h': showHelp(); process.exit(0); break; } } return options; } /** * 显示帮助信息 */ function showHelp(): void { console.log(` 端到端测试运行器 用法: npx ts-node e2e/test-runner.ts [选项] 选项: -p, --pattern 测试文件匹配模式 -b, --browser 浏览器类型 (chromium|firefox|webkit|all) -h, --headed 有头模式运行 -d, --debug 调试模式 -m, --mock 启用Mock (full|partial|none) --no-report 不生成报告 --no-parallel 禁用并行执行 -w, --workers 并行工作线程数 -r, --retries 失败重试次数 -t, --timeout 超时时间(毫秒) -g, --grep 过滤测试用例 --help 显示帮助 示例: npx ts-node e2e/test-runner.ts npx ts-node e2e/test-runner.ts --pattern "auth-e2e.spec.ts" npx ts-node e2e/test-runner.ts --browser firefox --headed npx ts-node e2e/test-runner.ts --mock full --grep "登录" `); } /** * 主函数 */ async function main(): Promise { const options = parseArgs(); const runner = new TestRunner(options); await runner.run(); } // 如果直接运行此文件 if (require.main === module) { main().catch(error => { console.error('❌ 运行失败:', error); process.exit(1); }); } export default TestRunner;