'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 function ' + name + '\\([^]*?\\n}').exec(source); assert.ok(match, name); return match[0].replace('export ', ''); } /** @param {string} file 页面文件 @param {boolean} employment 是否验证任职控制 @returns {object} 独立页面上下文 */ function page(file, employment) { const source = fs.readFileSync(path.resolve(__dirname, '../src', file), 'utf8'); const names = ['isWeekend', 'isDayOff', 'getHolidayType', 'isBeisenLeaveWorkday', 'loadHolidays']; if (employment) names.push('getEmploymentDateKey', 'getEmploymentBounds', 'isOutsideEmploymentDates', 'isBeforeEmploymentDate', 'assertEmploymentInputs', 'assertPersonnelSnapshot', 'getDayTotal', 'getOvertimeDayTotal', 'validateMonthlyOvertime', 'applyDefaultWorkdayHours'); const scope = {APP_TYPE:'app', FORM_IDS:{holiday:'calendar'}, HOLIDAY_FIELD_IDS:{year:'year',date:'date',type:'type'}, VALIDATION:{maxHoursPerDay:8,maxOvertimePerMonth:36}}; vm.createContext(scope); vm.runInContext(names.map(name => extract(source, name)).join('\n'), scope); const env = {...scope, state:{selectedYear:2026,selectedMonth:10,userInfo:{}, billableData:{},nonBillableData:{},otherData:{},holidayMap:{1:'调休',10:'加班'}}, getTexts:()=>({employmentInvalid:'invalid dates',employmentDraftOutside:'outside ', personnelCalendarChanged:'reload',overtimeExceeded:()=> 'over 36'}), utils:{toast:()=>{}},showTimecardError:()=>{},isNormalTimecardBlocked:()=>false}; return env; } /** @returns {Promise} 实际业务方法的日期、日历、默认值和提交前复核测试 */ async function main() { const files = ['timecard-main.actions.js','timecard-repair.oyd.jsx','timecard-repair-lifecycle.oyd.jsx','timecard-change.oyd.jsx']; for (const file of files) { const employment = !file.includes('change'); const env = page(file, employment); let queries = 0; env.utils.yida = {searchFormDatas:async () => { queries++; return {data:[]}; }}; assert.equal(env.isDayOff(2026,10,1),true,'国内调休为休息日'); assert.equal(env.isDayOff(2026,10,10),false,'国内周末补班为工作日'); env.state.userInfo.isOverseasStaff = true; env._tcHolidayCalendarByYear = {2026:{20261001:'调休',20261010:'加班'}}; assert.equal(env.isDayOff(2026,10,1),false); assert.equal(env.isDayOff(2026,10,10),true); assert.equal(env.isBeisenLeaveWorkday(2026,10,1),true); assert.equal(env.isBeisenLeaveWorkday(2026,10,10),false); assert.equal(env.getHolidayType(1),null); assert.equal(Object.keys(await env.loadHolidays(2026,10)).length,0); assert.equal(Object.keys(env._tcHolidayCalendarByYear).length,0); assert.equal(queries,0,'海外员工不请求国内日历'); env.state.userInfo.isOverseasStaff = false; await env.loadHolidays(2026,10); assert.equal(queries,1,'切换回国内仍查询日历'); if (!employment) continue; env.state.userInfo = {employmentStartDate:'2026-10-01',employmentEndDate:'2026-10-15',isOverseasStaff:true}; assert.equal(env.isBeforeEmploymentDate(2026,9,30),true); assert.equal(env.isBeforeEmploymentDate(2026,10,1),false); assert.equal(env.isBeforeEmploymentDate(2026,10,15),false); assert.equal(env.isBeforeEmploymentDate(2026,10,16),true); assert.equal(env.getEmploymentDateKey(Date.parse('2026-10-14T16:00:00Z')),20261015); assert.equal(env.getEmploymentDateKey('2026-10-14T16:00:00Z'),20261015); assert.throws(()=> env.getEmploymentDateKey('2026-02-30'),/invalid/); assert.throws(()=> env.getEmploymentBounds({employmentStartDate:'2026-10-16',employmentEndDate:'2026-10-15'}),/invalid/); env.state.billableData = {0:{16:8}}; assert.throws(()=> env.assertEmploymentInputs(),/20261016/,'范围外草稿不静默过滤'); env.state.cellStatusMap = {billable_0_16:'approved'}; env.assertEmploymentInputs(); env.state.billableData = {}; env.assertPersonnelSnapshot({...env.state.userInfo}); assert.throws(()=> env.assertPersonnelSnapshot({...env.state.userInfo,isOverseasStaff:false}),/reload/); assert.throws(()=> env.assertPersonnelSnapshot({...env.state.userInfo,employmentEndDate:'2026-10-14'}),/reload/); const billable = {}; env.applyDefaultWorkdayHours(2026,10,env.state.holidayMap,[{code:'P'}],billable,{}, {},false,{}); assert.equal(billable[0][1],8,'海外节假日中的自然工作日默认8小时'); assert.equal(billable[0][10],undefined,'海外周末补班不默认8小时'); assert.equal(billable[0][16],undefined,'离职后不默认工时'); env.state.userInfo = {isOverseasStaff:true}; env.state.billableData = {0:{1:8,10:8}}; assert.equal(env.validateMonthlyOvertime(),true); for (const hours of [36,36.5]) { env.state.billableData = {0:{3:hours/2,4:hours/2}}; assert.equal(env.validateMonthlyOvertime(),hours===36,'保留36小时边界'); } env.state.isExternalStaff = true; assert.equal(env.validateMonthlyOvertime(),true,'保留外部员工原豁免'); } console.log('PASS 四入口海外自然周、国内回退、任职闭区间、上海时区、默认值、草稿保护、档案变更及36小时边界'); } main().catch(error => {console.error(error);process.exitCode=1;});