Files
work-hours-tracker/test/lib/app-harness.js
T
OpenClaw Agent 2329955e32 baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。

此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。

当前状态(提交前已实测):
- index.html 内联薪酬算法已删除,改调 WHTPayroll
- shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用
- 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装

未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、
运行时 sqlite 数据与日志、时间戳备份、.trash。
2026-09-05 14:35:00 +08:00

123 lines
4.8 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.
// 在 Node 里跑 index.html 内联的那坨 setup(),拿到它返回的全部 computed。
//
// 这个文件是**唯一**焊在 index.html 结构上的东西:它靠正则抠出内联 <script>,
// 一旦前端拆成 SFC 就会立刻失效。这是刻意的——它的职责只剩「重构前后行为一致」对照,
// 前端拆完那天连同 app-parity 测试一起删掉,而 shared/payroll.js 上的那套断言不受影响。
//
// 与 verify_payroll.js 里那份内联桩件的区别:
// 1. Date 可冻结 → payDayAPassed / isCurrentMonth / cellClass 这些吃「今天」的输出变成确定值,
// 否则基准 JSON 每天跑出来都不一样,根本没法当基准。
// 2. 会把 shared/payroll.js 先注入沙箱,让重构后的 index.html 找得到 WHTPayroll。
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = path.join(__dirname, '..', '..');
const HTML = path.join(ROOT, 'index.html');
const PAYROLL = path.join(ROOT, 'shared', 'payroll.js');
/** 冻结「现在」:无参 new Date() 与 Date.now() 都回到固定时刻,带参构造照常。 */
function frozenDate(iso) {
const FIXED = new Date(iso).getTime();
return class FrozenDate extends Date {
constructor(...args) {
if (args.length === 0) super(FIXED);
else super(...args);
}
static now() { return FIXED; }
};
}
/** 极简 Vue 桩件:测试只需要能读写与求值,不需要真响应式。 */
function makeVue() {
const mounted = [];
const Vue = {
ref: (v) => ({ value: v }),
reactive: (o) => o,
computed: (fn) => ({ get value() { return fn(); } }),
watch: () => {},
onMounted: (fn) => mounted.push(fn),
createApp(opts) {
Vue._captured = opts.setup();
return { mount: () => Vue._captured };
}
};
return { Vue, mounted };
}
function makeStore(seed) {
const data = { ...seed };
return {
getItem: (k) => (k in data ? data[k] : null),
setItem: (k, v) => { data[k] = v; },
removeItem: (k) => { delete data[k]; },
_raw: () => data,
_records: () => JSON.parse(data.work_records || '{}'),
_settings: () => (data.work_settings ? JSON.parse(data.work_settings) : null),
_adjust: () => (data.work_month_adjust ? JSON.parse(data.work_month_adjust) : null)
};
}
/**
* 载入应用。
* @param {object} o
* @param {object} o.records 打卡记录,key 'YYYY-MM-DD'
* @param {number} o.year 当前查看年
* @param {number} o.month 当前查看月(0–11)
* @param {object} [o.settings] 覆盖设置,null/undefined 表示用默认
* @param {object} [o.adjust] 月度奖惩终稿,key 'YYYY-MM'
* @param {string} [o.now] 冻结的「现在」(ISO)
*/
function loadApp({ records = {}, year, month, settings = null, adjust = null, now = null }) {
const seed = { work_records: JSON.stringify(records) };
if (settings) seed.work_settings = JSON.stringify(settings);
if (adjust) seed.work_month_adjust = JSON.stringify(adjust);
const localStorage = makeStore(seed);
const { Vue, mounted } = makeVue();
const DateImpl = now ? frozenDate(now) : Date;
const sandbox = {
Vue, localStorage, console,
Date: DateImpl, JSON, Math, Number, RegExp, Object, Array, String, Boolean, Error,
navigator: {},
document: {
createElement: () => ({ style: {}, select() {}, click() {} }),
body: { appendChild() {}, removeChild() {} },
getElementById: () => null,
addEventListener() {}
},
confirm: () => true,
setTimeout: (fn) => { if (typeof fn === 'function') fn(); return 0; },
Blob: function () {},
URL: { createObjectURL: () => 'blob:x', revokeObjectURL() {} },
// 刻意不提供 window.WHTSync:测试跑「未登录/纯离线」路径,
// 顺带验证同步缺席时算钱逻辑完全不受影响。
location: { origin: 'http://test' },
fetch: () => Promise.reject(new Error('offline in test'))
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
// 先注入纯模块:重构后的 index.html 依赖它。重构前注入也无害(没人引用)。
if (fs.existsSync(PAYROLL)) {
vm.runInContext(fs.readFileSync(PAYROLL, 'utf8'), sandbox, { filename: 'shared/payroll.js' });
}
const src = fs.readFileSync(HTML, 'utf8');
const m = src.match(/<script>\s*([\s\S]*?)\s*<\/script>\s*<\/body>/);
if (!m) throw new Error('index.html 里没找到应用内联 <script>,harness 需要更新');
vm.runInContext(m[1], sandbox, { filename: 'index.html#app' });
mounted.forEach((fn) => fn()); // 触发 records / settings / adjust 载入
const app = Vue._captured;
app.currentYear.value = year;
app.currentMonth.value = month;
return { app, localStorage, sandbox };
}
module.exports = { loadApp, frozenDate, ROOT };