跨端: 平板三处真 bug(返回回登录页 / 避让重复叠加 / 联系人卡被裁 10vp)

用户报的「在主页返回为什么会直接回到登陆页」是**原语用错**:
LoginPage 用 pushUrl 进 MainPage,路由栈成 [LoginPage, MainPage],
返回自然弹回登录页。而 Logout.ets 早就是 replaceUrl 并写了理由
(「退出后不该还能'返回到已登出的页'」)—— 同一个不变式、相反方向,
只改了一半。三处 pushUrl → replaceUrl,设备实测:返回直接退出 app
(前台变 com.huawei.hmos.browser),不再回登录页。

另两处平板(HUAWEI MatePad Pro, 2800x1840, ratio 1.52 ⇒ 宽屏):
· 内容列 `this.isWide ? Theme.surface : (bgActive ? 透明 : surface)`
  —— 宽屏分支把 bgActive 丢掉了 ⇒ 平板上壁纸永远被挡。
· `top: this.isWide ? paneGap : statusBar` —— 宽屏分支把状态栏避让丢掉
  ⇒ 页签字压在系统时钟下。
· 侧栏自己也加了一次 topInset,而父 Row 的 padding 已经含状态栏
  ⇒ 83+83=166px 空白(用户:「避让有点用力过猛」)。
· 联系人列表项硬写 .height(85)+clip,内容实际要 95vp ⇒ 写信/归档行被裁一半。

判据 harmony-nav 新增「登录/退出必须用同一个原语」并做变异验证
(改回 pushUrl 即红);注册数 18→19,全绿 19/19。

★ 途中发现的记账缺口:设备判据连续失败时走 noteBusySkip 计数,
  .tmp/harmony-busy-skips.json 累到 10 后拒绝再当'礼貌跳过'。
  清除账本 + 让 app 真在前台后立刻 19/19 —— 说明**不是代码问题**,
  是账本把'设备忙'当成了证据。这一点记进 DEBTS。
This commit is contained in:
2026-09-19 23:38:18 +08:00
parent 110de79401
commit c1465e09ab
6 changed files with 284 additions and 35 deletions

View File

@ -1339,3 +1339,67 @@ test('★ 判据自检:设备忙的跳过有界 —— K 轮内礼貌、超了
rmSync(dir, { recursive: true, force: true });
}
});
test('★ 登录/退出必须用同一个原语(`replaceUrl`)—— 只改对一半会以"怪现象"回来', () => {
/*
* ★★ 2026-09-19 修的真 bug(用户报「在主页返回为什么会直接回到登陆页」):
*
* `LoginPage` 用 **`pushUrl`** 去主界面 ⇒ 路由栈是 `[LoginPage, MainPage]`
* ⇒ 在主页按返回,弹掉 MainPage,**回到登录页**。
*
* 而 `api/Logout.ets` 那一半**早就写对了**,注释也写了理由:
* 「④ replaceUrl 而不是 pushUrl:退出后不该还能"返回"到已登出的页」
*
* 两件事是同一条不变式的两端:
* · 退出 ⇒ 不该能返回到已登出的页 ⇒ `replaceUrl` ✓(早就对)
* · 登录 ⇒ 不该能返回到已登录的登录页 ⇒ `replaceUrl`(原来错着)
*
* ⇒ 这条判据的形状是**成对检查**,不是逐处检查 ——
* 因为它要防的不是"某一处写错",而是"**只改对了一半**"。
* 逐处判据在那个形状下必然漏(另一半当时全绿)。
*
* ★ 为什么不判"必须有 replaceUrl"这么简单:登录要走快速路径(已有账号)、
* doLogin、tryRestore 三条,逐个写死数量会在加第四条时假红。
* 所以判"**不许有** pushUrl 到 MainPage" —— 那是唯一会破坏不变式的写法。
*/
const login = code(join(HARMONY_ETS, 'pages/LoginPage.ets'));
const logout = code(join(HARMONY_ETS, 'api/Logout.ets'));
/* ① 登录侧:不许 pushUrl 到主界面 */
const badLogin = [...login.matchAll(/pushUrl\(\{[^}]*MainPage[^}]*\}/g)].map((m) => m[0]);
assert.deepEqual(badLogin, [],
'★ `LoginPage` 里不许用 `pushUrl` 去 `MainPage` —— 那会把主界面**压在登录页之上**,\n' +
' 于是在主页按返回会**回到登录页**(用户 2026-09-19 报的就是这个)。\n' +
' 登录是"到达"不是"进入下一层",要用 `replaceUrl`。\n' +
' 违规处:\n ' + badLogin.join('\n '));
/* ② 登录侧:至少要有一次 replaceUrl 到主界面(否则上面那条可能被"全删掉"满足) */
assert.match(login, /replaceUrl\(\{[^}]*MainPage[^}]*\}/,
'`LoginPage` 要有 `replaceUrl` 去 `MainPage`(上一条只是"不许 pushUrl",' +
'光删不写也能满足它 —— 这条补上"必须真的用对的那个")');
/* ③ 退出侧:同样的口径(早就对了,钉住别退化) */
assert.match(logout, /replaceUrl\(\{[^}]*LoginPage[^}]*\}/,
'`api/Logout.ets` 要用 `replaceUrl` 回登录页 —— 退出后不该还能"返回"到已登出的页');
assert.ok(!/pushUrl/.test(logout),
'`Logout` 里不该出现 `pushUrl`:与 LoginPage 是**同一不变式的两端**,' +
'两边都要 replaceUrl(一边对一边错就是本条要防的形状)');
/*
* ★★ 自检:**造一个违规样本喂给①那条正则**,确认它真的抓得到。
*
* 为什么必须做这一步(本仓反复记录过):形状判据最常见的失效方式是
* 正则写歪了 ⇒ **恒绿**。而恒绿的判据看起来和"代码是对的"一模一样。
* 这里主动构造 `pushUrl({ url: 'pages/MainPage' })` ——
* 正是修复前真实存在的那一行 —— 要求正则命中它。
*/
const violation = "this.getUIContext().getRouter().pushUrl({ url: 'pages/MainPage' });";
const re = /pushUrl\(\{[^}]*MainPage[^}]*\}/g;
assert.ok(re.test(violation),
'★ 自检失败:①的正则抓不到修复前那行真实代码 —— 说明正则是歪的,' +
'那样本判据**恒绿**(看起来和"代码正确"一样)。样本:' + violation);
/* 反过来:正确写法不该被误报 */
const ok = "this.getUIContext().getRouter().replaceUrl({ url: 'pages/MainPage' });";
assert.ok(!/pushUrl\(\{[^}]*MainPage[^}]*\}/.test(ok),
'★ 自检失败:正确写法(replaceUrl)被误判为违规');
});