|
|
@@ -0,0 +1,98 @@
|
|
|
+'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 extractFunction(source, name) {
|
|
|
+ const pattern = new RegExp('export (?:async )?function ' + name + '\\(');
|
|
|
+ const match = pattern.exec(source);
|
|
|
+ assert.ok(match, '缺少方法 ' + name);
|
|
|
+ const end = source.indexOf('\n/**', match.index + match[0].length);
|
|
|
+ return source.slice(match.index, end < 0 ? source.length : end).replace(/^export /, '');
|
|
|
+}
|
|
|
+
|
|
|
+/** @param {object} options @returns {object} */
|
|
|
+function createContext(options = {}) {
|
|
|
+ const state = {
|
|
|
+ selectedYear: 2026, selectedMonth: 8, userId: '626968159',
|
|
|
+ userInfo: { userName: '冷志刚/Zhigang Leng', staffType: '内部', managerId: '640891109' },
|
|
|
+ projectRows: [], ...options.state,
|
|
|
+ };
|
|
|
+ return {
|
|
|
+ state,
|
|
|
+ collectApprovalData: () => options.projectGroups || {},
|
|
|
+ collectOtherApprovalData: () => ({
|
|
|
+ otherRows: [{ dateStr: '20260813', activityName: 'Annual Leave', hours: 8 }],
|
|
|
+ otherTotal: 8, ...options.otherGroup,
|
|
|
+ }),
|
|
|
+ getRepairMonthText: (year, month) => String(year) + String(month).padStart(2, '0'),
|
|
|
+ fetchDeptManagerId: methods.fetchDeptManagerId,
|
|
|
+ buildTimecardRepairRequest: methods.buildTimecardRepairRequest,
|
|
|
+ getTexts: () => ({ repairBackendPending: '补填连接器未配置' }),
|
|
|
+ unwrapConnectorResponse: (response) => response.data,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const sourcePath = path.resolve(__dirname, '../src/timecard-repair.oyd.jsx');
|
|
|
+const schemaFile = process.argv[2];
|
|
|
+const schema = schemaFile ? JSON.parse(fs.readFileSync(schemaFile, 'utf8')).content : null;
|
|
|
+const compiled = process.argv[3] === 'compiled';
|
|
|
+const source = schema ? schema.actions.module[compiled ? 'compiled' : 'source'] : fs.readFileSync(sourcePath, 'utf8');
|
|
|
+const methods = vm.runInNewContext(
|
|
|
+ (compiled ? source : ['buildTimecardRepairRequest', 'submitTimecardRepairRequest', 'fetchDeptManagerId']
|
|
|
+ .map((name) => extractFunction(source, name)).join('\n'))
|
|
|
+ + '\n({buildTimecardRepairRequest, submitTimecardRepairRequest, fetchDeptManagerId})',
|
|
|
+ { exports: {}, REPAIR_DATA_SOURCES: { submit: 'startTimecardRepair' } },
|
|
|
+);
|
|
|
+
|
|
|
+/** @returns {Promise<void>} */
|
|
|
+async function main() {
|
|
|
+ const cases = [
|
|
|
+ ['内部员工缓存主管', { state: { deptManagerInfo: { managerId: '640891109', isFallback: false } } }, '640891109'],
|
|
|
+ ['缓存缺失时从内部人员档案获取', {}, '640891109'],
|
|
|
+ ['外部员工从有效项目获取', { state: {
|
|
|
+ userInfo: { staffType: '外部' },
|
|
|
+ projectRows: [{ managerId: '634735129' }, { managerId: 'project-manager' }],
|
|
|
+ } }, 'project-manager'],
|
|
|
+ ['内部员工未配置主管时保留业务兜底', { state: { userInfo: { staffType: '内部' } } }, '634735129'],
|
|
|
+ ['外部员工无有效项目经理时保留业务兜底', { state: { userInfo: { staffType: '外部' } } }, '634735129'],
|
|
|
+ ];
|
|
|
+ for (const [name, options, expected] of cases) {
|
|
|
+ const context = createContext(options);
|
|
|
+ const request = await methods.buildTimecardRepairRequest.call(context);
|
|
|
+ assert.equal(request.otherGroup.managerId, expected, name);
|
|
|
+ assert.equal(request.otherGroup.otherTotal, 8, name);
|
|
|
+ assert.equal(request.otherGroup.otherRows[0].hours, 8, name);
|
|
|
+ assert.equal(request.sourceMonthText, '202608', name);
|
|
|
+ assert.equal(request.submitterUserId, '626968159', name);
|
|
|
+ console.log('PASS ' + name);
|
|
|
+ }
|
|
|
+ const projectGroups = { pm: { managerId: 'original-pm', billableTotal: 8, nonBillableTotal: 0 } };
|
|
|
+ const projectOnly = createContext({ projectGroups, otherGroup: { otherRows: [], otherTotal: 0 } });
|
|
|
+ projectOnly.fetchDeptManagerId = () => { throw new Error('纯项目补填无需获取其他工时主管'); };
|
|
|
+ assert.equal((await methods.buildTimecardRepairRequest.call(projectOnly)).projectGroups, projectGroups);
|
|
|
+ console.log('PASS 项目工时分组保持原样');
|
|
|
+
|
|
|
+ const context = createContext();
|
|
|
+ let submitted;
|
|
|
+ context.getCallableDataSource = () => ({ load: async (payload) => {
|
|
|
+ submitted = JSON.parse(payload.inputs).Body;
|
|
|
+ return { data: { status: '审批中' } };
|
|
|
+ } });
|
|
|
+ const result = await methods.submitTimecardRepairRequest.call(context);
|
|
|
+ assert.equal(submitted.otherGroup.managerId, '640891109');
|
|
|
+ assert.equal(submitted.otherGroup.otherRows[0].activityName, 'Annual Leave');
|
|
|
+ assert.equal(result.status, '审批中');
|
|
|
+ console.log('PASS 连接器实际请求携带 Kevin ID');
|
|
|
+
|
|
|
+ const failedContext = createContext();
|
|
|
+ failedContext.fetchDeptManagerId = async () => { throw new Error('主管获取失败'); };
|
|
|
+ failedContext.getCallableDataSource = () => ({ load: () => assert.fail('主管获取失败后不得提交') });
|
|
|
+ await assert.rejects(methods.submitTimecardRepairRequest.call(failedContext), /主管获取失败/);
|
|
|
+ console.log('PASS 主管获取失败阻止提交');
|
|
|
+}
|
|
|
+
|
|
|
+main().catch((error) => { console.error(error); process.exitCode = 1; });
|