跨端: feat(P6): 表头与网格共用同一个 startOfWeek;"今天"的调用侧入可跑判据;禁用 toISOString 取日期键
pi 2026-09-14 两条,都赶在页面骨架之前定下来。 1. **唯一分叉点必须同时喂两处**:整月网格有两个地方依赖"周从哪天开始" —— 空格数(`leadingBlanks`)**与表头第一格**。表头若在页面里硬编码,就是**第二个分叉**: 格子全对、**表头整体错一列**,而原有 6 条判据一条都不会红(它们只看网格)。 新增 `weekdayLabels(startOfWeek)`(顺序只从这一个参数出)+ **交叉核对判据**: 把"1 号落在第几列"与"那一列的表头字"对上(6 个月份 × 2 种起始)。 2. **"今天"的调用侧**:`today` 入参化让纯逻辑侧干净了,代价是**唯一还能错的地方搬到了调用侧** —— 而它正好是纯逻辑判据够不着的。`toISOString()` 是 UTC 口径:UTC+8 的清晨会给**昨天**, "今天"就标到上一格,且在本机跑 UTC 的环境里**永远测不出来**。 新增 `isoOfLocal(now)`(本地年月日手工补零)+ `isoAtOffset(now, 分钟)`(与前者同源但不依赖进程时区, 好让三种偏移**可以真跑**)。判据钉住:UTC+8 / UTC-7 / UTC 三种偏移的日期、 **两种取法在 UTC+8 清晨必须不同**(把陷阱本身钉死)、以及本机两条取法自洽。 3. 附带一条**未来时**的判据:鸿蒙树里不许出现 `toISOString().slice(0,10)` 取日期键 (登记值 0 ⇒ 页面骨架写错时立刻红,报错写"正确修法 = isoOfLocal"与"最常见的错误修法 = 改期望值")。 它第一次跑就抓到了 `model/Calendar.ts` 里**解释这个陷阱的注释** —— 所以改扫 `code()` (去注释后的代码):规则管代码,注释是文档。
This commit is contained in:
@ -2,6 +2,8 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { code } from './lib/read.mjs';
|
||||
|
||||
/*
|
||||
★ 日历的纯逻辑(P6 第 1 步的逻辑那一半)—— **不需要设备**,所以它不该进静态欠账。
|
||||
@ -83,3 +85,83 @@ test('"今天":只有传入的那一天被标出来,且标在本月格子内
|
||||
// today 不在本月:不许标在别的月,也不许标错格
|
||||
assert.equal(C.monthGrid(2026, 9, 1, '2026-10-01').flat().filter(c => c.isToday).length, 0);
|
||||
});
|
||||
|
||||
/*
|
||||
* ★ 表头与网格必须**来自同一个 `startOfWeek`**(pi 2026-09-14 §1)。
|
||||
*
|
||||
* 危险形态:页面里把表头硬编码成 `['日','一',…]`(或另算一次)⇒ 格子位置全对、
|
||||
* **表头整体错一列**。上面 6 条判据一条都不会红,因为**它们只看网格**。
|
||||
* 所以这条判据做的是**交叉核对**:把"1 号落在第几列"与"那一列的表头字"对上。
|
||||
*/
|
||||
test('★ 表头顺序与 leadingBlanks 必须来自同一个 startOfWeek(否则表头整体错一列)', () => {
|
||||
for (const ws of [0, 1]) {
|
||||
const labels = C.weekdayLabels(ws);
|
||||
assert.equal(labels.length, 7);
|
||||
assert.equal(new Set(labels).size, 7, '表头七天不许重名');
|
||||
assert.equal(labels[0], ws === 0 ? '日' : '一', `startOfWeek=${ws} 的第一格必须是那一天`);
|
||||
assert.equal(labels[6], ws === 0 ? '六' : '日', '最后一格也要跟着换(只改第一格是半吊子)');
|
||||
}
|
||||
// 交叉核对:若干月份的 1 号落在那列,那一列的表头字必须就是 1 号的星期名
|
||||
for (const ws of [0, 1]) {
|
||||
const labels = C.weekdayLabels(ws);
|
||||
for (const [y, m] of [[2026, 9], [2026, 2], [2024, 2], [2026, 3], [2027, 1], [2026, 11]]) {
|
||||
const col = C.leadingBlanks(y, m, ws);
|
||||
assert.equal(labels[col], C.weekdayNameOf(y, m, 1),
|
||||
`${y}-${m}(startOfWeek=${ws}):1 号在第 ${col} 列,那一列表头写着「${labels[col]}」,` +
|
||||
`而 1 号其实是「${C.weekdayNameOf(y, m, 1)}」—— 表头与网格分叉了`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* ★ "今天"的调用侧:`toISOString()` 是 UTC,UTC+8 早上会把今天标到昨天那一格
|
||||
* (pi 2026-09-14 §2)。这一格纯逻辑判据够不着,所以把取今天的函数也放进可跑范围。
|
||||
*/
|
||||
test('★ 调用侧取今天:按偏移取(UTC+8 / UTC-8 / UTC),并钉住 toISOString 的 UTC 陷阱', () => {
|
||||
// 2026-09-14T16:30Z:UTC+8 已经是 15 日 00:30;UTC-7 还是 14 日 09:30
|
||||
const t = new Date('2026-09-14T16:30:00Z');
|
||||
assert.equal(C.isoAtOffset(t, 480), '2026-09-15', 'UTC+8 该给 15 日');
|
||||
assert.equal(C.isoAtOffset(t, -420), '2026-09-14', 'UTC-7 该给 14 日');
|
||||
assert.equal(C.isoAtOffset(t, 0), '2026-09-14', 'UTC 该给 14 日');
|
||||
|
||||
// 陷阱本身:两种取法在 UTC+8 的清晨**必须不同** —— 这就是"今天标到昨天"那一格
|
||||
const naive = t.toISOString().slice(0, 10);
|
||||
assert.equal(naive, '2026-09-14', '自检:naive 取法给 UTC 日期');
|
||||
assert.notEqual(C.isoAtOffset(t, 480), naive,
|
||||
'UTC+8 的清晨,toISOString 取法会给昨天 —— 页面若用它,"今天"就标到上一格(且本机跑 UTC 时永远测不出)');
|
||||
|
||||
// 同一时刻、两条取法必须自洽:本机偏移那条 == isoAtOffset(本机偏移)
|
||||
const now = new Date();
|
||||
assert.equal(C.isoOfLocal(now), C.isoAtOffset(now, -now.getTimezoneOffset()),
|
||||
'isoOfLocal 与 isoAtOffset(本机偏移) 必须一致(不一致说明其中一条用错了基准)');
|
||||
assert.match(C.isoOfLocal(now), /^\d{4}-\d{2}-\d{2}$/, '补零格式');
|
||||
});
|
||||
|
||||
/*
|
||||
* ★ 调用侧的**唯一入口**:鸿蒙树里不许出现 `toISOString().slice(0,10)` 这种取日期键的写法
|
||||
* (与"结算/日期键必须本地口径"同族)。文件里出现一次即红 —— 那是"今天标错格"的直接来源。
|
||||
* 登记值 0:现在没有(页面骨架还没写),所以这条的红是**未来时**。
|
||||
* 扫的是 (**去掉注释后的代码**)—— 第一次跑这条判据时它抓到了
|
||||
* 里**解释这个陷阱的注释**。规则管的是代码,注释是文档:
|
||||
* 这正是本仓库 code/prose 两套读取入口存在的理由(JSON 那类是数据,走 prose)。
|
||||
*/
|
||||
test('★ 鸿蒙树里不许用 toISOString 取日期键(今天标错格的直接来源)', () => {
|
||||
const hits = [];
|
||||
const scan = dir => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const f = join(dir, e.name);
|
||||
if (e.isDirectory()) { scan(f); continue; }
|
||||
if (!/\.(ets|ts)$/.test(e.name)) continue;
|
||||
code(f).split('\n').forEach((line, i) => {
|
||||
if (/toISOString\s*\(\s*\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(line)) {
|
||||
hits.push(`${f.slice(ETS.length + 1)}:${i + 1} ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
scan(ETS);
|
||||
assert.deepEqual(hits, [],
|
||||
`用 toISOString 取日期键了 —— **停下**:它是 UTC 口径,UTC+8 的清晨会给出昨天,` +
|
||||
`"今天"就标到上一格。**正确修法**:页面侧用 model/Calendar.ts 的 isoOfLocal(new Date())。` +
|
||||
`**最常见的错误修法**:把断言里的期望值改成 UTC 日期(那是把判据废掉)。\n ${hits.join('\n ')}`);
|
||||
});
|
||||
|
||||
@ -77,7 +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/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 9],
|
||||
['test/debt-visibility.test.mjs', [], 1],
|
||||
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2],
|
||||
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口
|
||||
|
||||
Reference in New Issue
Block a user