跨端: 鸿蒙端功能同步第一步——日历(只读月视图)上架,入口进底部导航
用户:「要给鸿蒙端做功能同步」。按 API 面盘点(WebUI 62 个 API 函数 vs 鸿蒙 38 个), 最大的用户面缺口是**日历**:纯逻辑(model/Calendar.ts)与判据早就在,一直没页面。 新增: - api/CalendarApi.ets:GET /calendar/events?from=&to=(与 WebUI 同参;区间按**网格**取, 不是月首月末 —— 首尾格子会显示邻月,只查当月会让那些格子永远空着) - pages/CalendarPage.ets:月网格(翻月/回今天)、点某天看当天日程、事件点、今天/选中两态、 加载失败说出来。**没做**:写侧(增删改)、农历重复、.ics、滑动翻页 —— 逐条写在文件头 - model/Calendar.ts:补 localIsoOf / hhmmAtOffset / deviceOffsetMinutes(偏移是入参 ⇒ 三时区可真跑) - model/Models.ets:CalendarEvent / CalendarListResponse(字段对齐服务端 JSON) - NavItems:加「日历」,底部成为 通信/日历/联系人 三项(与 WebUI 同序) - MainPage:日历是**常驻 pane**(visibility 控制),首次可见才拉数据;today 走 @Prop @Watch(visible) 在 pane 变可见时重算 ⇒ 结算欠账 calendar-today-recompute (DEBTS 15 笔 → 14 笔,余额里不再计这一笔) 判据:harmony-calendar 新增 6 条(网格/表头同源、事件归日走 localIsoOf、三时区钟点、 today 重算路径、翻月走 addMonths、变异自检);harmony-nav ② 分派与 ④ 让位跟着改成结构性判据 (④ 原来那个 400 字符窗口一加 pane 就红 —— 窗口式判据的又一次现身);harmony-logic 两处 「只剩两个平级页签」跟着改成三项。 真机实测(harmony-emu + hvigorw assembleHap + hdc install + uitest click + dumpLayout): 9 月网格星期对齐(周一起始,2026-09-01 落在「二」列)、事件点恰好在有日程的那 6 天 (11/17/18/24/25/30)、点 09-17 列出当天两条日程且钟点是本地时间(DB 里 02:20Z/08:30Z → 界面 10:20/16:30)。
This commit is contained in:
@ -192,7 +192,9 @@ test('★ 鸿蒙树里不许用 toISOString 取日期键(今天标错格的直
|
||||
* 只是残留发生在**调用侧**。
|
||||
*
|
||||
* 纯逻辑侧能钉的是:同一次输入给同一结果、不同输入各自独立、**不许跨调用残留**。
|
||||
* 调用侧义务(pane 变可见时重算)登记在 docs/DEBTS.json 的 `calendar-today-recompute`。
|
||||
* 调用侧义务(pane 变可见时重算)原登记在 docs/DEBTS.json 的 `calendar-today-recompute`,
|
||||
* **2026-09-14 已结算**:日历页落地时用 `@Prop @Watch('onVisibleChanged') visible` 实现,
|
||||
* 判据见本文件末尾的「★ today 在 pane **变可见时**重算」。
|
||||
*/
|
||||
test('★ today 不许跨调用残留:连续两次不同输入各自独立,且推进一天后值必须变', () => {
|
||||
const mark = g => g.flat().filter(c => c.isToday).map(c => c.iso);
|
||||
@ -210,3 +212,84 @@ test('★ today 不许跨调用残留:连续两次不同输入各自独立,
|
||||
assert.equal(mark(C.monthGrid(2026, 9, 1)).length, 0,
|
||||
'不传就不许有今天(不许内部兜底取系统时间 —— 那正是"取一次"的写法)');
|
||||
});
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
* P6 第 1 步的**页面那一半**(2026-09-14:日历入口上架)
|
||||
*
|
||||
* 上面那些钉的是"算术对不对";从这里开始钉"页面有没有如实消费它"。
|
||||
* 两者分工明确:算术错 → 日子错位;消费错 → 数据对但画错、或永远画不出来。
|
||||
*/
|
||||
|
||||
test('页面:网格与表头都走 Calendar.ts 的同一套入参(WEEK_START=1,与 WebUI 同值)', () => {
|
||||
const page = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
// 网格:行来自 monthGrid,且 weekStart 传的是常量(不是字面量 0/1 —— 那会与表头分叉)
|
||||
assert.match(page, /monthGrid\(this\.year, this\.month, WEEK_START, this\.todayIso\)/,
|
||||
'网格必须由 monthGrid 生成,并把 WEEK_START 与 todayIso 一起传进去');
|
||||
assert.match(page, /weekdayLabels\(WEEK_START\)/,
|
||||
'表头顺序必须来自 weekdayLabels(WEEK_START) —— 硬编码成 日/一/二… 就是第二个分叉点(表头整体错一列)');
|
||||
const decl = /const WEEK_START: number = (\d)/.exec(page);
|
||||
assert.ok(decl, 'WEEK_START 要是常量,判据要能读到它');
|
||||
assert.equal(decl[1], '1', '周起始必须是周一(1)—— 与 WebUI lib/calendar.ts 的 startOfWeek 同值,两端不同值会让同一天落在不同列');
|
||||
});
|
||||
|
||||
test('页面:事件按 localIsoOf 归日,且不自己切时间戳', () => {
|
||||
const page = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
assert.match(page, /localIsoOf\(/, '事件归日必须走 localIsoOf');
|
||||
assert.ok(!/\.slice\(0, 10\)/.test(page),
|
||||
'页面里不许切时间戳前 10 位取日期(那是 UTC 日期 ⇒ UTC+8 的凌晨会把事件画到前一天)');
|
||||
// 会话/列表侧的日期格式化也不许自己切(同一族:只有 localIsoOf/hhmmAtOffset 两个出口)
|
||||
assert.match(page, /hhmmAtOffset\(/, '钟点显示走 hhmmAtOffset(偏移是入参,可判据)');
|
||||
});
|
||||
|
||||
test('★ localIsoOf / hhmmAtOffset:偏移是入参,所以三个时区都能真跑', () => {
|
||||
// UTC+8 的 09-15 00:30 在 UTC 里是 09-14T16:30Z —— 切字符串会给 09-14(错一天)
|
||||
assert.equal(C.localIsoOf('2026-09-14T16:30:00Z'), C.isoAtOffset(new Date('2026-09-14T16:30:00Z'), C.deviceOffsetMinutes(new Date('2026-09-14T16:30:00Z'))),
|
||||
'localIsoOf 必须与 isoAtOffset(设备偏移) 同口径');
|
||||
assert.equal(C.hhmmAtOffset('2026-09-14T16:30:00Z', 480), '00:30', 'UTC+8 的钟点');
|
||||
assert.equal(C.hhmmAtOffset('2026-09-14T16:30:00Z', -480), '08:30', 'UTC-8 的钟点');
|
||||
assert.equal(C.hhmmAtOffset('2026-09-14T16:30:00Z', 0), '16:30', 'UTC 的钟点');
|
||||
// 坏值不许把整个月画崩
|
||||
assert.equal(C.localIsoOf('not-a-time'), '');
|
||||
assert.equal(C.hhmmAtOffset('not-a-time', 480), '');
|
||||
});
|
||||
|
||||
test('★ today 在 pane **变可见时**重算(DEBTS 的 calendar-today-recompute)', () => {
|
||||
/*
|
||||
* 这条欠账的到期前提就是"日历页面骨架写完后立刻",现在到期了 —— 于是要么实现、要么改前提。
|
||||
* 实现方式:`@Prop @Watch('onVisibleChanged') visible` + aboutToAppear 各自重算一次。
|
||||
*
|
||||
* 为什么两条都要:
|
||||
* · 重新挂载(切页签)→ aboutToAppear 重算 ✓;
|
||||
* · 常驻 pane 一直开着跨过午夜 → **只有 @Watch(visible)** 能救(卸载重挂不会发生)。
|
||||
* 判据盯的是"重算路径存在且绑定在可见性上",不是"今天恰好是几号"(那个由纯逻辑判据管)。
|
||||
*/
|
||||
const raw = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
assert.match(raw, /@Prop @Watch\('onVisibleChanged'\) visible: boolean/,
|
||||
'visible 必须挂 @Watch —— 否则常驻 pane 跨午夜后 today 停在昨天');
|
||||
assert.match(raw, /onVisibleChanged\([^)]*\): void \{[\s\S]{0,200}?refreshToday\(\)/,
|
||||
'onVisibleChanged 里必须重算 today');
|
||||
assert.match(raw, /refreshToday\(\)[\s\S]{0,60}isoOfLocal\(new Date\(\)\)/,
|
||||
'today 只能由 isoOfLocal(new Date()) 求(toISOString 是 UTC 口径,清晨会给出昨天)');
|
||||
// 首次可见才拉数据:常驻挂载 ≠ 常拉(隐藏着也发请求是给用户白花的流量/电)
|
||||
assert.match(raw, /if \(!this\.loadedOnce\)/,
|
||||
'首次可见才拉数据 —— 常驻挂载不等于启动就拉');
|
||||
});
|
||||
|
||||
test('页面:翻月走 addMonths(跨年两个方向已被纯逻辑钉过),不自己算 month+1', () => {
|
||||
const page = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
assert.match(page, /addMonths\(this\.year, this\.month, delta\)/, '翻月必须复用 addMonths');
|
||||
assert.ok(!/this\.month \+ 1|this\.month - 1/.test(page),
|
||||
'页面里不许自己算 month±1 —— "翻到 13 月"就是那样来的(纯逻辑判据已覆盖 addMonths,别在页面重写一遍)');
|
||||
});
|
||||
|
||||
test('★ 判据自检:把错误写法喂进来必须判红', () => {
|
||||
// ① 自己切时间戳取日期键(UTC 口径 ⇒ 错一天)
|
||||
assert.ok(/\.slice\(0, 10\)/.test("localIsoOf(e.event_time.slice(0, 10))"), '切时间戳能被"不许切"这条命中');
|
||||
// ② 表头硬编码(与网格分叉)
|
||||
assert.ok(!/weekdayLabels/.test("['日','一','二','三','四','五','六']"), '硬编码表头能被"必须来自 weekdayLabels"命中');
|
||||
// ③ month±1 自算
|
||||
assert.ok(/this\.month \+ 1/.test('const m = this.month + 1;'), '自算 month+1 能被命中');
|
||||
// ④ 没有 @Watch(today 跨午夜停在昨天)
|
||||
assert.ok(!/@Prop @Watch\('onVisibleChanged'\)/.test('@Prop visible: boolean = true;'), '缺 @Watch 能被命中');
|
||||
});
|
||||
|
||||
@ -219,7 +219,7 @@ test('页面把折叠逻辑真正接上了(不是"逻辑写好了没人用")
|
||||
|
||||
// ───────────────────────── 撤掉平级「会话」tab(P2a 收尾) ─────────────────────────
|
||||
|
||||
test('底部只剩 通信 / 联系人 两个平级页签,「会话」不再是入口', () => {
|
||||
test('底部是 通信 / 日历 / 联系人 三个平级页签,「会话」不再是入口', () => {
|
||||
/*
|
||||
* pi 的判断:WebUI 里「会话」从来不是一个入口,它是**两处已有视图**(联系人页的卡片视图
|
||||
* + 收件箱的会话折叠)。鸿蒙原来把它单列成 tab,等于把"卡片视图"放错了位置。
|
||||
@ -238,7 +238,9 @@ test('底部只剩 通信 / 联系人 两个平级页签,「会话」不再是
|
||||
assert.ok(navLabels > 0, '底栏要由 NAV_ITEMS 驱动');
|
||||
const navSource = code(join(HARMONY_ETS, 'model/NavItems.ts'));
|
||||
const labels = [...navSource.matchAll(/label:\s*'([^']+)'/g)].map(m => m[1]);
|
||||
assert.deepEqual(labels, ['通信', '联系人'], `平级项应只剩两个,实际:${labels.join('、')}`);
|
||||
// 2026-09-14:日历页(P6 第 1 步)落地后入口上架 ⇒ 三项。撤掉「会话」这条判断本身没变:
|
||||
// 它不是第三个地方,而是"同一批数据的另一种看法"(收件箱折叠 + 联系人卡片视图)。
|
||||
assert.deepEqual(labels, ['通信', '日历', '联系人'], `平级项应为 通信/日历/联系人,实际:${labels.join('、')}`);
|
||||
assert.ok(!/struct\s+SessionsTab/.test(pageCode), 'SessionsTab 已经撤了,不该再留在页面里');
|
||||
assert.ok(!/sessions\(\)/.test(pageCode), '撤了入口就不该再拉 /me/sessions(否则是没人看的请求)');
|
||||
});
|
||||
@ -512,7 +514,8 @@ test('通信页把三栏真的接上了:内部页签 + 徽标 + 悬浮加号 +
|
||||
// P5:底栏标签现在来自 NAV_ITEMS(自绘浮动条),不是 TabBarBuilder 的参数
|
||||
const navSource2 = code(join(HARMONY_ETS, 'model/NavItems.ts'));
|
||||
const tabLabels = [...navSource2.matchAll(/label:\s*'([^']+)'/g)].map(m => m[1]);
|
||||
assert.deepEqual(tabLabels, ['通信', '联系人'], `底部应只剩两项且第一项是通信,实际:${tabLabels.join('、')}`);
|
||||
// 2026-09-14:日历页(P6 第 1 步)做完后入口上架 —— 三项且顺序固定(通信/日历/联系人)
|
||||
assert.deepEqual(tabLabels, ['通信', '日历', '联系人'], `底部应为通信/日历/联系人三项,实际:${tabLabels.join('、')}`);
|
||||
// 通信页现在还要收一个 `bgActive`(背景开着时让出页面底,否则壁纸全被盖住)——
|
||||
// 所以这里钉的是"带参数地渲染通信页",不是光有个名字
|
||||
assert.match(sendCode, /CommPage\(\{ bgActive: this\.bgActive \}\)/, '第一项要渲染通信页(并把背景开关传下去)');
|
||||
|
||||
@ -85,8 +85,12 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配
|
||||
test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人)', () => {
|
||||
/*
|
||||
* 换了条之后最容易出的错是"点了没反应"——点击改了状态,但内容仍写死挂第一个页面。
|
||||
* 所以把 index → 页面的映射钉死:0 → CommPage,否则 → ContactsTab,
|
||||
* 且两边都要收到 `bgActive`(背景开着时让出页面底,否则壁纸被内容盖住)。
|
||||
* 所以把 index → 页面的映射钉死:0 → CommPage、2 → ContactsTab、1 → CalendarPage,
|
||||
* 且三边都要收到 `bgActive`(背景开着时让出页面底,否则壁纸被内容盖住)。
|
||||
*
|
||||
* 日历与另两个不同:它**常驻挂载**(不是 if/else 的一支),用 `visibility` 控制显示 ——
|
||||
* 理由写在 `CalendarPage.ets` 与 build() 的注释里(today 跨午夜、保住"在看哪个月")。
|
||||
* 所以这里断言的是**两件事**:索引→页面的配对,以及日历那一支确实是"常驻 + 可见性开关"。
|
||||
*/
|
||||
const stack = main.slice(main.indexOf('Stack({ alignContent: Alignment.Bottom })'));
|
||||
// 只在**根 build()** 里数分派(页面内部也有 if/else,扫全文会数错)
|
||||
@ -96,13 +100,32 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人
|
||||
const first = braceBody(root, 'if (this.currentIndex === 0)');
|
||||
const second = braceBody(root.slice(root.indexOf('if (this.currentIndex === 0)')), 'else');
|
||||
assert.match(first, /CommPage\(\{ bgActive: this\.bgActive \}\)/, 'index 0 要挂通信页');
|
||||
assert.match(second, /ContactsTab\(\{ bgActive: this\.bgActive \}\)/, 'index 1 要挂联系人页');
|
||||
assert.match(second, /ContactsTab\(\{ bgActive: this\.bgActive \}\)/, 'index 2 要挂联系人页');
|
||||
/*
|
||||
* 分派必须只有这两支,且必须**穷尽** NAV_ITEMS —— 现在只有两项,用 if/else 就够了;
|
||||
* 一旦 NAV_ITEMS 变长,这条会红并提醒改成按清单挂载(否则第 3 项点了没内容)。
|
||||
* 联系人那一支必须**显式绑在 index 2**。注意条件在 `else if (...)` 里,
|
||||
* 不在 `else` 的**花括号正文**里 —— braceBody 只取正文,所以这条要看 root 原文。
|
||||
* 写成"否则就挂联系人"的后果:以后再加一项,新索引会被它静默接住(点了显示联系人)。
|
||||
*/
|
||||
assert.equal(N.NAV_ITEMS.length, 2,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项,而内容分派只有 if/else 两支 —— 加了项必须同时加分派`);
|
||||
assert.match(root, /else if \(this\.currentIndex === 2\)/, '联系人必须显式绑在 index 2,不许用 else 兜底');
|
||||
/*
|
||||
* 日历(index 1):**常驻挂载 + visible 由索引驱动 + Visibility 开关**,三样缺一不可。
|
||||
* · `visible: this.currentIndex === 1` 是 `@Watch` 的触发源 —— 不带它,today 就不会在
|
||||
* pane 变可见时重算(DEBTS 的 calendar-today-recompute);
|
||||
* · `visibility(...=== 1 ? Visible : None)` 是显示开关;常驻 + 不隐藏 = 三个 pane 叠在一起。
|
||||
*/
|
||||
assert.match(root, /CalendarPage\(\{ bgActive: this\.bgActive, visible: this\.currentIndex === 1 \}\)/,
|
||||
'index 1 要挂日历页,并把"是否可见"传下去(它是 today 重算的触发源)');
|
||||
assert.match(root, /\.visibility\(this\.currentIndex === 1 \? Visibility\.Visible : Visibility\.None\)/,
|
||||
'常驻挂载就要用 visibility 控制显示');
|
||||
assert.ok(!/if \(this\.currentIndex === 1\)/.test(root),
|
||||
'日历不该写成 if/else 的一支:它是常驻 pane(卸载重挂会丢掉"在看哪个月",且 today 只能靠挂载重算)');
|
||||
/*
|
||||
* 分派必须**穷尽** NAV_ITEMS。2026-09-14 日历入口上架时这条**如约变红**
|
||||
* (当时是 `length === 2` + 两支 if/else)—— 加项必须同时加分派,否则第 3 项点了没内容。
|
||||
* 现在的形状与"三项"绑定:通信(0) / 日历(1) / 联系人(2),日历那一支**常驻挂载**。
|
||||
*/
|
||||
assert.equal(N.NAV_ITEMS.length, 3,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项,与分派(通信/日历/联系人)不匹配 —— 加了项必须同时加分派`);
|
||||
// 内容挂在浮动条**下面**(先内容后条),否则条会被内容盖住、点不到
|
||||
const navAt = stack.indexOf('this.NavBar()');
|
||||
const contentAt = stack.indexOf('CommPage({ bgActive: this.bgActive })');
|
||||
@ -180,13 +203,31 @@ test('④ 悬浮 + 让位:自绘浮动层(留白/圆角/系统材质),
|
||||
assert.ok(N.NAV_CONTENT_RESERVE >= N.NAV_BAR_HEIGHT + N.NAV_BAR_BOTTOM,
|
||||
`内容让位(${N.NAV_CONTENT_RESERVE})必须 ≥ 条高 + 离底留白(${N.NAV_BAR_HEIGHT + N.NAV_BAR_BOTTOM})`);
|
||||
assert.match(main, /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/, '内容底部要让出这段高度');
|
||||
// 让位要生效在**内容**上,不是条上(条自己 padding 不解决遮挡)
|
||||
const contentPad = main.slice(main.indexOf('Column() {\n if (this.currentIndex === 0)'));
|
||||
assert.match(contentPad.slice(0, 400), /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/,
|
||||
'让位要加在挂载内容的那层上');
|
||||
/*
|
||||
* 让位要生效在**内容**上,不是条上(条自己 padding 不解决遮挡)。
|
||||
*
|
||||
* 这里原来是 `main.slice(定位).slice(0, 400)` 的**窗口式**断言 —— 2026-09-14 加日历
|
||||
* 那一支时它红了:内容层里多了一个 pane,`.padding(...)` 落到了 400 字符之外。
|
||||
* 判的是"同一件事",红的原因却只是"变长了",正是本仓库反复记的窗口式判据。
|
||||
* 改成**按花括号配对取正文**:内容层那个 Column 的 `{...}` 里必须出现让位。
|
||||
*/
|
||||
const contentOpen = main.indexOf('Column() {\n if (this.currentIndex === 0)');
|
||||
assert.ok(contentOpen > 0, '要能找到挂载内容的那层 Column');
|
||||
// `.padding(...)` 是**链在 `}` 之后**的,所以不能只看 {} 里面 —— 从配对结束处往后取到 NavBar 之前
|
||||
const braceAt = main.indexOf('{', contentOpen);
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
for (let i = braceAt; i < main.length; i++) {
|
||||
if (main[i] === '{') depth++;
|
||||
else if (main[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
assert.ok(end > 0, '内容层的花括号要闭合');
|
||||
const chain = main.slice(end, main.indexOf('this.NavBar()', end));
|
||||
assert.match(chain, /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/,
|
||||
'让位要加在挂载内容的那层上(链在那个 Column 的 } 之后)');
|
||||
});
|
||||
|
||||
test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且平级项仍是两项', () => {
|
||||
test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且平级项是 通信/日历/联系人', () => {
|
||||
/*
|
||||
* 反面:如果只是"加了自绘条但仍留着系统 bar",界面上会出现两条导航(且系统那条
|
||||
* 依然贴边、依然占位),这正是这一期要消除的东西。所以断言根导航里**没有**
|
||||
@ -194,7 +235,7 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
* 注意:**通信页内部**的三栏仍然用系统 `Tabs`(那是页面内部页签,不是根导航)——
|
||||
* 所以这条只在"根组件"的范围内断言,别扫全文。
|
||||
*/
|
||||
const root = main.slice(main.indexOf('build() {\n /*\n * 底部两项'));
|
||||
const root = main.slice(main.indexOf('build() {\n /*\n * 底部三项'));
|
||||
assert.ok(root.length > 300, '要能取到根 build() 的正文');
|
||||
const navCode = stripComments(root);
|
||||
assert.ok(!/\bTabs\(/.test(navCode), '根导航里不该再有系统 Tabs');
|
||||
@ -203,8 +244,8 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
// 反面之二:也不许把旧 builder 留着不用(留着就是死代码,下一个人会以为它还在生效)
|
||||
assert.ok(!/TabBarBuilder/.test(stripComments(main)), 'TabBarBuilder 已被 NavBar 取代,不该留在文件里');
|
||||
|
||||
// 平级项仍是两项且顺序不变(信息架构这一期不动)
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => i.label), ['通信', '联系人'], '平级项与顺序');
|
||||
// 平级项:通信/日历/联系人(日历是 P6 第 1 步做完后上架的,顺序与 WebUI 一致)
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => i.label), ['通信', '日历', '联系人'], '平级项与顺序');
|
||||
assert.equal(new Set(N.NAV_ITEMS.map(i => i.key)).size, N.NAV_ITEMS.length, '键要唯一(ForEach 的 key)');
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => N.navKeyAt(N.NAV_ITEMS.indexOf(i))), N.NAV_ITEMS.map(i => i.key),
|
||||
'navKeyAt 与清单一致');
|
||||
@ -212,6 +253,7 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
assert.equal(N.normalizeNavIndex(-1), 0);
|
||||
assert.equal(N.normalizeNavIndex(99), 0);
|
||||
assert.equal(N.normalizeNavIndex(1), 1);
|
||||
assert.equal(N.normalizeNavIndex(2), 2, '第三项(联系人)也必须归一到自己');
|
||||
assert.throws(() => N.navKeyAt(1.5) === undefined, undefined, '非整数下标不该悄悄生效');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user