Files
railwaysystem/build_ui/tests/unit/phase0ProductDefectsRemediation.spec.mjs
T
wangzhibiao 62085e8a49 修复培训:支持逾期报到人工确认并修复学员选择器字段映射
fix(train): handle overdue report confirmation and student picker fields
2026-09-23 19:08:50 +08:00

291 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const planPagePath = path.resolve(__dirname, '../../src/views/train/plan/index.vue');
const enrollPanelPath = path.resolve(__dirname, '../../src/views/train/plan/components/EnrollmentPanel.vue');
const requestPath = path.resolve(__dirname, '../../src/utils/request.js');
const enrollApiPath = path.resolve(__dirname, '../../src/api/train/enrollment.js');
const planSource = fs.readFileSync(planPagePath, 'utf8');
const enrollSource = fs.readFileSync(enrollPanelPath, 'utf8');
const requestSource = fs.readFileSync(requestPath, 'utf8');
const enrollApiSource = fs.readFileSync(enrollApiPath, 'utf8');
console.log('=== Running Phase 0 Product Defects Remediation Regression Suite ===\n');
// ==========================================
// 1. DEFECT 1 — REPORT-OVERDUE-01 (BLOCKER)
// ==========================================
console.log('--- Checking DEFECT 1: REPORT-OVERDUE-01 ---');
{
// 1.1 request-level silentErrorCodes contract
assert.strictEqual(
requestSource.includes('const silentCodes = (res.config && res.config.silentErrorCodes) || []'),
true,
'request.js 必须通过 res.config.silentErrorCodes 按需抑制全局 Notification'
);
assert.strictEqual(
requestSource.includes('!silentCodes.includes(code)'),
true,
'当业务错误码不在 silentErrorCodes 中时必须正常触发 Notification.error'
);
assert.strictEqual(
enrollApiSource.includes('silentErrorCodes: [40051]'),
true,
'confirmReport API 请求必须显式声明 silentErrorCodes: [40051]'
);
assert.strictEqual(
requestSource.includes('err.code = code'),
true,
'request.js 必须在 rejected Error 上附加业务错误码 code'
);
// 1.2 模拟 request.js 拦截器行为:仅当请求配置 silentErrorCodes 包含对应 code 时抑制
let notifications = [];
const mockNotification = {
error: (opts) => { notifications.push(opts.title); }
};
const simulateResponseInterceptor = (res) => {
const code = res.data.code || 200;
const msg = res.data.msg || '系统未知错误';
if (code !== 200) {
const silentCodes = (res.config && res.config.silentErrorCodes) || [];
if (!silentCodes.includes(code)) {
mockNotification.error({ title: msg });
}
const err = new Error(msg);
err.code = code;
err.data = res.data;
return Promise.reject(err);
}
return Promise.resolve(res.data);
};
// 测试用例 A: confirmReport 携带 silentErrorCodes: [40051],40051 被抑制
notifications = [];
try {
await simulateResponseInterceptor({
data: { code: 40051, msg: '报到时限提醒' },
config: { url: '/train/enrollments/1/report', silentErrorCodes: [40051] }
});
} catch (e) {
assert.strictEqual(e.code, 40051);
}
assert.strictEqual(notifications.length, 0, 'confirmReport 请求收到 40051 不得触发全局 Notification.error');
// 测试用例 B: 其他普通请求收到 40051,未配置 silentErrorCodes,必须正常弹出 Notification.error
notifications = [];
try {
await simulateResponseInterceptor({
data: { code: 40051, msg: '其他模块时限提醒' },
config: { url: '/train/other/action' }
});
} catch (e) {
assert.strictEqual(e.code, 40051);
}
assert.strictEqual(notifications.length, 1, '未配置 silentErrorCodes 的普通请求收到 40051 必须正常弹出全局 Notification.error');
assert.strictEqual(notifications[0], '其他模块时限提醒');
// 1.3 EnrollmentPanel.vue: doConfirmReport & 循环防重控制
assert.strictEqual(
enrollSource.includes('!forceOverride && isOverdueWarning'),
true,
'二次确认弹窗必须严格限定在首次提交 (!forceOverride) 且发生逾期警告时触发'
);
assert.strictEqual(
enrollSource.includes('this.$modal.msgError((err && err.message) || \'报到失败\')'),
true,
'override 请求失败或非逾期错误必须结束流程并展示实际错误,不得再次弹窗'
);
// 1.4 模拟报到确认全生命周期:
// 场景 A: initial 40051 -> confirm dialog exactly once -> cancel -> no mutation
// 场景 B: initial 40051 -> confirm dialog exactly once -> confirm -> override success
// 场景 C: initial 40051 -> confirm -> override request fails with 40051 again -> NO second dialog, error message displayed
let dialogShownCount = 0;
let modalErrors = [];
const simulateDoConfirmReport = async (row, forceOverride = false, backendBehavior = 'normal_overdue') => {
const payload = {
reportWay: 4,
verifyNote: '管理员人工核验到班'
};
if (forceOverride) {
payload.forceOverride = true;
}
try {
if (!forceOverride) {
const err = new Error('已超过本期培训报到截止时间,触发时限提醒');
err.code = 40051;
throw err;
}
if (backendBehavior === 'override_fails_again') {
const err = new Error('强制逾期报到仍被拒绝:业务已封存');
err.code = 40051;
throw err;
}
return { code: 200, msg: '报到成功' };
} catch (err) {
const isOverdueWarning = err && (
err.code === 40051 ||
(err.message && (err.message.includes('40051') || err.message.includes('已超过本期培训报到截止时间') || err.message.includes('报到截止时间')))
);
if (!forceOverride && isOverdueWarning) {
dialogShownCount++;
// 模拟用户确认
return simulateDoConfirmReport(row, true, backendBehavior);
} else {
modalErrors.push(err.message || '报到失败');
return { failed: true, error: err };
}
}
};
// 回归测试 1: 首次 40051 弹窗刚好 1 次,用户确认后二次提交成功
dialogShownCount = 0;
modalErrors = [];
const resSuccess = await simulateDoConfirmReport({ enrollmentId: 101 }, false, 'normal_overdue');
assert.strictEqual(dialogShownCount, 1, '逾期报到确认弹窗必须刚好出现 1 次');
assert.strictEqual(resSuccess.code, 200, '二次确认携带 override 后成功');
assert.strictEqual(modalErrors.length, 0, '成功流程不得展示错误弹窗');
// 回归测试 2: 二次提交 (forceOverride=true) 再次收到 40051,严禁再次弹窗,必须结束流程并展示实际错误
dialogShownCount = 0;
modalErrors = [];
const resFail = await simulateDoConfirmReport({ enrollmentId: 101 }, false, 'override_fails_again');
assert.strictEqual(dialogShownCount, 1, 'override 再次失败时不得出现第 2 次确认弹窗');
assert.strictEqual(modalErrors.length, 1, 'override 失败必须展示实际错误');
assert.strictEqual(modalErrors[0], '强制逾期报到仍被拒绝:业务已封存');
console.log('✔ PASS: REPORT-OVERDUE-01 verified (request-level suppression + no confirmation loop + single prompt)');
}
// ==========================================
// 2. DEFECT 2 — ENROLL-PICKER-01 (MAJOR)
// ==========================================
console.log('\n--- Checking DEFECT 2: ENROLL-PICKER-01 ---');
{
// 2.1 修复 sGender=0 的 falsy 缺陷
assert.strictEqual(
enrollSource.includes('scope.row.sGender !== undefined && scope.row.sGender !== null ? scope.row.sGender : scope.row.gender'),
true,
'性别列必须显式判断 scope.row.sGender !== undefined && !== null,禁止将 0 当作 falsy 过滤'
);
const formatGender = (val) => {
if (val === '0' || val === 0 || val === '男') return '男';
if (val === '1' || val === 1 || val === '女') return '女';
return '-';
};
// 性别映射全覆盖测试
assert.strictEqual(formatGender(0), '男', 'sGender=0 必须正确渲染为 男');
assert.strictEqual(formatGender('0'), '男', 'sGender="0" 必须正确渲染为 男');
assert.strictEqual(formatGender(1), '女', 'sGender=1 必须正确渲染为 女');
assert.strictEqual(formatGender('1'), '女', 'sGender="1" 必须正确渲染为 女');
assert.strictEqual(formatGender(null), '-', 'null 必须安全退化为 -');
assert.strictEqual(formatGender(undefined), '-', 'undefined 必须安全退化为 -');
assert.strictEqual(formatGender(''), '-', '空字符串必须安全退化为 -');
// 2.2 验证表格列字段映射
assert.strictEqual(
enrollSource.includes('prop="sName"'),
true,
'学员姓名列 prop 必须为 sName'
);
assert.strictEqual(
enrollSource.includes('scope.row.sName || scope.row.studentName || \'-\''),
true,
'学员姓名展示必须优先取 sName'
);
assert.strictEqual(
enrollSource.includes('prop="sGender"'),
true,
'学员性别列 prop 必须为 sGender'
);
assert.strictEqual(
enrollSource.includes('scope.row.idCardMasked || \'-\''),
true,
'身份证号列必须绑定 idCardMasked'
);
// 2.3 收敛 StudentQuery 检索字段:严格单字段 sName
assert.strictEqual(
enrollSource.includes("studentSearchQuery: {\n sName: ''\n }"),
true,
'studentSearchQuery 仅保留单一契约字段 sName,删除冗余的 studentName'
);
assert.strictEqual(
enrollSource.includes("sName: keyword || undefined"),
true,
'searchStudentPool 请求参数仅发送 sName: keyword'
);
assert.strictEqual(
enrollSource.includes("studentName: keyword"),
false,
'严禁在请求中同时冗余发送 studentName'
);
// 2.4 selectedStudentText 缺失姓名时占位符修复:严禁退回 sId / studentId
const getSelectedStudentText = (selectedStudent) => {
if (!selectedStudent) return '';
const name = (selectedStudent.sName || selectedStudent.studentName || '').trim();
const org = selectedStudent.orgName ? ` - ${selectedStudent.orgName}` : '';
return name ? `${name}${org}` : '-';
};
// 测试用例 A: 完整学员
const normalStudent = {
sId: 'S1001',
sName: '张伟',
orgName: '大连供电段'
};
assert.strictEqual(getSelectedStudentText(normalStudent), '张伟 - 大连供电段');
// 测试用例 B: 姓名缺失但存在 sId / studentId 的异常情况
const anonymousStudent = {
sId: 'S9999',
studentId: 9999,
sName: '',
orgName: '沈阳车辆段'
};
const textAnonymous = getSelectedStudentText(anonymousStudent);
assert.strictEqual(textAnonymous, '-', '姓名缺失时必须展示明确占位符 -,严禁退化展示内部数字/编号 ID');
assert.strictEqual(textAnonymous.includes('S9999'), false, '展示严禁包含 internal ID');
assert.strictEqual(textAnonymous.includes('undefined'), false, '展示严禁包含 undefined');
console.log('✔ PASS: ENROLL-PICKER-01 verified (sGender falsy fixed + singular sName search + human-readable display)');
}
// ==========================================
// 3. AUDIT: PLAN-EDIT-01 & PLAN-DATE-RULE-01
// ==========================================
console.log('\n--- Auditing PLAN-EDIT-01 & PLAN-DATE-RULE-01 (Scope Rollback) ---');
{
assert.strictEqual(
planSource.includes("this.form.startTime >= this.form.endTime"),
true,
'原始计划表单已包含 startTime >= endTime 校验'
);
assert.strictEqual(
planSource.includes("this.form.reportDeadline > this.form.startTime"),
true,
'原始计划表单已包含 reportDeadline > startTime 校验'
);
assert.strictEqual(
planSource.includes("this.$modal.msgError('报到截止时间不得晚于开班时间')"),
true,
'原始计划表单已包含明确错误提示阻止非法提交'
);
console.log('✔ PASS: Verified PLAN-EDIT-01 and PLAN-DATE-RULE-01 are cleanly retained at baseline with original checks intact.');
}
console.log('\n================================================================');
console.log('ALL PHASE 0 DEFECT REMEDIATION REGRESSION TESTS PASSED (0 FAIL)');
console.log('================================================================');