feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
import { TestCase, TestError, ErrorType } from '../models/test-result';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface ErrorAnalysis {
|
||||
type: ErrorType;
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
rootCause: string;
|
||||
suggestedFix: string;
|
||||
autoFixable: boolean;
|
||||
fixStrategy?: FixStrategy;
|
||||
}
|
||||
|
||||
export enum ErrorCategory {
|
||||
INFRASTRUCTURE = 'infrastructure',
|
||||
APPLICATION = 'application',
|
||||
TEST_CODE = 'test_code',
|
||||
DATA = 'data',
|
||||
CONFIGURATION = 'configuration',
|
||||
TIMING = 'timing'
|
||||
}
|
||||
|
||||
export enum ErrorSeverity {
|
||||
CRITICAL = 'critical',
|
||||
HIGH = 'high',
|
||||
MEDIUM = 'medium',
|
||||
LOW = 'low'
|
||||
}
|
||||
|
||||
export interface FixStrategy {
|
||||
type: 'code' | 'config' | 'data' | 'wait' | 'retry';
|
||||
description: string;
|
||||
actions: FixAction[];
|
||||
}
|
||||
|
||||
export interface FixAction {
|
||||
type: string;
|
||||
target: string;
|
||||
value?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class ErrorAnalyzer {
|
||||
private patterns: Map<ErrorType, RegExp[]>;
|
||||
|
||||
constructor() {
|
||||
this.patterns = this.initializePatterns();
|
||||
}
|
||||
|
||||
private initializePatterns(): Map<ErrorType, RegExp[]> {
|
||||
return new Map([
|
||||
[ErrorType.TIMEOUT, [
|
||||
/timeout/i,
|
||||
/timed out/i,
|
||||
/exceeded.*timeout/i,
|
||||
/waiting.*failed/i
|
||||
]],
|
||||
[ErrorType.ELEMENT_NOT_FOUND, [
|
||||
/element.*not found/i,
|
||||
/no element.*matching/i,
|
||||
/selector.*not found/i,
|
||||
/unable to find/i,
|
||||
/waiting for selector/i
|
||||
]],
|
||||
[ErrorType.API_ERROR, [
|
||||
/api.*error/i,
|
||||
/request failed/i,
|
||||
/status code.*\d{3}/i,
|
||||
/http.*error/i,
|
||||
/response.*error/i
|
||||
]],
|
||||
[ErrorType.ASSERTION_ERROR, [
|
||||
/assertion.*failed/i,
|
||||
/expect.*received/i,
|
||||
/expected.*but received/i,
|
||||
/assertionerror/i
|
||||
]],
|
||||
[ErrorType.NETWORK_ERROR, [
|
||||
/network error/i,
|
||||
/connection refused/i,
|
||||
/econnrefused/i,
|
||||
/enotfound/i,
|
||||
/socket hang up/i
|
||||
]],
|
||||
[ErrorType.AUTH_ERROR, [
|
||||
/unauthorized/i,
|
||||
/forbidden/i,
|
||||
/authentication failed/i,
|
||||
/invalid token/i,
|
||||
/session expired/i
|
||||
]],
|
||||
[ErrorType.DATA_ERROR, [
|
||||
/json parse error/i,
|
||||
/invalid json/i,
|
||||
/data.*invalid/i,
|
||||
/schema.*validation/i
|
||||
]],
|
||||
[ErrorType.ENVIRONMENT_ERROR, [
|
||||
/environment.*error/i,
|
||||
/config.*error/i,
|
||||
/setup.*failed/i,
|
||||
/initialization.*failed/i
|
||||
]]
|
||||
]);
|
||||
}
|
||||
|
||||
analyze(testCase: TestCase): ErrorAnalysis {
|
||||
if (!testCase.error) {
|
||||
return this.createDefaultAnalysis();
|
||||
}
|
||||
|
||||
const error = testCase.error;
|
||||
const type = this.detectErrorType(error);
|
||||
const category = this.categorizeError(type, error);
|
||||
const severity = this.assessSeverity(type, testCase);
|
||||
const rootCause = this.identifyRootCause(error, type);
|
||||
const suggestedFix = this.suggestFix(type, error, testCase);
|
||||
const autoFixable = this.isAutoFixable(type, error);
|
||||
const fixStrategy = autoFixable ? this.createFixStrategy(type, error, testCase) : undefined;
|
||||
|
||||
const analysis: ErrorAnalysis = {
|
||||
type,
|
||||
category,
|
||||
severity,
|
||||
rootCause,
|
||||
suggestedFix,
|
||||
autoFixable,
|
||||
fixStrategy
|
||||
};
|
||||
|
||||
logger.debug('错误分析完成', {
|
||||
testId: testCase.id,
|
||||
type,
|
||||
category,
|
||||
severity,
|
||||
autoFixable
|
||||
});
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
private detectErrorType(error: TestError): ErrorType {
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
const patternEntries = Array.from(this.patterns.entries());
|
||||
for (const [type, regexps] of patternEntries) {
|
||||
for (const regex of regexps) {
|
||||
if (regex.test(message)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorType.UNKNOWN;
|
||||
}
|
||||
|
||||
private categorizeError(type: ErrorType, error: TestError): ErrorCategory {
|
||||
switch (type) {
|
||||
case ErrorType.TIMEOUT:
|
||||
case ErrorType.NETWORK_ERROR:
|
||||
return ErrorCategory.INFRASTRUCTURE;
|
||||
|
||||
case ErrorType.API_ERROR:
|
||||
case ErrorType.AUTH_ERROR:
|
||||
return ErrorCategory.APPLICATION;
|
||||
|
||||
case ErrorType.ASSERTION_ERROR:
|
||||
return ErrorCategory.TEST_CODE;
|
||||
|
||||
case ErrorType.DATA_ERROR:
|
||||
return ErrorCategory.DATA;
|
||||
|
||||
case ErrorType.ENVIRONMENT_ERROR:
|
||||
return ErrorCategory.CONFIGURATION;
|
||||
|
||||
case ErrorType.ELEMENT_NOT_FOUND:
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes('loading') || message.includes('spinner')) {
|
||||
return ErrorCategory.TIMING;
|
||||
}
|
||||
return ErrorCategory.TEST_CODE;
|
||||
|
||||
default:
|
||||
return ErrorCategory.TEST_CODE;
|
||||
}
|
||||
}
|
||||
|
||||
private assessSeverity(type: ErrorType, testCase: TestCase): ErrorSeverity {
|
||||
if (testCase.priority === 'high') {
|
||||
if (type === ErrorType.API_ERROR || type === ErrorType.AUTH_ERROR) {
|
||||
return ErrorSeverity.CRITICAL;
|
||||
}
|
||||
return ErrorSeverity.HIGH;
|
||||
}
|
||||
|
||||
if (testCase.priority === 'medium') {
|
||||
if (type === ErrorType.TIMEOUT || type === ErrorType.NETWORK_ERROR) {
|
||||
return ErrorSeverity.HIGH;
|
||||
}
|
||||
return ErrorSeverity.MEDIUM;
|
||||
}
|
||||
|
||||
return ErrorSeverity.LOW;
|
||||
}
|
||||
|
||||
private identifyRootCause(error: TestError, type: ErrorType): string {
|
||||
const message = error.message;
|
||||
|
||||
switch (type) {
|
||||
case ErrorType.TIMEOUT:
|
||||
if (message.includes('navigation')) {
|
||||
return '页面导航超时,可能是网络延迟或页面加载过慢';
|
||||
}
|
||||
if (message.includes('element')) {
|
||||
return '元素等待超时,元素可能未及时渲染';
|
||||
}
|
||||
return '操作超时,系统响应过慢';
|
||||
|
||||
case ErrorType.ELEMENT_NOT_FOUND:
|
||||
return '目标元素不存在,可能是选择器错误或页面结构变化';
|
||||
|
||||
case ErrorType.API_ERROR:
|
||||
const statusMatch = message.match(/status code[:\s]*(\d+)/i);
|
||||
if (statusMatch) {
|
||||
return `API 返回错误状态码: ${statusMatch[1]}`;
|
||||
}
|
||||
return 'API 请求失败';
|
||||
|
||||
case ErrorType.ASSERTION_ERROR:
|
||||
return '断言失败,实际结果与预期不符';
|
||||
|
||||
case ErrorType.NETWORK_ERROR:
|
||||
return '网络连接失败,服务可能未启动或不可达';
|
||||
|
||||
case ErrorType.AUTH_ERROR:
|
||||
return '认证失败,令牌可能已过期或无效';
|
||||
|
||||
case ErrorType.DATA_ERROR:
|
||||
return '数据格式错误,JSON 解析失败';
|
||||
|
||||
case ErrorType.ENVIRONMENT_ERROR:
|
||||
return '环境配置错误';
|
||||
|
||||
default:
|
||||
return '未知错误';
|
||||
}
|
||||
}
|
||||
|
||||
private suggestFix(type: ErrorType, error: TestError, testCase: TestCase): string {
|
||||
switch (type) {
|
||||
case ErrorType.TIMEOUT:
|
||||
return '增加超时时间或优化等待策略,确保元素加载完成后再操作';
|
||||
|
||||
case ErrorType.ELEMENT_NOT_FOUND:
|
||||
return '检查选择器是否正确,确认页面结构是否变化,添加更健壮的等待逻辑';
|
||||
|
||||
case ErrorType.API_ERROR:
|
||||
return '检查 API 服务状态,验证请求参数和认证信息';
|
||||
|
||||
case ErrorType.ASSERTION_ERROR:
|
||||
return '检查断言条件,确认预期值是否正确,可能需要更新测试预期';
|
||||
|
||||
case ErrorType.NETWORK_ERROR:
|
||||
return '确认服务已启动,检查网络连接和防火墙设置';
|
||||
|
||||
case ErrorType.AUTH_ERROR:
|
||||
return '刷新认证令牌,检查用户凭证和权限配置';
|
||||
|
||||
case ErrorType.DATA_ERROR:
|
||||
return '验证数据格式,检查 JSON 结构是否符合预期';
|
||||
|
||||
case ErrorType.ENVIRONMENT_ERROR:
|
||||
return '检查环境变量和配置文件,确保所有依赖已正确安装';
|
||||
|
||||
default:
|
||||
return '需要人工分析错误日志,确定具体问题';
|
||||
}
|
||||
}
|
||||
|
||||
private isAutoFixable(type: ErrorType, error: TestError): boolean {
|
||||
const autoFixableTypes = [
|
||||
ErrorType.TIMEOUT,
|
||||
ErrorType.AUTH_ERROR
|
||||
];
|
||||
|
||||
return autoFixableTypes.includes(type);
|
||||
}
|
||||
|
||||
private createFixStrategy(type: ErrorType, error: TestError, testCase: TestCase): FixStrategy | undefined {
|
||||
switch (type) {
|
||||
case ErrorType.TIMEOUT:
|
||||
return {
|
||||
type: 'wait',
|
||||
description: '增加等待时间和重试策略',
|
||||
actions: [
|
||||
{
|
||||
type: 'increase_timeout',
|
||||
target: testCase.id,
|
||||
value: '60000',
|
||||
description: '将超时时间增加到 60 秒'
|
||||
},
|
||||
{
|
||||
type: 'add_retry',
|
||||
target: testCase.id,
|
||||
value: '3',
|
||||
description: '添加 3 次重试'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
case ErrorType.AUTH_ERROR:
|
||||
return {
|
||||
type: 'config',
|
||||
description: '刷新认证令牌',
|
||||
actions: [
|
||||
{
|
||||
type: 'refresh_token',
|
||||
target: 'auth',
|
||||
description: '重新获取认证令牌'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private createDefaultAnalysis(): ErrorAnalysis {
|
||||
return {
|
||||
type: ErrorType.UNKNOWN,
|
||||
category: ErrorCategory.TEST_CODE,
|
||||
severity: ErrorSeverity.LOW,
|
||||
rootCause: '未知错误',
|
||||
suggestedFix: '需要人工分析',
|
||||
autoFixable: false
|
||||
};
|
||||
}
|
||||
|
||||
analyzeBatch(testCases: TestCase[]): Map<string, ErrorAnalysis> {
|
||||
const results = new Map<string, ErrorAnalysis>();
|
||||
|
||||
for (const testCase of testCases) {
|
||||
if (testCase.status === 'failed' && testCase.error) {
|
||||
results.set(testCase.id, this.analyze(testCase));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getErrorStatistics(analyses: Map<string, ErrorAnalysis>): {
|
||||
byType: Record<ErrorType, number>;
|
||||
byCategory: Record<ErrorCategory, number>;
|
||||
bySeverity: Record<ErrorSeverity, number>;
|
||||
autoFixableCount: number;
|
||||
} {
|
||||
const byType: Record<ErrorType, number> = {} as Record<ErrorType, number>;
|
||||
const byCategory: Record<ErrorCategory, number> = {} as Record<ErrorCategory, number>;
|
||||
const bySeverity: Record<ErrorSeverity, number> = {} as Record<ErrorSeverity, number>;
|
||||
let autoFixableCount = 0;
|
||||
|
||||
const analysisValues = Array.from(analyses.values());
|
||||
for (const analysis of analysisValues) {
|
||||
byType[analysis.type] = (byType[analysis.type] || 0) + 1;
|
||||
byCategory[analysis.category] = (byCategory[analysis.category] || 0) + 1;
|
||||
bySeverity[analysis.severity] = (bySeverity[analysis.severity] || 0) + 1;
|
||||
if (analysis.autoFixable) {
|
||||
autoFixableCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return { byType, byCategory, bySeverity, autoFixableCount };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user