| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- 'use strict';
- const assert = require('node:assert/strict');
- const fs = require('node:fs');
- const path = require('node:path');
- const vm = require('node:vm');
- /** @param {string} source 源码 @param {string} name 方法名 @returns {string} 可执行方法。 */
- function extract(source, name) {
- const match = new RegExp('export (?:async )?function ' + name + '\\([\\s\\S]*?\\n}').exec(source);
- assert.ok(match, '缺少方法 ' + name);
- return match[0].replace('export ', '');
- }
- /** @param {string} file 源文件 @returns {object} 隔离运行态。 */
- function library(file) {
- const source = fs.readFileSync(file, 'utf8');
- const names = ['getRepairMonthText', 'getDayApprovalLock', 'resolveHourValue', 'getOvertimeChangeAdjustment',
- 'getOvertimeDisplayTotals', 'resolveOvertimeRecord', 'buildOvertimeBaseline', 'applyDefaultWorkdayHours',
- 'processTimecardRecords', 'getAdjustmentSummary', 'collectApprovalData', 'collectOtherApprovalData',
- 'buildApprovalGroupKey', 'isRepairDayLocked', 'isRepairDayAllowed', 'loadTimecardData',
- 'prepareTimecardRecords', 'updateCellValue', 'buildTimecardRepairRequest'];
- const scope = {};
- vm.createContext(scope);
- vm.runInContext(source.slice(0, source.indexOf('export function didMount'))
- + names.map(name => extract(source, name)).join('\n'), scope);
- return scope;
- }
- /** @param {object} lib 方法与字段 @param {string} mode 场景 @returns {object} 页面实例。 */
- function instance(lib, mode = 'NORMAL') {
- const env = { ...lib, state: { selectedYear: 2026, selectedMonth: 8, userId: 'user',
- isExternalStaff: true, repairMode: mode, userInfo: {staffType: '外部'}, reentryAllowedDays: [] },
- isBeforeEmploymentDate: () => false,
- getTexts: () => ({ repairContextIncomplete: 'incomplete', staffTypeCheckFailed: 'staff mismatch' }) };
- env.setState = update => Object.assign(env.state, update);
- return env;
- }
- /** @param {object} lib 字段映射 @param {number} day 日期 @param {number} hours 工时 @param {boolean} repair 补填 @param {boolean} approved 已审批 @returns {object} 测试实例。 */
- function record(lib, day, hours, repair = false, approved = true) {
- const main = lib.TIME_FIELD_IDS;
- const row = lib.BILLABLE_SUB;
- const data = {};
- data[main.dayText] = '202608' + String(day).padStart(2, '0');
- data[main.dailyDraftTotal] = hours;
- data[main.dailySubmittedTotal] = approved ? hours : 0;
- data[main.dailyApprovedTotal] = approved ? hours : 0;
- data[repair ? lib.REPAIR_APPROVAL_STATUS_FIELD : main.approvalStatus] = approved ? '已审批' : '暂存';
- data[row.table] = [{ [row.projectCode]: 'P', [row.draft]: hours,
- [row.submitted]: approved ? hours : 0, [row.approved]: approved ? hours : 0 }];
- if (repair) data[lib.REPAIR_FILL_TYPE_FIELD] = '补填';
- return { formInstId: (repair ? 'repair-' : 'original-') + day, formData: data };
- }
- /** @param {object} env 页面 @param {Array<object>} original 正式记录 @param {Array<object>} repair 补填记录 @returns {void} 完整加载处理链。 */
- function process(env, original, repair) {
- env.processTimecardRecords(env.prepareTimecardRecords(original, repair), 2026, 8, {},
- [{code: 'P', name: 'Project', managerId: 'manager'}], false, false);
- }
- /** @returns {Promise<void>} 验证真实问题形态与重录隔离。 */
- async function main() {
- for (const file of ['timecard-repair.oyd.jsx', 'timecard-repair-lifecycle.oyd.jsx']) {
- const lib = library(path.resolve(__dirname, '../src', file));
- 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];
- const original = days.map(day => record(lib, day, day === 8 ? 4 : [6,7,11,12,13,18].includes(day) ? 10 : 8));
- const drafts = days.filter(day => new Date(2026,7,day).getDay() % 6 !== 0).map(day => record(lib, day, 0, true, false));
- const env = instance(lib);
- process(env, original, drafts);
- const summary = env.getAdjustmentSummary();
- assert.equal(summary.baseline, 192);
- assert.equal(summary.delta, 8);
- assert.equal(summary.total, 200);
- for (const day of days) {
- assert.equal(env.state.cellStatusMap['billable_0_' + day], 'approved');
- assert.equal(env.state.cellStatusMap['nonBillable_0_' + day], 'approved');
- assert.equal(env.state.cellStatusMap['other_0_' + day], 'approved');
- const before = env.state.billableData[0][day];
- env.updateCellValue('billable', 0, day, 99);
- assert.equal(env.state.billableData[0][day], before);
- }
- let groups = env.collectApprovalData();
- let rows = Object.values(groups).flatMap(group => group.billableRows);
- assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260831']);
- const request = await env.buildTimecardRepairRequest();
- assert.equal(request.repairMode, 'NORMAL');
- process(env, original, drafts.concat([record(lib,31,8,true)]));
- assert.equal(env.isRepairDayLocked(31), true);
- assert.equal(Object.keys(env.collectApprovalData()).length, 0, '31日已审批补填不可再次提交');
- const reentry = instance(lib, 'REENTRY');
- reentry.state.reentryAllowedDays = ['20260803'];
- process(reentry, original, []);
- assert.equal(reentry.getAdjustmentSummary().baseline, 0, '重录不回带已冲销原审批基线');
- assert.equal(reentry.state.billableData[0][3], 8);
- assert.equal(Object.keys(reentry.state.billableData[0]).length, 1, '范围外日期不得默认8h');
- assert.equal(reentry.isRepairDayLocked(4), true);
- rows = Object.values(reentry.collectApprovalData()).flatMap(group => group.billableRows);
- assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260803']);
- await assert.rejects(reentry.loadTimecardData('user',2026,8), /incomplete/, '重录不能原生回退绕过资格');
- await assert.rejects(env.loadTimecardData('user',2026,8,
- {staffType:'外部',repairMode:'REENTRY',summaryReadMode:'repair-only',originalRecords:[],repairRecords:[]}), /incomplete/);
- const change = record(lib,4,-8,true);
- change.formData[lib.REPAIR_FILL_TYPE_FIELD] = '变更';
- assert.equal(env.prepareTimecardRecords([], [change]).length,0);
- }
- console.log('PASS 双补填入口:192h基线、零草稿覆盖保护、只提交31日8h、已审批补填锁定、重录范围及模式隔离');
- }
- main().catch(error => { console.error(error); process.exitCode = 1; });
|