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,475 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import { logger } from '../utils/logger';
import {
TestResult,
TestModule,
TestSuite,
TestCase,
TestStatus,
TestResultBuilder,
TestModuleBuilder,
ErrorType
} from '../models/test-result';
const execAsync = promisify(exec);
interface PlaywrightTest {
status: 'passed' | 'failed' | 'timedOut' | 'skipped';
duration: number;
error?: {
message: string;
stack?: string;
};
annotations: Array<{ type: string; description?: string }>;
results?: Array<{
status: 'passed' | 'failed' | 'timedOut' | 'skipped';
duration: number;
error?: {
message: string;
stack?: string;
};
stdout?: Array<{ text: string }>;
stderr?: Array<{ text: string }>;
}>;
}
interface PlaywrightSpec {
title: string;
tests: PlaywrightTest[];
status: 'expected' | 'unexpected' | 'skipped' | 'flaky';
file?: string;
line?: number;
tags?: string[];
ok?: boolean;
}
interface PlaywrightSuite {
title: string;
suites: PlaywrightSuite[];
specs: PlaywrightSpec[];
}
interface PlaywrightProject {
name: string;
suites: PlaywrightSuite[];
}
interface PlaywrightReport {
config?: {
projects?: PlaywrightProject[];
};
suites?: PlaywrightSuite[];
projects?: PlaywrightProject[];
errors?: Array<{
message: string;
stack?: string;
location?: {
file: string;
line: number;
column: number;
};
}>;
stats: {
startTime?: string;
duration: number;
expected: number;
unexpected: number;
skipped: number;
flaky?: number;
};
}
export class TestExecutor {
private testRoot: string;
private resultsDir: string;
constructor() {
this.testRoot = process.cwd();
this.resultsDir = path.join(this.testRoot, 'test-results');
this.ensureResultsDir();
}
private ensureResultsDir(): void {
if (!fs.existsSync(this.resultsDir)) {
fs.mkdirSync(this.resultsDir, { recursive: true });
}
}
async executeAll(): Promise<TestResult> {
logger.section('执行全量测试');
const builder = new TestResultBuilder();
builder.setStartTime(new Date());
const modules: Array<'api' | 'admin' | 'uniapp'> = ['api', 'admin', 'uniapp'];
for (const moduleName of modules) {
logger.info(`开始执行 ${moduleName} 模块测试...`);
const module = await this.executeModule(moduleName);
builder.addModule(module);
}
builder.setEndTime(new Date());
const result = builder.build();
logger.info('全量测试执行完成', {
totalTests: result.totalTests,
passed: result.passedTests,
failed: result.failedTests,
passRate: `${result.passRate.toFixed(2)}%`
});
return result;
}
async executeModule(moduleName: 'api' | 'admin' | 'uniapp'): Promise<TestModule> {
const builder = new TestModuleBuilder(moduleName);
builder.setStartTime(new Date());
const testDir = this.getTestDir(moduleName);
if (!fs.existsSync(testDir)) {
logger.warn(`${moduleName} 测试目录不存在: ${testDir}`);
builder.setEndTime(new Date());
return builder.build();
}
const testFiles = this.findTestFiles(testDir);
if (testFiles.length === 0) {
logger.warn(`${moduleName} 模块没有找到测试文件`);
builder.setEndTime(new Date());
return builder.build();
}
logger.info(`找到 ${testFiles.length} 个测试文件`);
const reportPath = path.join(this.resultsDir, `${moduleName}-report.json`);
await this.runPlaywrightTests(moduleName, testDir, reportPath);
const suites = await this.parseTestResults(reportPath, moduleName);
for (const suite of suites) {
builder.addSuite(suite);
}
builder.setEndTime(new Date());
return builder.build();
}
private getTestDir(moduleName: 'api' | 'admin' | 'uniapp'): string {
const baseDir = path.join(this.testRoot, 'e2e');
switch (moduleName) {
case 'api':
return path.join(baseDir, 'config');
case 'admin':
return path.join(baseDir, 'admin');
case 'uniapp':
return path.join(baseDir, 'uniapp');
default:
return baseDir;
}
}
private findTestFiles(dir: string): string[] {
const files: string[] = [];
const scanDir = (currentDir: string) => {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
scanDir(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.spec.ts')) {
files.push(fullPath);
}
}
};
scanDir(dir);
return files;
}
private async runPlaywrightTests(
moduleName: string,
testDir: string,
reportPath: string
): Promise<void> {
logger.info(`运行 Playwright 测试: ${moduleName}`);
const command = [
'npx playwright test',
testDir,
'--reporter=json',
'--workers=2',
'--retries=1'
].join(' ');
try {
const { stdout, stderr } = await execAsync(command, {
cwd: this.testRoot,
maxBuffer: 1024 * 1024 * 50
});
if (stdout) {
const jsonContent = this.extractJsonFromOutput(stdout);
if (jsonContent) {
fs.writeFileSync(reportPath, jsonContent, 'utf-8');
logger.debug(`Playwright 报告已保存: ${reportPath}`);
}
}
if (stderr) {
logger.debug(`Playwright 错误输出: ${stderr}`);
}
} catch (error: unknown) {
const execError = error as { stdout?: string; stderr?: string; message?: string };
if (execError.stdout) {
const jsonContent = this.extractJsonFromOutput(execError.stdout);
if (jsonContent) {
fs.writeFileSync(reportPath, jsonContent, 'utf-8');
logger.debug(`Playwright 报告已保存: ${reportPath}`);
}
}
if (execError.stderr) {
logger.debug(`Playwright 错误输出: ${execError.stderr}`);
}
logger.warn(`${moduleName} 测试执行有失败用例`);
}
}
private extractJsonFromOutput(output: string): string | null {
const ansiRegex = /\x1b\[[0-9;]*m/g;
const cleanOutput = output.replace(ansiRegex, '');
const lines = cleanOutput.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('{')) {
try {
JSON.parse(trimmed);
return trimmed;
} catch {
continue;
}
}
}
const jsonStart = cleanOutput.indexOf('{');
if (jsonStart !== -1) {
try {
const jsonStr = cleanOutput.substring(jsonStart);
JSON.parse(jsonStr);
return jsonStr;
} catch {
return null;
}
}
return null;
}
private async parseTestResults(
reportPath: string,
moduleName: 'api' | 'admin' | 'uniapp'
): Promise<TestSuite[]> {
const suites: TestSuite[] = [];
if (!fs.existsSync(reportPath)) {
logger.warn(`测试报告不存在: ${reportPath}`);
return suites;
}
try {
const content = fs.readFileSync(reportPath, 'utf-8');
const report = JSON.parse(content) as PlaywrightReport;
const allSuites = this.getAllSuites(report);
const testCases = this.extractTestCases(allSuites, moduleName);
if (report.errors && report.errors.length > 0) {
for (const error of report.errors) {
const errorTestCase: TestCase = {
id: `${moduleName}-error-${Date.now()}`,
name: '测试加载错误',
suite: '全局错误',
module: moduleName,
status: TestStatus.FAILED,
duration: 0,
startTime: new Date(),
steps: [],
retries: 0,
maxRetries: 0,
tags: ['error'],
priority: 'high',
error: {
type: ErrorType.ENVIRONMENT_ERROR,
message: error.message,
stack: error.stack,
timestamp: new Date()
}
};
testCases.push(errorTestCase);
}
}
const suite: TestSuite = {
id: `${moduleName}-suite`,
name: `${moduleName} 测试套件`,
module: moduleName,
tests: testCases,
status: testCases.some(t => t.status === TestStatus.FAILED)
? TestStatus.FAILED
: TestStatus.PASSED,
duration: testCases.reduce((sum, t) => sum + t.duration, 0),
startTime: new Date(),
endTime: new Date(),
passRate: testCases.length > 0
? (testCases.filter(t => t.status === TestStatus.PASSED).length / testCases.length) * 100
: 0
};
suites.push(suite);
} catch (error) {
logger.error(`解析测试报告失败: ${reportPath}`, error);
}
return suites;
}
private getAllSuites(report: PlaywrightReport): PlaywrightSuite[] {
const allSuites: PlaywrightSuite[] = [];
const collectSuites = (suites: PlaywrightSuite[]) => {
for (const suite of suites) {
allSuites.push(suite);
if (suite.suites && suite.suites.length > 0) {
collectSuites(suite.suites);
}
}
};
if (report.projects) {
for (const project of report.projects) {
collectSuites(project.suites);
}
}
if (report.suites) {
collectSuites(report.suites);
}
return allSuites;
}
private extractTestCases(
suites: PlaywrightSuite[],
moduleName: 'api' | 'admin' | 'uniapp'
): TestCase[] {
const cases: TestCase[] = [];
let idCounter = 1;
for (const suite of suites) {
for (const spec of suite.specs) {
for (const test of spec.tests) {
const testResult = test.results?.[0] || test;
const testCase: TestCase = {
id: `${moduleName}-test-${idCounter++}`,
name: spec.title,
suite: suite.title,
module: moduleName,
status: this.mapStatus(testResult.status),
duration: testResult.duration || 0,
startTime: new Date(),
steps: [],
retries: 0,
maxRetries: 1,
tags: spec.tags || test.annotations?.map(a => a.type) || [],
priority: 'medium'
};
if (testResult.error) {
testCase.error = {
type: this.detectErrorType(testResult.error.message),
message: testResult.error.message,
stack: testResult.error.stack,
timestamp: new Date()
};
}
cases.push(testCase);
}
}
}
return cases;
}
private mapStatus(status: string): TestStatus {
switch (status) {
case 'passed':
return TestStatus.PASSED;
case 'failed':
return TestStatus.FAILED;
case 'timedOut':
return TestStatus.FAILED;
case 'skipped':
return TestStatus.SKIPPED;
default:
return TestStatus.PENDING;
}
}
private detectErrorType(message: string): ErrorType {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('timeout') || lowerMessage.includes('timed out')) {
return ErrorType.TIMEOUT;
}
if (lowerMessage.includes('not found') || lowerMessage.includes('no element') || lowerMessage.includes('selector')) {
return ErrorType.ELEMENT_NOT_FOUND;
}
if (lowerMessage.includes('api') || lowerMessage.includes('request failed') || lowerMessage.includes('status code')) {
return ErrorType.API_ERROR;
}
if (lowerMessage.includes('assertion') || lowerMessage.includes('expect') || lowerMessage.includes('assert')) {
return ErrorType.ASSERTION_ERROR;
}
if (lowerMessage.includes('network') || lowerMessage.includes('connection') || lowerMessage.includes('econnrefused')) {
return ErrorType.NETWORK_ERROR;
}
if (lowerMessage.includes('auth') || lowerMessage.includes('unauthorized') || lowerMessage.includes('forbidden')) {
return ErrorType.AUTH_ERROR;
}
if (lowerMessage.includes('data') || lowerMessage.includes('json') || lowerMessage.includes('parse')) {
return ErrorType.DATA_ERROR;
}
if (lowerMessage.includes('environment') || lowerMessage.includes('config') || lowerMessage.includes('setup')) {
return ErrorType.ENVIRONMENT_ERROR;
}
return ErrorType.UNKNOWN;
}
async runSpecificTest(testPath: string): Promise<TestCase | null> {
logger.info(`运行单个测试: ${testPath}`);
try {
const { stdout } = await execAsync(
`npx playwright test ${testPath} --reporter=json`,
{ cwd: this.testRoot, maxBuffer: 1024 * 1024 * 10 }
);
const report = JSON.parse(stdout) as PlaywrightReport;
const allSuites = this.getAllSuites(report);
const cases = this.extractTestCases(allSuites, 'api');
return cases[0] || null;
} catch (error) {
logger.error(`运行测试失败: ${testPath}`, error);
return null;
}
}
}