Files
ftb-project-management/apps/web/lib/test-case.test.ts
2026-06-29 14:18:58 +08:00

369 lines
12 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 preserves planned test time', () => {
const tc = normalizeTestCase({
id: 'tc-1',
caseNo: 'TC-001',
versionId: 'v1',
title: 'scheduled test case',
priority: 'P2',
status: 'pending',
categoryId: 'cat-test-api',
plannedTestAt: '2026-07-01T09:30:00.000Z',
createdBy: 'tester',
createdAt: '2026-06-25',
updatedAt: '2026-06-25',
} as any);
assert.equal(tc.plannedTestAt, '2026-07-01T09:30:00.000Z');
});
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,
plannedTestAt: '2026-07-01T09:30:00.000Z',
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.equal(copied.plannedTestAt, '2026-07-01T09:30:00.000Z');
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);
});