跨端: 平板三处真 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:
@ -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)被误判为违规');
|
||||
});
|
||||
|
||||
@ -204,8 +204,34 @@ export function findHdc() {
|
||||
}
|
||||
|
||||
/** 在 hdc 上跑一条命令,带回 spawnSync 结果。 */
|
||||
/**
|
||||
* 选定目标设备的连接键(`AGENTMAIL_HARMONY_TARGET`,如 `192.168.2.108:43679`)。
|
||||
*
|
||||
* ★★ 为什么需要它(2026-09-19 装真平板时撞出来的):
|
||||
* 原来所有助手都**不带 `-t`** ⇒ 一旦同时存在两个目标
|
||||
* (模拟器 `127.0.0.1:5555` + 真平板 `192.168.2.108:43679`),
|
||||
* `hdc` 会挑一个 —— 于是设备判据可能在**另一台设备**上跑,
|
||||
* 而它照样报绿("跑错设备"与"跑对"从输出上看不出来)。
|
||||
*
|
||||
* 这与本仓那条"判据的边界要显式"是同一条:#一台设备#是环境事实,
|
||||
* 不能靠"当前只有一台"这种偶然。
|
||||
*
|
||||
* 用法:`AGENTMAIL_HARMONY_TARGET=192.168.2.108:43679 node run-all.mjs`
|
||||
* 不设时行为与从前完全一致(多目标时由 hdc 自己挑)。
|
||||
*/
|
||||
export function targetKey() {
|
||||
const k = (process.env.AGENTMAIL_HARMONY_TARGET || '').trim();
|
||||
return k.length > 0 ? k : null;
|
||||
}
|
||||
|
||||
function sh(hdc, args, timeout = 20000) {
|
||||
return spawnSync(hdc, args, { encoding: 'utf8', timeout });
|
||||
/*
|
||||
* 注入 `-t <key>`:放在 `hdc` 之后、子命令之前(hdc 的参数序)。
|
||||
* 已经被调用方显式带 `-t` 时不重复注入。
|
||||
*/
|
||||
const key = targetKey();
|
||||
const argv = key && !args.includes('-t') ? ['-t', key, ...args] : args;
|
||||
return spawnSync(hdc, argv, { encoding: 'utf8', timeout });
|
||||
}
|
||||
|
||||
/** `hdc list targets` 的输出(trim 后)。null 表示 hdc 都没找到。 */
|
||||
@ -566,34 +592,66 @@ export function closeColor(a, b, tol = 12) {
|
||||
*
|
||||
* 坐标系与 `dumpLayout` 的 `bounds` 一致(屏幕像素,原点左上)。
|
||||
*/
|
||||
export function readPixels(pngPath, screenW = 3184, screenH = 2232) {
|
||||
export function readPixels(pngPath, screenW, screenH) {
|
||||
if (!existsSync(pngPath)) return null;
|
||||
const out = join(tmpdir(), `hm-rgb-${process.pid}-${Date.now()}.raw`);
|
||||
|
||||
/*
|
||||
* ★★ 尺寸必须**问出来**,不能猜(2026-09-19 装真平板时撞出来的)。
|
||||
*
|
||||
* 我原来的实现是"从 raw 字节数 + 一个候选宽度表反推":
|
||||
*
|
||||
* const px = buf.length / 3;
|
||||
* let w = screenW; // 默认 3184(模拟器)
|
||||
* for (const cand of [3184, 2232, 2560, 1920, 1280, 1080]) ...
|
||||
*
|
||||
* 在模拟器(3184x2232,正好在表里)一直是对的,
|
||||
* **装到真平板(2800x1840)就错了**:
|
||||
* 候选表里没有 2800 => 退到 1280 => 算出 **1280x4025**
|
||||
* (像素数恰好相同:5152000)=> 不报错,`at(x,y)` 全读到错位的行。
|
||||
*
|
||||
* 危险之处是**它长得像成功**:句柄返回了、尺寸是个正数、
|
||||
* `at()` 也不越界 —— 只有拿 `file` 去看原图才发现对不上。
|
||||
* 这个错会一路带进结论("某个颜色不对"其实是读错了行)。
|
||||
*
|
||||
* => 改用 `ffprobe` 问真实尺寸(一次调用,几毫秒),
|
||||
* 并且**校验 raw 字节数是否等于 w*h*3** ——
|
||||
* 不等就返回 null(宁可让判据说"读不到",也不要给它一张错位的图)。
|
||||
*/
|
||||
let w = screenW || 0;
|
||||
let h = screenH || 0;
|
||||
if (!w || !h) {
|
||||
const probe = spawnSync('ffprobe', [
|
||||
'-v', 'error', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=width,height',
|
||||
'-of', 'csv=p=0', pngPath,
|
||||
], { encoding: 'utf8', timeout: 20000 });
|
||||
const out = (probe.stdout || '').trim(); // 形如 "2800,1840"
|
||||
const m = /^(\d+),(\d+)$/.exec(out);
|
||||
if (!m) return null;
|
||||
w = Number(m[1]);
|
||||
h = Number(m[2]);
|
||||
}
|
||||
if (!w || !h) return null;
|
||||
|
||||
const raw = join(tmpdir(), `hm-rgb-${process.pid}-${Date.now()}.raw`);
|
||||
const r = spawnSync('ffmpeg', [
|
||||
'-loglevel', 'error', '-y', '-i', pngPath,
|
||||
'-f', 'rawvideo', '-pix_fmt', 'rgb24', out,
|
||||
'-f', 'rawvideo', '-pix_fmt', 'rgb24', raw,
|
||||
], { encoding: 'utf8', timeout: 60000 });
|
||||
if (r.error || !existsSync(out)) return null;
|
||||
if (r.error || !existsSync(raw)) return null;
|
||||
|
||||
let buf;
|
||||
try {
|
||||
buf = bytes(out);
|
||||
buf = bytes(raw);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
try { unlinkSync(out); } catch { /* 清不掉就算了 */ }
|
||||
}
|
||||
/* 尺寸从数据长度反推 —— 这样调用方不必传对屏幕尺寸 */
|
||||
const px = buf.length / 3;
|
||||
let w = screenW;
|
||||
let h = Math.round(px / w);
|
||||
if (h * w !== px) {
|
||||
/* 退一步:按常见屏宽找整除(设备可能是折叠态/别的分辨率) */
|
||||
w = 0;
|
||||
for (const cand of [3184, 2232, 2560, 1920, 1280, 1080]) {
|
||||
if (px % cand === 0) { w = cand; h = px / cand; break; }
|
||||
}
|
||||
if (!w) return null;
|
||||
try { unlinkSync(raw); } catch { /* 清不掉就算了 */ }
|
||||
}
|
||||
|
||||
/* 字节数必须严丝合缝 —— 这是防"错位读到别的行"的那道闸 */
|
||||
if (buf.length !== w * h * 3) return null;
|
||||
|
||||
return {
|
||||
w,
|
||||
h,
|
||||
|
||||
@ -83,7 +83,7 @@ const SUITE = [
|
||||
// P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types
|
||||
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 27],
|
||||
// P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位
|
||||
['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 18],
|
||||
['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 19],
|
||||
// 上下黑边(用户 2026-09-17/18 报过两次)——钉的是一整套东西的两半:
|
||||
// `setWindowLayoutFullScreen(true)`(消黑边)+ `getWindowAvoidArea`(让开时钟/手势条)。
|
||||
// 只做前半 ⇒ 页签被时钟盖住(上一次就是这样退回去的,黑边于是留了三天);
|
||||
|
||||
Reference in New Issue
Block a user