verify-repair-scenarios.cjs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. 'use strict';
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const path = require('node:path');
  5. const vm = require('node:vm');
  6. /** @param {string} source 源码 @param {string} name 方法名 @returns {string} 可执行方法。 */
  7. function extract(source, name) {
  8. const match = new RegExp('export (?:async )?function ' + name + '\\([\\s\\S]*?\\n}').exec(source);
  9. assert.ok(match, '缺少方法 ' + name);
  10. return match[0].replace('export ', '');
  11. }
  12. /** @param {string} file 源文件 @returns {object} 隔离运行态。 */
  13. function library(file) {
  14. const source = fs.readFileSync(file, 'utf8');
  15. const names = ['getRepairMonthText', 'getDayApprovalLock', 'resolveHourValue', 'getOvertimeChangeAdjustment',
  16. 'getOvertimeDisplayTotals', 'resolveOvertimeRecord', 'buildOvertimeBaseline', 'applyDefaultWorkdayHours',
  17. 'processTimecardRecords', 'getAdjustmentSummary', 'collectApprovalData', 'collectOtherApprovalData',
  18. 'buildApprovalGroupKey', 'isRepairDayLocked', 'isRepairDayAllowed', 'loadTimecardData',
  19. 'prepareTimecardRecords', 'updateCellValue', 'buildTimecardRepairRequest'];
  20. const scope = {};
  21. vm.createContext(scope);
  22. vm.runInContext(source.slice(0, source.indexOf('export function didMount'))
  23. + names.map(name => extract(source, name)).join('\n'), scope);
  24. return scope;
  25. }
  26. /** @param {object} lib 方法与字段 @param {string} mode 场景 @returns {object} 页面实例。 */
  27. function instance(lib, mode = 'NORMAL') {
  28. const env = { ...lib, state: { selectedYear: 2026, selectedMonth: 8, userId: 'user',
  29. isExternalStaff: true, repairMode: mode, userInfo: {staffType: '外部'}, reentryAllowedDays: [] },
  30. isBeforeEmploymentDate: () => false,
  31. getTexts: () => ({ repairContextIncomplete: 'incomplete', staffTypeCheckFailed: 'staff mismatch' }) };
  32. env.setState = update => Object.assign(env.state, update);
  33. return env;
  34. }
  35. /** @param {object} lib 字段映射 @param {number} day 日期 @param {number} hours 工时 @param {boolean} repair 补填 @param {boolean} approved 已审批 @returns {object} 测试实例。 */
  36. function record(lib, day, hours, repair = false, approved = true) {
  37. const main = lib.TIME_FIELD_IDS;
  38. const row = lib.BILLABLE_SUB;
  39. const data = {};
  40. data[main.dayText] = '202608' + String(day).padStart(2, '0');
  41. data[main.dailyDraftTotal] = hours;
  42. data[main.dailySubmittedTotal] = approved ? hours : 0;
  43. data[main.dailyApprovedTotal] = approved ? hours : 0;
  44. data[repair ? lib.REPAIR_APPROVAL_STATUS_FIELD : main.approvalStatus] = approved ? '已审批' : '暂存';
  45. data[row.table] = [{ [row.projectCode]: 'P', [row.draft]: hours,
  46. [row.submitted]: approved ? hours : 0, [row.approved]: approved ? hours : 0 }];
  47. if (repair) data[lib.REPAIR_FILL_TYPE_FIELD] = '补填';
  48. return { formInstId: (repair ? 'repair-' : 'original-') + day, formData: data };
  49. }
  50. /** @param {object} env 页面 @param {Array<object>} original 正式记录 @param {Array<object>} repair 补填记录 @returns {void} 完整加载处理链。 */
  51. function process(env, original, repair) {
  52. env.processTimecardRecords(env.prepareTimecardRecords(original, repair), 2026, 8, {},
  53. [{code: 'P', name: 'Project', managerId: 'manager'}], false, false);
  54. }
  55. /** @returns {Promise<void>} 验证真实问题形态与重录隔离。 */
  56. async function main() {
  57. for (const file of ['timecard-repair.oyd.jsx', 'timecard-repair-lifecycle.oyd.jsx']) {
  58. const lib = library(path.resolve(__dirname, '../src', file));
  59. const days = [3,4,5,6,7,8,10,11,12,13,14,17,18,19,20,21,22,24,25,26,27,28,29];
  60. const original = days.map(day => record(lib, day, day === 8 ? 4 : [6,7,11,12,13,18].includes(day) ? 10 : 8));
  61. const drafts = days.filter(day => new Date(2026,7,day).getDay() % 6 !== 0).map(day => record(lib, day, 0, true, false));
  62. const env = instance(lib);
  63. process(env, original, drafts);
  64. const summary = env.getAdjustmentSummary();
  65. assert.equal(summary.baseline, 192);
  66. assert.equal(summary.delta, 8);
  67. assert.equal(summary.total, 200);
  68. for (const day of days) {
  69. assert.equal(env.state.cellStatusMap['billable_0_' + day], 'approved');
  70. assert.equal(env.state.cellStatusMap['nonBillable_0_' + day], 'approved');
  71. assert.equal(env.state.cellStatusMap['other_0_' + day], 'approved');
  72. const before = env.state.billableData[0][day];
  73. env.updateCellValue('billable', 0, day, 99);
  74. assert.equal(env.state.billableData[0][day], before);
  75. }
  76. let groups = env.collectApprovalData();
  77. let rows = Object.values(groups).flatMap(group => group.billableRows);
  78. assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260831']);
  79. const request = await env.buildTimecardRepairRequest();
  80. assert.equal(request.repairMode, 'NORMAL');
  81. process(env, original, drafts.concat([record(lib,31,8,true)]));
  82. assert.equal(env.isRepairDayLocked(31), true);
  83. assert.equal(Object.keys(env.collectApprovalData()).length, 0, '31日已审批补填不可再次提交');
  84. const reentry = instance(lib, 'REENTRY');
  85. reentry.state.reentryAllowedDays = ['20260803'];
  86. process(reentry, original, []);
  87. assert.equal(reentry.getAdjustmentSummary().baseline, 0, '重录不回带已冲销原审批基线');
  88. assert.equal(reentry.state.billableData[0][3], 8);
  89. assert.equal(Object.keys(reentry.state.billableData[0]).length, 1, '范围外日期不得默认8h');
  90. assert.equal(reentry.isRepairDayLocked(4), true);
  91. rows = Object.values(reentry.collectApprovalData()).flatMap(group => group.billableRows);
  92. assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260803']);
  93. await assert.rejects(reentry.loadTimecardData('user',2026,8), /incomplete/, '重录不能原生回退绕过资格');
  94. await assert.rejects(env.loadTimecardData('user',2026,8,
  95. {staffType:'外部',repairMode:'REENTRY',summaryReadMode:'repair-only',originalRecords:[],repairRecords:[]}), /incomplete/);
  96. const change = record(lib,4,-8,true);
  97. change.formData[lib.REPAIR_FILL_TYPE_FIELD] = '变更';
  98. assert.equal(env.prepareTimecardRecords([], [change]).length,0);
  99. }
  100. console.log('PASS 双补填入口:192h基线、零草稿覆盖保护、只提交31日8h、已审批补填锁定、重录范围及模式隔离');
  101. }
  102. main().catch(error => { console.error(error); process.exitCode = 1; });