跨端: feat(P6): 日历纯逻辑 model/Calendar.ts + 6 条可跑判据(第 1 步的逻辑那一半,不需要设备)
This commit is contained in:
85
client/electron/test/harmony-calendar.test.mjs
Normal file
85
client/electron/test/harmony-calendar.test.mjs
Normal file
@ -0,0 +1,85 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
/*
|
||||
★ 日历的纯逻辑(P6 第 1 步的逻辑那一半)—— **不需要设备**,所以它不该进静态欠账。
|
||||
|
||||
日历最容易悄悄错的是算术:闰年、月首落在星期几、网格行数、跨年翻月、"今天"标错格。
|
||||
这些在界面上表现为"看起来正常但日子错位",属于最难靠肉眼发现的一类,
|
||||
所以先把它放在可跑判据下面(与 `Wallpaper.ts` 同一个模式:纯逻辑 + node 直接跑)。
|
||||
*/
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ETS = join(HERE, '..', '..', '..', 'client', 'harmony', 'entry', 'src', 'main', 'ets');
|
||||
const C = await import(pathToFileURL(join(ETS, 'model', 'Calendar.ts')).href);
|
||||
|
||||
test('闰年与月长:整百年只有能被 400 整除才是闰年', () => {
|
||||
assert.equal(C.daysInMonth(2024, 2), 29, '2024 是闰年');
|
||||
assert.equal(C.daysInMonth(2026, 2), 28, '2026 不是');
|
||||
assert.equal(C.daysInMonth(2000, 2), 29, '2000 能被 400 整除 ⇒ 闰年');
|
||||
assert.equal(C.daysInMonth(2100, 2), 28, '2100 能被 100 但不能被 400 整除 ⇒ 平年(这条最容易写错)');
|
||||
assert.deepEqual([1, 3, 5, 7, 8, 10, 12].map(m => C.daysInMonth(2026, m)), [31, 31, 31, 31, 31, 31, 31]);
|
||||
assert.deepEqual([4, 6, 9, 11].map(m => C.daysInMonth(2026, m)), [30, 30, 30, 30]);
|
||||
});
|
||||
|
||||
test('月首星期与"周一起始/周日起始"的唯一分叉点', () => {
|
||||
assert.equal(C.firstWeekday(2026, 9), 2, '2026-09-01 是周二(0=周日)');
|
||||
assert.equal(C.leadingBlanks(2026, 9, 1), 1, '周一起始:周二前留 1 格');
|
||||
assert.equal(C.leadingBlanks(2026, 9, 0), 2, '周日起始:留 2 格');
|
||||
// 同一份代码在不同本地时区必须给同一天(一律 UTC 构造/取星期)
|
||||
assert.equal(C.firstWeekday(2026, 1, ), new Date(Date.UTC(2026, 0, 1)).getUTCDay());
|
||||
});
|
||||
|
||||
test('网格形状:每行 7 格、行数 4~6、本月日子各出现恰好一次', () => {
|
||||
for (const [y, m, len] of [[2026, 2, 28], [2024, 2, 29], [2026, 9, 30], [2026, 8, 31]]) {
|
||||
const rows = C.monthGrid(y, m, 1);
|
||||
assert.ok(rows.length >= 4 && rows.length <= 6, `${y}-${m} 行数应在 4~6(实际 ${rows.length})`);
|
||||
for (const r of rows) assert.equal(r.length, 7, '每行必须 7 格(否则界面会错位)');
|
||||
const days = rows.flat().filter(c => c.inMonth).map(c => c.day);
|
||||
assert.equal(days.length, len, `${y}-${m} 本月天数`);
|
||||
assert.deepEqual(days, [...Array(len).keys()].map(i => i + 1), '本月日子必须连续且各一次');
|
||||
// 首格的位置必须等于 leadingBlanks(否则整月错位一格)
|
||||
const first = rows.flat().findIndex(c => c.inMonth);
|
||||
assert.equal(first, C.leadingBlanks(y, m, 1), `${y}-${m} 首格位置`);
|
||||
}
|
||||
});
|
||||
|
||||
test('格子的 iso 键唯一、且与年月日一致(键错了会让"今天"标错格)', () => {
|
||||
const cells = C.monthGrid(2026, 9, 1).flat().filter(c => c.inMonth);
|
||||
const keys = cells.map(c => c.iso);
|
||||
assert.equal(new Set(keys).size, keys.length, 'iso 必须唯一');
|
||||
assert.equal(keys[0], '2026-09-01');
|
||||
assert.equal(keys[keys.length - 1], '2026-09-30');
|
||||
assert.ok(keys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k)), '补零:2026-09-01 而不是 2026-9-1');
|
||||
});
|
||||
|
||||
test('翻月:跨年两个方向都对(这是"翻到 13 月"的经典错法)', () => {
|
||||
assert.deepEqual(C.addMonths(2026, 12, 1), [2027, 1]);
|
||||
assert.deepEqual(C.addMonths(2026, 1, -1), [2025, 12]);
|
||||
assert.deepEqual(C.addMonths(2026, 6, 0), [2026, 6]);
|
||||
assert.deepEqual(C.addMonths(2026, 1, -13), [2024, 12], '一次翻一年零一个月也要对');
|
||||
for (const [y, m] of [[2026, 12], [2026, 1], [2100, 12]]) {
|
||||
for (const d of [-25, -1, 1, 25]) {
|
||||
const [ny, nm] = C.addMonths(y, m, d);
|
||||
assert.ok(nm >= 1 && nm <= 12, `翻月后月份必须落在 1~12(得到 ${nm})`);
|
||||
// 我第一版这里写的是"年份落在 2024~2126"—— **那是我拍的范围,不是性质**:
|
||||
// 2026-01 往回 25 个月 = 2023-12 完全正确,却把我的断言撞红了(判据错、代码对)。
|
||||
// 换成**往返性质**:翻过去再翻回来必须回到原点(对任意 delta 都成立,无需拍范围)。
|
||||
assert.deepEqual(C.addMonths(ny, nm, -d), [y, m], `翻 ${d} 个月再翻回来必须回到 ${y}-${m}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('"今天":只有传入的那一天被标出来,且标在本月格子内', () => {
|
||||
const rows = C.monthGrid(2026, 9, 1, '2026-09-14');
|
||||
const today = rows.flat().filter(c => c.isToday);
|
||||
assert.equal(today.length, 1, '必须恰好一格是今天');
|
||||
assert.equal(today[0].iso, '2026-09-14');
|
||||
assert.ok(today[0].inMonth, '今天必须落在本月格里');
|
||||
// 不传 today:一格都不许标(否则会标到"服务器时间"或"上次渲染残留"上)
|
||||
assert.equal(C.monthGrid(2026, 9, 1).flat().filter(c => c.isToday).length, 0, '不传 today 就不该有今天');
|
||||
// today 不在本月:不许标在别的月,也不许标错格
|
||||
assert.equal(C.monthGrid(2026, 9, 1, '2026-10-01').flat().filter(c => c.isToday).length, 0);
|
||||
});
|
||||
@ -77,6 +77,7 @@ const SUITE = [
|
||||
['test/appearance-defaults.test.mjs', [], 4],
|
||||
['test/build-stamp.test.mjs', [], 7],
|
||||
['test/packaging.test.mjs', [], 5],
|
||||
['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6],
|
||||
['test/debt-visibility.test.mjs', [], 1],
|
||||
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2],
|
||||
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口
|
||||
@ -547,7 +548,8 @@ try {
|
||||
: debtRegStatic === measuredStatic
|
||||
? `static-only=${measuredStatic}==登记 ✓`
|
||||
: `static-only=${measuredStatic}≠登记${debtRegStatic} ✗`;
|
||||
debtsLine = ` debts=${total} (${staticNote} / other=${other} by go-test)(${items})`;
|
||||
const env = led.debts.filter(d => d.kind === 'env').reduce((n, d) => n + d.count, 0);
|
||||
debtsLine = ` debts=${total} (${staticNote} / other=${other} by go-test / env=${env} 等环境或人)(${items})`;
|
||||
} catch (e) {
|
||||
debtsLine = ` debts=登记读不到(${e.code || e.message})`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user