跨端: 鸿蒙顶栏 —— 兜底改品牌名、一言只留句子、整批轮播(修"轮播不转")

用户三条裁定,逐条落地:

① 兜底文案「暂无待办」→「AgentMail」
   触发条件只是"三个计数为零",而"暂无待办"读起来像"没事可做"——
   是在**替用户下结论**,结论与事实不等价。品牌名不带判断。

② 一言**只显示句子,不显示出处**
   原来 `quote + ' —— ' + source`。出处占近 1/3 宽度、把句子本身
   挤到省略号;且 hitokoto 的出处格式杂(动漫角色/诗词/网名),
   窄带上排起来脏。⇒ 与签名对齐:都是"当前状态的一句话"。
   `topQuoteSource` 状态随之删除(只写不读的死字段)。

③ ★★ 修一个**不报错、不崩溃**的静默 bug:轮播根本不转
   `this.topQuote = content.quotes[0].text` —— 注释还振振有词
   「批次的意义是少请求,不是一次全显示」。那句话本身没错,
   但**只取第一句** ⇒ 本地永远只有 1 句 ⇒
   `startTopbarRotation` 里 `texts.length <= 1` 的守卫直接 `return`。
   实测:连拍 6 张(18 秒)**全是同一句**。

   ★ 更坏的是它先前是**假象**:看着"在转",靠的是兜底占了轮播位
     (一言 ↔ 暂无待办 交替)。等 ① 把兜底改成不占轮播位,真相立刻暴露。
     ⇒ 教训:**"看起来在工作"可能是另一个东西在工作**。
       做 ① 时顺手连拍验证,才撞出这个真 bug。

   修法:整批存进本地状态、本地在批内逐句轮。服务端一批给 10 句
   (每次顺序还随机),这正好落实用户最初那句「app 本地缓存一部分」
   ——缓存的是**一批**,不是一个。

   ★ 顺带:兜底**不占轮播位**(它只做"唯一候选")
     摘要要有真计数(unread/pending/contacts 至少一个 > 0)才占位。
     实测四张连拍得到 `一言 → 兜底 → 兜底 → 一言`,
     等于用户看到的内容里一半是废话。

实测凭据(2in1 模拟器 3120×2080)
  · 连拍 7 张(21s):6 句不同一言,全不带出处
  · 连拍 5 张(15s):4 句不同,无兜底占位
  · 顶栏文案与三键垂直中心差 0.0px(上一提交已校准)

判据(harmony-2in1,共 23 条)
  · 兜底必须存在(退回空串会让整块消失),且**不含判断词**
    ("暂无/没有/无"开头——正则断言,改回「暂无待办」即判红)
  · 兜底不占轮播位:`summaryIsReal` 守卫 + `out.length === 0` 才 push(两半都钉)
  · 一言存**整批**、逐句进候选、不许有单句形态 `topQuote`
  · 出处不许出现在代码里

★ 判据自己踩的坑,记下来免得重犯:
  一言那条我第一版写成反向正则 `/this\.topQuote \+ ' —+ ' \+ this\.topQuoteSource/`
  —— **只匹配单引号**。变异时我把拼接写成双引号 `" —— "`,正则没命中
  ⇒ **假绿**。变异验证当场抓到。改成**正向断言**
  `out.push(this.topQuote);`(必须原样推进、不许在此处拼接):
  正向比反向窄,且不依赖引号风格。

全量:577/577 绿。
This commit is contained in:
2026-09-25 19:09:48 +08:00
parent 9d50352e7e
commit ab1c856ffe
2 changed files with 186 additions and 25 deletions

View File

