/* parseDateStrict 的单元测试(node 直接跑,无依赖)。 * 用法:node internal/web/static/js/datetime-input.test.js * 这里只测解析,不碰 DOM —— 解析是整个修复的核心,边界必须钉死。 */ const fs = require('fs'); const path = require('path'); const vm = require('vm'); const src = fs.readFileSync(path.join(__dirname, 'datetime-input.js'), 'utf8'); const sandbox = { window: {} }; sandbox.window.document = undefined; vm.createContext(sandbox); vm.runInContext(src, sandbox); const api = sandbox.window.rondDatetime; let pass = 0, fail = 0; function eq(got, want, label) { const g = JSON.stringify(got), w = JSON.stringify(want); if (g === w) { pass++; console.log(' ok ' + label); } else { fail++; console.log(' FAIL ' + label + '\n got ' + g + '\n want ' + w); } } function parse(s) { const d = api.parseDate(s); if (d === false || d === null || d === undefined) return null; if (isNaN(d.getTime())) return 'INVALID_DATE'; return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + 'T' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); } console.log('=== 必须接受的写法 ==='); eq(parse('2026'), '2026-01-01T00:00', '2026(只给年)'); eq(parse('2026/6/6'), '2026-06-06T00:00', '2026/6/6 斜杠'); eq(parse('2026-6-6'), '2026-06-06T00:00', '2026-6-6 单数字月日'); eq(parse('2026-06-06'), '2026-06-06T00:00', '2026-06-06 完整'); eq(parse('20260606'), '2026-06-06T00:00', '20260606 八位纯数字'); eq(parse('2026-06-06T09:30'),'2026-06-06T09:30', '标准格式'); eq(parse('2026/6/6 09:30'), '2026-06-06T09:30', '斜杠+空格+时间'); eq(parse('2026-06-06 09:30'),'2026-06-06T09:30', '横杠+空格+时间'); eq(parse('2026-6-6 9:30'), '2026-06-06T09:30', '时间也用单数字'); eq(parse('202606'), '2026-06-01T00:00', '202606 六位(年月)'); eq(parse('2026-06'), '2026-06-01T00:00', '2026-06 年月'); eq(parse(' 2026/6/6 '), '2026-06-06T00:00', '前后空白'); eq(parse('2026.6.6'), '2026-06-06T00:00', '点号分隔'); eq(parse('20260606T0930'), '2026-06-06T09:30', '十二位纯数字'); eq(parse('202606060930'), '2026-06-06T09:30', '十二位纯数字无 T'); console.log('\n=== 必须拒绝的脏输入(flatpickr 原版会静默错解) ==='); eq(parse('202606'), '2026-06-01T00:00', '202606 按位取仍是合法年月(歧义但唯一解)'); eq(parse('2026060630'), null, '2026060630 十位:日=30 但月推断不了 → 拒'); eq(parse('2026/13/6'), null, '月 13 非法'); eq(parse('2026/2/30'), null, '2 月 30 日不存在'); eq(parse('2026/4/31'), null, '4 月 31 日不存在'); eq(parse('2026/00/6'), null, '月 0'); eq(parse('2026/6/0'), null, '日 0'); eq(parse('2026666'), null, '2026666 七位:原版解成 2031-06 → 必须拒'); eq(parse('20260606456'), null, '超长脏串'); eq(parse('abc'), null, '非数字'); eq(parse(''), null, '空串'); eq(parse(' '), null, '纯空白'); eq(parse('2026-6-6x'), null, '尾部残留字符'); eq(parse('2026年6月6日'), null, '中文写法(服务端不收)'); eq(parse('99'), null, '两位数(补录也只可能是四位年)'); eq(parse('2026-6-6T25:00'), null, '小时 25 非法'); eq(parse('2026-6-6T09:60'), null, '分钟 60 非法'); eq(parse('2026/6/6/9'), null, '多一段'); console.log('\n=== round-trip:解析结果必须能原样喂回 formatDate ==='); (function () { const cases = ['2026-06-06T09:30', '2026-01-01T00:00', '2026-12-31T23:55']; let bad = []; for (const c of cases) { const d = api.parseDate(c); if (!d) { bad.push(c + ' 解析失败'); continue; } const out = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + 'T' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0'); if (out !== c) bad.push(c + ' → ' + out); } eq(bad, [], 'round-trip 一致'); })(); console.log('\n=== 汇总 ==='); console.log(' pass=' + pass + ' fail=' + fail); process.exit(fail === 0 ? 0 : 1);