feat(版本详情): 优化任务统计与测试轮次
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { Priority } from './derive';
|
||||
import type { Reference } from './dev-task';
|
||||
import type { DevTaskStatus, Reference } from './dev-task';
|
||||
import type { Bug } from './bug';
|
||||
import { DEFAULT_TEST_CATEGORY_ID } from './task-category';
|
||||
import { calcActualElapsedHours, type TimeInterval } from './work-hours';
|
||||
import { aggregateWorkEffort } from './work-effort-engine';
|
||||
@@ -11,6 +12,8 @@ export interface TestCase {
|
||||
caseNo: string;
|
||||
versionId: string;
|
||||
requirementId?: string;
|
||||
roundNo?: number;
|
||||
sourceCaseId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
categoryId: string;
|
||||
@@ -83,6 +86,8 @@ export function normalizeTestCase(testCase: Partial<TestCase>, index = 0): TestC
|
||||
caseNo: testCase.caseNo || `TC-${String(index + 1).padStart(3, '0')}`,
|
||||
versionId: testCase.versionId || '',
|
||||
requirementId: testCase.requirementId,
|
||||
roundNo: getTestCaseRoundNo(testCase),
|
||||
sourceCaseId: testCase.sourceCaseId,
|
||||
title: testCase.title || `测试用例 ${index + 1}`,
|
||||
description: testCase.description,
|
||||
categoryId: testCase.categoryId || DEFAULT_TEST_CATEGORY_ID,
|
||||
@@ -110,14 +115,94 @@ export function normalizeTestCases(cases: Partial<TestCase>[] = []): TestCase[]
|
||||
return cases.map((tc, index) => normalizeTestCase(tc, index));
|
||||
}
|
||||
|
||||
export function calcTestProgress(cases: TestCase[]): { total: number; executed: number; passed: number; failed: number; blocked: number; passRate: number; completionRate: number } {
|
||||
export type CreateTestCaseInput = Omit<TestCase, 'id' | 'caseNo' | 'createdAt' | 'updatedAt' | 'status'>;
|
||||
|
||||
export function getTestCaseRoundNo(testCase: Partial<Pick<TestCase, 'roundNo'>>): number {
|
||||
const roundNo = testCase.roundNo;
|
||||
if (typeof roundNo !== 'number' || !Number.isFinite(roundNo) || roundNo < 1) return 1;
|
||||
return Math.floor(roundNo);
|
||||
}
|
||||
|
||||
export function getNextTestRoundNo(cases: Pick<TestCase, 'roundNo'>[]): number {
|
||||
const maxRound = cases.reduce((max, testCase) => Math.max(max, getTestCaseRoundNo(testCase)), 0);
|
||||
return maxRound + 1;
|
||||
}
|
||||
|
||||
export function isTestCaseTested(testCase: Pick<TestCase, 'status'>): boolean {
|
||||
return testCase.status === 'passed' || testCase.status === 'failed' || testCase.status === 'blocked';
|
||||
}
|
||||
|
||||
export function canStartNextTestRound(cases: Pick<TestCase, 'roundNo' | 'status'>[]): boolean {
|
||||
const firstRoundCases = cases.filter((testCase) => getTestCaseRoundNo(testCase) === 1);
|
||||
if (firstRoundCases.length === 0) return false;
|
||||
const latestRoundNo = Math.max(...cases.map((testCase) => getTestCaseRoundNo(testCase)));
|
||||
const latestRoundCases = cases.filter((testCase) => getTestCaseRoundNo(testCase) === latestRoundNo);
|
||||
return latestRoundCases.length > 0 && latestRoundCases.every(isTestCaseTested);
|
||||
}
|
||||
|
||||
export type RequirementDeliveryStatus = 'submitted' | 'pending';
|
||||
|
||||
type RequirementDevTaskRef = {
|
||||
requirementId: string;
|
||||
status: DevTaskStatus;
|
||||
};
|
||||
|
||||
export function getRequirementDeliveryStatus(
|
||||
requirementId: string | undefined,
|
||||
devTasks: RequirementDevTaskRef[],
|
||||
): RequirementDeliveryStatus {
|
||||
if (!requirementId) return 'pending';
|
||||
const requirementTasks = devTasks.filter((task) => task.requirementId === requirementId);
|
||||
if (requirementTasks.length === 0) return 'pending';
|
||||
return requirementTasks.every((task) => task.status === 'submitted') ? 'submitted' : 'pending';
|
||||
}
|
||||
|
||||
export function copyTestCaseToRound(source: TestCase, roundNo: number, createdBy: string): CreateTestCaseInput {
|
||||
return {
|
||||
versionId: source.versionId,
|
||||
requirementId: source.requirementId,
|
||||
roundNo,
|
||||
sourceCaseId: source.sourceCaseId ?? source.id,
|
||||
title: source.title,
|
||||
description: source.description,
|
||||
categoryId: source.categoryId,
|
||||
priority: source.priority,
|
||||
estimateHours: source.estimateHours,
|
||||
aiEstimateHours: source.aiEstimateHours,
|
||||
assigneeId: source.assigneeId,
|
||||
startedAt: undefined,
|
||||
completedAt: undefined,
|
||||
executedAt: undefined,
|
||||
executedBy: undefined,
|
||||
failReason: undefined,
|
||||
blockReason: undefined,
|
||||
references: source.references,
|
||||
aiDraft: source.aiDraft,
|
||||
aiDraftAt: source.aiDraftAt,
|
||||
createdBy,
|
||||
};
|
||||
}
|
||||
|
||||
type TestCaseBugRef = Pick<Bug, 'testCaseId' | 'status'>;
|
||||
|
||||
export function calcTestProgress(
|
||||
cases: TestCase[],
|
||||
bugs: TestCaseBugRef[] = [],
|
||||
): { 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 effectiveBugCaseIds = new Set(
|
||||
bugs.filter((bug) => bug.status !== 'rejected').map((bug) => bug.testCaseId),
|
||||
);
|
||||
const executedCases = cases.filter(
|
||||
(c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked' || effectiveBugCaseIds.has(c.id),
|
||||
);
|
||||
const cleanPassed = executedCases.filter((c) => c.status === 'passed' && !effectiveBugCaseIds.has(c.id)).length;
|
||||
const executed = executedCases.length;
|
||||
const passRate = executed > 0 ? Math.round((cleanPassed / executed) * 100) : 0;
|
||||
const completionRate = aggregateWorkEffort(cases.map((c) => ({
|
||||
estimateHours: getTestCaseEstimateHours(c),
|
||||
actualHours: getTestCaseActualHours(c),
|
||||
|
||||
Reference in New Issue
Block a user