feat(test-case+bug): 测试用例模块 + Bug 模块完整实现
需求验收体系: - 测试用例:五态状态机(待执行/执行中/通过/失败/阻塞) - Bug:六态状态机(待修复/修复中/已修复/验证中/已关闭/已拒绝) - Bug 通过测试用例间接关联需求(不冗余存版本) - Bug 默认修复人 = 关联需求的开发任务负责人 - 测试进度 = 已执行用例/总用例, 通过率 = 通过/已执行 UI: - 版本详情页新增"测试用例" Tab + "BUG" Tab - 测试用例:统计栏+筛选+按需求分组列表+详情抽屉+提BUG入口 - Bug:统计栏+筛选+列表+详情抽屉(链式跳转用例→需求) - 概览胶囊"测试"阶段进度联动 - "与我相关"新增测试用例/Bug分组 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
apps/web/lib/bug.ts
Normal file
75
apps/web/lib/bug.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { Priority } from './derive';
|
||||
|
||||
export type BugStatus = 'open' | 'fixing' | 'fixed' | 'verifying' | 'closed' | 'rejected';
|
||||
export type BugSeverity = 'critical' | 'major' | 'minor' | 'trivial';
|
||||
|
||||
export interface Bug {
|
||||
id: string;
|
||||
bugNo: string;
|
||||
testCaseId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
severity: BugSeverity;
|
||||
priority: Priority;
|
||||
reportedBy: string;
|
||||
assigneeId: string;
|
||||
status: BugStatus;
|
||||
resolvedAt?: string;
|
||||
closedAt?: string;
|
||||
resolution?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const BUG_STATUS_LABEL: Record<BugStatus, string> = {
|
||||
open: '待修复',
|
||||
fixing: '修复中',
|
||||
fixed: '已修复',
|
||||
verifying: '验证中',
|
||||
closed: '已关闭',
|
||||
rejected: '已拒绝',
|
||||
};
|
||||
|
||||
export const BUG_STATUS_COLOR: Record<BugStatus, string> = {
|
||||
open: 'bg-red-50 text-red-600',
|
||||
fixing: 'bg-blue-50 text-blue-600',
|
||||
fixed: 'bg-indigo-50 text-indigo-600',
|
||||
verifying: 'bg-purple-50 text-purple-600',
|
||||
closed: 'bg-emerald-50 text-emerald-600',
|
||||
rejected: 'bg-zinc-100 text-zinc-500',
|
||||
};
|
||||
|
||||
export const BUG_SEVERITY_LABEL: Record<BugSeverity, string> = {
|
||||
critical: '致命',
|
||||
major: '严重',
|
||||
minor: '一般',
|
||||
trivial: '轻微',
|
||||
};
|
||||
|
||||
export const BUG_SEVERITY_COLOR: Record<BugSeverity, string> = {
|
||||
critical: 'bg-red-100 text-red-700',
|
||||
major: 'bg-orange-50 text-orange-700',
|
||||
minor: 'bg-yellow-50 text-yellow-700',
|
||||
trivial: 'bg-zinc-100 text-zinc-600',
|
||||
};
|
||||
|
||||
export const BUG_ALLOWED_TRANSITIONS: Record<BugStatus, BugStatus[]> = {
|
||||
open: ['fixing', 'rejected'],
|
||||
fixing: ['fixed'],
|
||||
fixed: ['verifying'],
|
||||
verifying: ['closed', 'open'],
|
||||
closed: [],
|
||||
rejected: [],
|
||||
};
|
||||
|
||||
export function canBugTransition(from: BugStatus, to: BugStatus): boolean {
|
||||
return BUG_ALLOWED_TRANSITIONS[from].includes(to);
|
||||
}
|
||||
|
||||
export function generateBugNo(existingBugs: Bug[]): string {
|
||||
const maxNum = existingBugs.reduce((max, b) => {
|
||||
const num = parseInt(b.bugNo.replace('BUG-', ''), 10);
|
||||
return isNaN(num) ? max : Math.max(max, num);
|
||||
}, 0);
|
||||
return `BUG-${String(maxNum + 1).padStart(3, '0')}`;
|
||||
}
|
||||
69
apps/web/lib/test-case.ts
Normal file
69
apps/web/lib/test-case.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Priority } from './derive';
|
||||
|
||||
export type TestCaseStatus = 'pending' | 'running' | 'passed' | 'failed' | 'blocked';
|
||||
|
||||
export interface TestCase {
|
||||
id: string;
|
||||
caseNo: string;
|
||||
requirementId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: Priority;
|
||||
assigneeId?: string;
|
||||
status: TestCaseStatus;
|
||||
executedAt?: string;
|
||||
executedBy?: string;
|
||||
failReason?: string;
|
||||
blockReason?: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const TEST_CASE_STATUS_LABEL: Record<TestCaseStatus, string> = {
|
||||
pending: '待执行',
|
||||
running: '执行中',
|
||||
passed: '通过',
|
||||
failed: '失败',
|
||||
blocked: '阻塞',
|
||||
};
|
||||
|
||||
export const TEST_CASE_STATUS_COLOR: Record<TestCaseStatus, string> = {
|
||||
pending: 'bg-zinc-100 text-zinc-600',
|
||||
running: 'bg-blue-50 text-blue-600',
|
||||
passed: 'bg-emerald-50 text-emerald-600',
|
||||
failed: 'bg-red-50 text-red-600',
|
||||
blocked: 'bg-orange-50 text-orange-600',
|
||||
};
|
||||
|
||||
export const TC_ALLOWED_TRANSITIONS: Record<TestCaseStatus, TestCaseStatus[]> = {
|
||||
pending: ['running'],
|
||||
running: ['passed', 'failed', 'blocked'],
|
||||
passed: ['running'],
|
||||
failed: ['running'],
|
||||
blocked: ['running'],
|
||||
};
|
||||
|
||||
export function canTcTransition(from: TestCaseStatus, to: TestCaseStatus): boolean {
|
||||
return TC_ALLOWED_TRANSITIONS[from].includes(to);
|
||||
}
|
||||
|
||||
export function generateCaseNo(existingCases: TestCase[]): string {
|
||||
const maxNum = existingCases.reduce((max, c) => {
|
||||
const num = parseInt(c.caseNo.replace('TC-', ''), 10);
|
||||
return isNaN(num) ? max : Math.max(max, num);
|
||||
}, 0);
|
||||
return `TC-${String(maxNum + 1).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
export function calcTestProgress(cases: TestCase[]): { total: number; executed: number; passed: number; failed: number; blocked: number; passRate: number; completionRate: number } {
|
||||
const total = cases.length;
|
||||
if (total === 0) return { total: 0, executed: 0, passed: 0, failed: 0, blocked: 0, passRate: 0, completionRate: 0 };
|
||||
const passed = cases.filter((c) => c.status === 'passed').length;
|
||||
const failed = cases.filter((c) => c.status === 'failed').length;
|
||||
const blocked = cases.filter((c) => c.status === 'blocked').length;
|
||||
const executed = passed + failed + blocked;
|
||||
const passRate = (passed + failed) > 0 ? Math.round((passed / (passed + failed)) * 100) : 0;
|
||||
const completionRate = Math.round((executed / total) * 100);
|
||||
return { total, executed, passed, failed, blocked, passRate, completionRate };
|
||||
}
|
||||
Reference in New Issue
Block a user