fix(version): 修正概览耗时与排名统计
This commit is contained in:
262
apps/web/lib/version-overview.test.ts
Normal file
262
apps/web/lib/version-overview.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
calcBugSeverityRanking,
|
||||
calcPersonalEffortRanking,
|
||||
calcStageEffortMetrics,
|
||||
calcVersionOverviewEffortTotals,
|
||||
} from './version-overview';
|
||||
import type { VersionPlan } from './version-plan';
|
||||
import type { DevTask } from './dev-task';
|
||||
import type { TestCase } from './test-case';
|
||||
import type { Bug } from './bug';
|
||||
import type { OvertimeRecord } from './overtime';
|
||||
|
||||
function plan(patch: Partial<VersionPlan>): VersionPlan {
|
||||
return {
|
||||
id: patch.id || 'plan-1',
|
||||
versionId: patch.versionId || 'v-1',
|
||||
type: patch.type || 'research',
|
||||
title: patch.title || '计划',
|
||||
owner: patch.owner || 'Alice',
|
||||
startTime: patch.startTime || '2026-06-22T09:00:00',
|
||||
endTime: patch.endTime || '2026-06-22T18:00:00',
|
||||
status: patch.status || 'completed',
|
||||
actualStartAt: patch.actualStartAt,
|
||||
completedAt: patch.completedAt,
|
||||
createdAt: patch.createdAt || '2026-06-22T08:00:00',
|
||||
addedBy: patch.addedBy || 'Alice',
|
||||
};
|
||||
}
|
||||
|
||||
function devTask(patch: Partial<DevTask>): DevTask {
|
||||
return {
|
||||
id: patch.id || 'dev-1',
|
||||
taskNo: patch.taskNo || 'DEV-001',
|
||||
requirementId: patch.requirementId || 'req-1',
|
||||
title: patch.title || '开发任务',
|
||||
categoryId: patch.categoryId || 'cat-1',
|
||||
assigneeId: patch.assigneeId || 'Alice',
|
||||
priority: patch.priority || 'P2',
|
||||
expectedStartAt: patch.expectedStartAt || '2026-06-22T09:00:00',
|
||||
expectedEndAt: patch.expectedEndAt || '2026-06-22T18:00:00',
|
||||
estimateHours: patch.estimateHours,
|
||||
aiEstimateHours: patch.aiEstimateHours,
|
||||
actualStartAt: patch.actualStartAt,
|
||||
actualEndAt: patch.actualEndAt,
|
||||
status: patch.status || 'submitted',
|
||||
isBlocked: patch.isBlocked ?? false,
|
||||
createdBy: patch.createdBy || 'Alice',
|
||||
createdAt: patch.createdAt || '2026-06-22T08:00:00',
|
||||
updatedAt: patch.updatedAt || '2026-06-22T18:00:00',
|
||||
};
|
||||
}
|
||||
|
||||
function testCase(patch: Partial<TestCase>): TestCase {
|
||||
return {
|
||||
id: patch.id || 'tc-1',
|
||||
caseNo: patch.caseNo || 'TC-001',
|
||||
versionId: patch.versionId || 'v-1',
|
||||
title: patch.title || '测试用例',
|
||||
categoryId: patch.categoryId || 'cat-1',
|
||||
priority: patch.priority || 'P2',
|
||||
assigneeId: patch.assigneeId || 'Bob',
|
||||
status: patch.status || 'passed',
|
||||
estimateHours: patch.estimateHours,
|
||||
aiEstimateHours: patch.aiEstimateHours,
|
||||
startedAt: patch.startedAt,
|
||||
completedAt: patch.completedAt,
|
||||
createdBy: patch.createdBy || 'Bob',
|
||||
createdAt: patch.createdAt || '2026-06-22T08:00:00',
|
||||
updatedAt: patch.updatedAt || '2026-06-22T18:00:00',
|
||||
};
|
||||
}
|
||||
|
||||
function bug(patch: Partial<Bug>): Bug {
|
||||
return {
|
||||
id: patch.id || 'bug-1',
|
||||
bugNo: patch.bugNo || 'BUG-001',
|
||||
versionId: patch.versionId || 'v-1',
|
||||
testCaseId: patch.testCaseId || 'tc-1',
|
||||
title: patch.title || 'Bug',
|
||||
description: patch.description || '描述',
|
||||
severity: patch.severity || 'major',
|
||||
priority: patch.priority || 'P1',
|
||||
reportedBy: patch.reportedBy || 'QA',
|
||||
assigneeId: patch.assigneeId || 'Bob',
|
||||
status: patch.status || 'closed',
|
||||
createdAt: patch.createdAt || '2026-06-22T10:00:00',
|
||||
updatedAt: patch.updatedAt || '2026-06-22T12:00:00',
|
||||
closedAt: patch.closedAt,
|
||||
resolvedAt: patch.resolvedAt,
|
||||
};
|
||||
}
|
||||
|
||||
test('calcStageEffortMetrics returns actual hours and AI estimates per stage', () => {
|
||||
const metrics = calcStageEffortMetrics({
|
||||
plans: [
|
||||
plan({ type: 'research', actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' }),
|
||||
plan({ type: 'product', actualStartAt: '2026-06-23T09:00:00', completedAt: '2026-06-23T12:00:00' }),
|
||||
],
|
||||
devTasks: [
|
||||
devTask({
|
||||
actualStartAt: '2026-06-24T09:00:00',
|
||||
actualEndAt: '2026-06-24T14:00:00',
|
||||
estimateHours: 4.5,
|
||||
aiEstimateHours: 3.25,
|
||||
}),
|
||||
],
|
||||
testCases: [
|
||||
testCase({
|
||||
startedAt: '2026-06-25T09:00:00',
|
||||
completedAt: '2026-06-25T11:00:00',
|
||||
estimateHours: 2.25,
|
||||
aiEstimateHours: 1.5,
|
||||
}),
|
||||
],
|
||||
bugs: [
|
||||
bug({ createdAt: '2026-06-26T10:00:00', closedAt: '2026-06-26T10:10:00' }),
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(metrics.requirement.actualHours, 9);
|
||||
assert.equal(metrics.product_design.actualHours, 3);
|
||||
assert.equal(metrics.dev.actualHours, 5);
|
||||
assert.equal(metrics.dev.estimateHours, 4.5);
|
||||
assert.equal(metrics.dev.aiEstimateHours, 3.25);
|
||||
assert.equal(metrics.testing.actualHours, 2);
|
||||
assert.equal(metrics.testing.estimateHours, 2.25);
|
||||
assert.equal(metrics.testing.aiEstimateHours, 1.5);
|
||||
assert.equal(metrics.bug.actualHours, 0.5);
|
||||
});
|
||||
|
||||
test('calcStageEffortMetrics uses updatedAt for terminal test cases missing completedAt', () => {
|
||||
const metrics = calcStageEffortMetrics({
|
||||
plans: [],
|
||||
devTasks: [],
|
||||
testCases: [
|
||||
testCase({
|
||||
status: 'passed',
|
||||
startedAt: '2026-06-25T09:00:00',
|
||||
completedAt: undefined,
|
||||
updatedAt: '2026-06-25T09:20:00',
|
||||
}),
|
||||
],
|
||||
bugs: [],
|
||||
now: new Date('2026-06-26T18:00:00'),
|
||||
});
|
||||
|
||||
assert.equal(metrics.testing.actualHours, 0.5);
|
||||
});
|
||||
|
||||
test('calcVersionOverviewEffortTotals sums actual hours and overtime records separately', () => {
|
||||
const overtimeRecords: OvertimeRecord[] = [
|
||||
{
|
||||
id: 'ot-1',
|
||||
projectId: 'project-1',
|
||||
versionId: 'v-1',
|
||||
person: 'Alice',
|
||||
startTime: '2026-06-22T19:00:00',
|
||||
endTime: '2026-06-22T20:15:00',
|
||||
duration: 1.25,
|
||||
reasonId: 'reason-3',
|
||||
createdAt: '2026-06-22T20:20:00',
|
||||
},
|
||||
{
|
||||
id: 'ot-2',
|
||||
projectId: 'project-1',
|
||||
versionId: 'v-1',
|
||||
person: 'Bob',
|
||||
startTime: '2026-06-22T20:00:00',
|
||||
endTime: '2026-06-22T22:03:00',
|
||||
duration: 2.05,
|
||||
reasonId: 'reason-4',
|
||||
createdAt: '2026-06-22T22:10:00',
|
||||
},
|
||||
];
|
||||
|
||||
const totals = calcVersionOverviewEffortTotals({
|
||||
plans: [plan({ actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' })],
|
||||
devTasks: [devTask({ actualStartAt: '2026-06-22T10:00:00', actualEndAt: '2026-06-22T12:00:00' })],
|
||||
testCases: [],
|
||||
bugs: [],
|
||||
overtimeRecords,
|
||||
});
|
||||
|
||||
assert.equal(totals.actualHours, 11);
|
||||
assert.equal(totals.overtimeHours, 3.3);
|
||||
});
|
||||
|
||||
test('calcPersonalEffortRanking includes bug work and sorts by total hours', () => {
|
||||
const overtimeRecords: OvertimeRecord[] = [
|
||||
{
|
||||
id: 'ot-1',
|
||||
projectId: 'project-1',
|
||||
versionId: 'v-1',
|
||||
person: 'Alice',
|
||||
startTime: '2026-06-23T19:00:00',
|
||||
endTime: '2026-06-23T20:30:00',
|
||||
duration: 1.5,
|
||||
reasonId: 'reason-3',
|
||||
createdAt: '2026-06-23T20:35:00',
|
||||
},
|
||||
{
|
||||
id: 'ot-2',
|
||||
projectId: 'project-1',
|
||||
versionId: 'v-1',
|
||||
person: 'Bob',
|
||||
startTime: '2026-06-25T19:00:00',
|
||||
endTime: '2026-06-25T21:00:00',
|
||||
duration: 2,
|
||||
reasonId: 'reason-4',
|
||||
createdAt: '2026-06-25T21:10:00',
|
||||
},
|
||||
];
|
||||
const ranking = calcPersonalEffortRanking({
|
||||
plans: [plan({ owner: 'Alice', actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' })],
|
||||
devTasks: [devTask({ assigneeId: 'Alice', actualStartAt: '2026-06-23T09:00:00', actualEndAt: '2026-06-23T11:00:00' })],
|
||||
testCases: [testCase({ assigneeId: 'Bob', startedAt: '2026-06-24T09:00:00', completedAt: '2026-06-24T11:00:00' })],
|
||||
bugs: [bug({ assigneeId: 'Bob', createdAt: '2026-06-25T10:00:00', closedAt: '2026-06-25T12:00:00' })],
|
||||
overtimeRecords,
|
||||
});
|
||||
|
||||
assert.equal(ranking[0].name, 'Alice');
|
||||
assert.equal(ranking[0].actualHours, 11);
|
||||
assert.equal(ranking[0].overtimeHours, 1.5);
|
||||
assert.equal(ranking[0].total, 12.5);
|
||||
assert.equal(ranking[1].name, 'Bob');
|
||||
assert.equal(ranking[1].actualHours, 4);
|
||||
assert.equal(ranking[1].overtimeHours, 2);
|
||||
assert.equal(ranking[1].total, 6);
|
||||
});
|
||||
|
||||
test('calcBugSeverityRanking groups bugs by assignee and severity', () => {
|
||||
const ranking = calcBugSeverityRanking([
|
||||
bug({ id: 'bug-1', assigneeId: 'Alice', severity: 'critical' }),
|
||||
bug({ id: 'bug-2', assigneeId: 'Alice', severity: 'major' }),
|
||||
bug({ id: 'bug-3', assigneeId: 'Alice', severity: 'minor' }),
|
||||
bug({ id: 'bug-4', assigneeId: 'Bob', severity: 'critical' }),
|
||||
bug({ id: 'bug-5', assigneeId: 'Bob', severity: 'trivial' }),
|
||||
bug({ id: 'bug-6', assigneeId: 'Alice', severity: 'trivial' }),
|
||||
]);
|
||||
|
||||
assert.deepEqual(ranking, [
|
||||
{
|
||||
assigneeId: 'Alice',
|
||||
critical: 1,
|
||||
major: 1,
|
||||
minor: 1,
|
||||
trivial: 1,
|
||||
total: 4,
|
||||
},
|
||||
{
|
||||
assigneeId: 'Bob',
|
||||
critical: 1,
|
||||
major: 0,
|
||||
minor: 0,
|
||||
trivial: 1,
|
||||
total: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user