349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
TEST_CASE_STATUS_LABEL,
|
|
aggregateTestCaseHours,
|
|
calcTestProgress,
|
|
canStartNextTestRound,
|
|
copyTestCaseToRound,
|
|
getNextTestRoundNo,
|
|
getRequirementDeliveryStatus,
|
|
getTestCaseEstimateHours,
|
|
getTestCaseRoundNo,
|
|
normalizeTestCase,
|
|
} from './test-case';
|
|
import { DEFAULT_TEST_CATEGORY_ID } from './task-category';
|
|
|
|
test('normalizeTestCase backfills missing categoryId', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '登录正常',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(tc.categoryId, DEFAULT_TEST_CATEGORY_ID);
|
|
});
|
|
|
|
test('normalizeTestCase keeps existing categoryId', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '登录正常',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-api',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(tc.categoryId, 'cat-test-api');
|
|
});
|
|
|
|
test('normalizeTestCase backfills missing roundNo to the first test round', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '第一轮用例',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-api',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(tc.roundNo, 1);
|
|
assert.equal(getTestCaseRoundNo(tc), 1);
|
|
});
|
|
|
|
test('getNextTestRoundNo returns one more than the current max round', () => {
|
|
const cases = [
|
|
normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: 'R1', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: 'R3', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 3, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
];
|
|
|
|
assert.equal(getNextTestRoundNo(cases), 4);
|
|
});
|
|
|
|
test('canStartNextTestRound requires latest round cases all tested', () => {
|
|
assert.equal(canStartNextTestRound([]), false);
|
|
assert.equal(canStartNextTestRound([
|
|
normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '待测', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
]), false);
|
|
assert.equal(canStartNextTestRound([
|
|
normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '不通过', priority: 'P2', status: 'failed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
]), true);
|
|
assert.equal(canStartNextTestRound([
|
|
normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '首轮通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '二轮待测', priority: 'P2', status: 'running', categoryId: 'cat-test-api', roundNo: 2, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any),
|
|
]), false);
|
|
});
|
|
|
|
test('getRequirementDeliveryStatus marks a requirement submitted only when all its dev tasks are submitted', () => {
|
|
const tasks = [
|
|
{ requirementId: 'req-1', status: 'submitted' },
|
|
{ requirementId: 'req-1', status: 'submitted' },
|
|
{ requirementId: 'req-2', status: 'testing' },
|
|
] as any;
|
|
|
|
assert.equal(getRequirementDeliveryStatus('req-1', tasks), 'submitted');
|
|
assert.equal(getRequirementDeliveryStatus('req-2', tasks), 'pending');
|
|
assert.equal(getRequirementDeliveryStatus('req-empty', tasks), 'pending');
|
|
});
|
|
|
|
test('copyTestCaseToRound preserves estimates and references but clears execution state', () => {
|
|
const source = normalizeTestCase({
|
|
id: 'tc-source',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
requirementId: 'req-1',
|
|
title: '登录正常',
|
|
description: '步骤',
|
|
priority: 'P1',
|
|
status: 'passed',
|
|
categoryId: 'cat-test-functional',
|
|
estimateHours: 0.75,
|
|
aiEstimateHours: 0.25,
|
|
assigneeId: 'QA',
|
|
startedAt: '2026-06-25T01:00:00.000Z',
|
|
completedAt: '2026-06-25T02:00:00.000Z',
|
|
executedAt: '2026-06-25T02:00:00.000Z',
|
|
failReason: 'old fail',
|
|
blockReason: 'old block',
|
|
references: [{ type: 'requirement', id: 'req-1', label: 'REQ-001 登录' }],
|
|
aiDraft: true,
|
|
aiDraftAt: '2026-06-25T00:00:00.000Z',
|
|
roundNo: 1,
|
|
createdBy: 'AI',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
const copied = copyTestCaseToRound(source, 2, 'tester');
|
|
|
|
assert.equal(copied.roundNo, 2);
|
|
assert.equal(copied.sourceCaseId, 'tc-source');
|
|
assert.equal(copied.aiEstimateHours, 0.25);
|
|
assert.equal(copied.estimateHours, 0.75);
|
|
assert.deepEqual(copied.references, source.references);
|
|
assert.equal(copied.startedAt, undefined);
|
|
assert.equal(copied.completedAt, undefined);
|
|
assert.equal(copied.executedAt, undefined);
|
|
assert.equal(copied.failReason, undefined);
|
|
assert.equal(copied.blockReason, undefined);
|
|
assert.equal(copied.createdBy, 'tester');
|
|
});
|
|
|
|
test('normalizeTestCase keeps missing executor estimate empty', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '登录正常',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-api',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(tc.estimateHours, undefined);
|
|
assert.equal(getTestCaseEstimateHours(tc), 0);
|
|
});
|
|
|
|
test('getTestCaseEstimateHours prefers executor estimate over AI estimate', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '登录正常',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-api',
|
|
estimateHours: 0.7,
|
|
aiEstimateHours: 0.2,
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(getTestCaseEstimateHours(tc), 0.7);
|
|
});
|
|
|
|
test('getTestCaseEstimateHours falls back to AI estimate', () => {
|
|
const tc = normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '登录正常',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-api',
|
|
aiEstimateHours: 0.2,
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any);
|
|
|
|
assert.equal(getTestCaseEstimateHours(tc), 0.2);
|
|
});
|
|
|
|
test('test case status labels use waiting and testing wording', () => {
|
|
assert.equal(TEST_CASE_STATUS_LABEL.pending, '待测试');
|
|
assert.equal(TEST_CASE_STATUS_LABEL.running, '测试中');
|
|
assert.equal(TEST_CASE_STATUS_LABEL.failed, '不通过');
|
|
});
|
|
|
|
test('calcTestProgress uses estimate-weighted completion', () => {
|
|
const progress = calcTestProgress([
|
|
normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '待测试',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-functional',
|
|
estimateHours: 1,
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-2',
|
|
caseNo: 'TC-002',
|
|
versionId: 'v1',
|
|
title: '已通过',
|
|
priority: 'P2',
|
|
status: 'passed',
|
|
categoryId: 'cat-test-functional',
|
|
estimateHours: 3,
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
]);
|
|
|
|
assert.equal(progress.completionRate, 75);
|
|
});
|
|
|
|
test('calcTestProgress calculates pass rate from passed cases that never had effective bugs', () => {
|
|
const progress = calcTestProgress([
|
|
normalizeTestCase({
|
|
id: 'tc-clean',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '无 Bug 通过',
|
|
priority: 'P2',
|
|
status: 'passed',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-bug-fixed',
|
|
caseNo: 'TC-002',
|
|
versionId: 'v1',
|
|
title: '提过 Bug 后通过',
|
|
priority: 'P2',
|
|
status: 'passed',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-failed',
|
|
caseNo: 'TC-003',
|
|
versionId: 'v1',
|
|
title: '不通过',
|
|
priority: 'P2',
|
|
status: 'failed',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-running-with-bug',
|
|
caseNo: 'TC-004',
|
|
versionId: 'v1',
|
|
title: '测试中已提 Bug',
|
|
priority: 'P2',
|
|
status: 'running',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-rejected-only',
|
|
caseNo: 'TC-005',
|
|
versionId: 'v1',
|
|
title: '仅有驳回 Bug',
|
|
priority: 'P2',
|
|
status: 'passed',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
normalizeTestCase({
|
|
id: 'tc-pending',
|
|
caseNo: 'TC-006',
|
|
versionId: 'v1',
|
|
title: '未执行',
|
|
priority: 'P2',
|
|
status: 'pending',
|
|
categoryId: 'cat-test-functional',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
], [
|
|
{ testCaseId: 'tc-bug-fixed', status: 'closed' },
|
|
{ testCaseId: 'tc-running-with-bug', status: 'open' },
|
|
{ testCaseId: 'tc-rejected-only', status: 'rejected' },
|
|
]);
|
|
|
|
assert.equal(progress.executed, 5);
|
|
assert.equal(progress.passed, 3);
|
|
assert.equal(progress.passRate, 40);
|
|
});
|
|
|
|
test('aggregateTestCaseHours returns estimate and actual totals', () => {
|
|
const hours = aggregateTestCaseHours([
|
|
normalizeTestCase({
|
|
id: 'tc-1',
|
|
caseNo: 'TC-001',
|
|
versionId: 'v1',
|
|
title: '测试中',
|
|
priority: 'P2',
|
|
status: 'running',
|
|
categoryId: 'cat-test-functional',
|
|
estimateHours: 1,
|
|
startedAt: '2026-06-25T01:00:00.000Z',
|
|
createdBy: 'tester',
|
|
createdAt: '2026-06-25',
|
|
updatedAt: '2026-06-25',
|
|
} as any),
|
|
], new Date('2026-06-25T02:00:00.000Z'));
|
|
|
|
assert.equal(hours.estimate, 1);
|
|
assert.equal(hours.actual, 1);
|
|
});
|