@ -670,15 +670,97 @@ test('2in1|顶栏摘要"零"时也要有文案(否则整块消失)', () =>
* 实测:那封邮件已读 ⇒ 三项全 0 ⇒ `topbarTexts()` 成空数组 ⇒ 整块**不渲染**,
* 顶栏右上什么都没有。
*
* 而"没有未读"恰恰是**常态**(收件箱清干净了)。那时该说"暂无待办",
* 而不是整块消失让人以为坏了。
* 而"没有未读"恰恰是**常态**(收件箱清干净了)。
*
* 改坏会红:把 `return '暂无待办'` 改回 `return ''`。
* ★★ 文案改过两轮,判据跟着改(用户裁定):
* 第 0 版 空串 → 整块消失(顶栏右侧空着、与三键失衡)
* 第 1 版 「暂无待办」→ 补上了"零也是信息",但这句话**信息量为 0 且会误导**:
* 它读起来像"没事可做",实际只表示"三个计数是 0"。
* 第 2 版 「AgentMail」(品牌名)—— 不带任何判断。
*
* ⇒ 判据钉**两条**:① 兜底必须存在(别退回空串让整块消失);
* ② 兜底文案**不含判断词**("暂无/没有/无"开头都是在替用户下结论)。
*
* 改坏会红:把 `TOPBAR_FALLBACK` 改回 `''`,或改回 `'暂无待办'`。
*/
const MAIN = code(join(ETS, 'pages/MainPage.ets'));
assert.match(MAIN, /暂无待办/,
'摘要为零时要有兜底文案 —— "零"本身也是信息,不该让整块消失');
});
assert.match(MAIN, /const TOPBAR_FALLBACK: string = '[^']+';/,
'兜底文案要存在(退回空串会让整块消失、顶栏右侧空着)');
const fallback = MAIN.match(/const TOPBAR_FALLBACK: string = '([^']+)';/);
assert.ok(fallback, '要能取到兜底文案');
assert.doesNotMatch(fallback[1], /^暂无|^没有|^无/,
'兜底文案不许下判断("暂无X"是在替用户下结论,而触发条件只是计数为零)');
/*
* ③ 兜底**不占轮播位** —— 它只做"唯一候选"。
*
* 实测:四张连拍(3s 一张)得到 `一言 → 兜底 → 兜底 → 一言`,
* 轮播位有一半被废话吃掉(那时一言只有 2 条)。
* ⇒ 摘要有真计数才配占位;兜底只在**三类都空**时才作为唯一候选出现。
*
* 结构上是两半,都要钉住(少一半就退回旧行为):
* a) 摘要入口有 `summaryIsReal` 守卫(假摘要不进数组)
* b) 数组为空时才 push 兜底(不是无条件 push)
*
* 改坏会红:删掉任一半。
*/
assert.match(MAIN, /const summaryIsReal: boolean = this\.navUnread > 0 \|\| this\.navPending > 0 \|\| this\.navContacts > 0;/,
'摘要要有"是否为真计数"的判断');
assert.match(MAIN, /if \(summaryIsReal\) \{\s*\n\s*out\.push\(summary\);\s*\n\s*\}/,
'假摘要(兜底)不许进候选数组 —— 否则它会被轮播到、占掉一半时长');
assert.match(MAIN, /if \(out\.length === 0\) \{\s*\n\s*out\.push\(TOPBAR_FALLBACK\);\s*\n\s*\}/,
'兜底只在数组为空时作为唯一候选进入');
/*
* ④ 一言**只显示句子**,不带出处(用户裁定)。
*
* 原形状 `quote + ' —— ' + source` 的问题:
* 出处占近 1/3 宽度、把句子本身挤到省略号;
* 且 hitokoto 的出处格式杂(动漫角色/诗词/网名),窄带上排起来脏。
* ⇒ 与签名对齐:两者都是"当前状态的一句话",都不带出处。
*
* 判据钉**两半**:① 拼装里不含 `——` + source;
* ② `topQuoteSource` 这个状态整个不存在了(留着就是只写不读的死字段)。
*
* 改坏会红:把拼接改回去,或把 `topQuoteSource` 状态加回来。
*/
/*
* ★ 这里我第一版写漏了,记下来:
* 正则写成 `/this\.topQuote \+ ' —+ ' \+ this\.topQuoteSource/` ——
* **只匹配单引号**。而变异时我把拼接写成双引号 `" —— "`,
* 正则没命中 ⇒ 判据**假绿**(变异验证的意义就在这:它当场抓到了)。
*
* ⇒ 改成**正向断言**:必须存在"把 topQuote 原样推进数组"这一句。
* 正向比反向窄,且不依赖引号风格。
*/
assert.match(MAIN, /@State topQuotes: string\[\] = \[\];/,
'一言要存**整批**(单个字符串装不下一批,见下条)');
assert.match(MAIN, /this\.topQuotes\.push\(q\)|out\.push\(q\);/,
'一言要**逐句**进候选数组');
assert.doesNotMatch(MAIN, /this\.topQuote\b(?!s)/,
'不许再有单句形态的 topQuote(它是"只取第一句"那个 bug 的载体)');
assert.doesNotMatch(MAIN, /this\.topQuoteSource/,
'出处不许出现在代码里(只写不读的死字段也要删)');
/*
* ⑤ **整批都要存下来** —— 只取第一句会让轮播静默失效。
*
* 这是本轮最值钱的一条判据,因为那个 bug **不报错、不崩溃**:
* `this.topQuote = content.quotes[0].text` ⇒ 本地只有 1 句 ⇒
* `startTopbarRotation` 的 `texts.length <= 1` 守卫直接 return ⇒ 不轮播。
* 我连拍 6 张(18s)全是同一句才发现。
*
* ★ 更坏的是它是**假象**:先前看着"在转",是因为兜底文案占了位
* (一言 ↔ 暂无待办 交替);等兜底改成不占轮播位,转的其实是兜底。
* ⇒ 判据必须钉"服务端给的那一批**全都进**本地状态"。
*
* 改坏会红:把赋值改回 `quotes[0]`。
*/
assert.match(MAIN, /for \(let i = 0; i < content\.quotes\.length; i\+\+\)/,
'要遍历服务端返回的整批(只取 [0] 会让轮播静默失效)');
assert.match(MAIN, /this\.topQuotes = qs;/,
'整批赋给本地状态');
assert.doesNotMatch(MAIN, /@State topQuoteSource/,
'出处状态要删干净,不留只写不读的死字段');});
test('2in1|一言与签名要在客户端缓存(账号级),离线也能轮播', () => {
/*