判据: 管理页设备判据(到期静态判据的第一条升级)+ 修设备判据基建的三个真 bug
`run-all.mjs` 的 `STATIC_ONLY` 里登记着 6 条"只能静态验"的判据,前提是
「本工作区能装、能点设备」。设备现在可用 ⇒ 它们**到期**了(欠账当场变红)。
这一批升级第一条:`harmony-admin`(用户管理页)。
## 一、新设备判据:管理页真的能打开、列表真的渲染
上面 27 条静态判据判的都是逻辑与接线形态。它们全绿时,
"管理页能不能打开、列表能不能渲染"**一句都没验过** —— 而它最容易坏:
路由注册对了但入口没接上、接口回来了但列表没渲染。
判据自己搭现场:拉起应用 → 进「我的」→ 滚到底 → 点「管理」→
断言真的在管理页(出现「新建用户」)且**渲染出用户行**。
## 二、途中撞出三个**判据自身**的 bug(都已修,都写了教训)
1. **`tapText` 从来没成功点过任何东西**(`lib/harmony-device.mjs`)
`findByText` 返回的是**数组**,我当单个节点用了 ⇒ `node.attributes` 恒
`undefined` ⇒ 恒返回 `false`。症状极隐蔽:调用方以为"没找到那个文案",
实际是帮手自己坏了。修:从数组里挑,且**优先挑可点的那个**
(同名文案常常一个可点、一个不可点)。
2. **判据之间互相干扰**(新增 `backToMain`)
每个设备判据都是写操作,会把前台留在它操作完的那一页。`harmony-admin`
把人留在管理页 —— 那是 `pushUrl` 出去的独立 `@Entry` 页,**没有侧栏**
⇒ 后面的 `cross-client-gesture` / `harmony-appearance` 找不到侧栏、
双双 skip(skip 原因写的是"宽屏侧栏找不到",看起来像功能没了)。
`launchOurApp` 解决不了(`aa start` 只切前台,不弹栈)⇒ 新增 `backToMain`。
3. **"找不到元素"要先分清是功能缺失还是判据没摆好现场**
本判据连栽四种形态,每一种都伪装成"功能缺失":
· 锚点文案错(入口是「管理」,我按「用户管理」找 —— 后者只是说明的一部分)
· 元素在滚动下方(dumpLayout 只报可见节点)
· 文字节点不可点(`.onClick` 在包住它的容器上,ArkUI 的常态)
· **滚动步长跨过了它**(诊断打印现场才发现:停在了「系统通知」那一带,
而「管理」只占 ~0.04 屏,一次 0.2 屏的滑动必然越过)
⇒ 最后改成"**先滚到底、再小步回扫**"(只管往前找在不均匀列表上必漏),
并加了 `AGENTMAIL_ADMIN_DEBUG=1` 的诊断入口把现场打出来。
## 三、判据
`run-all.mjs` → `files=32 ran=32 checks=505 pass=505 fail=0 skip=0
red=0 broken=0 unreported=0`(连跑两次稳定)。
`harmony-admin` 27 → 28 条。`hvigorw assembleHap` 成功;前端重建。
**设备实测**:管理页从「我的」页打开,显示「管理 / 用户管理 3 / 新建用户」
与三行用户(jianf 管理员 / gui-lab 用户 / test 用户)。
**仍到期未升级**:`harmony-appearance`(已加 2 条设备判据,但登记里其余部分仍静态)、
`harmony-logic`、`cross-client-theme`、`appearance-defaults`、`harmony-imageprep`
—— 按"每条缺什么设备侧验证"逐条来,不为了消数字而凑。
This commit is contained in:
@ -526,3 +526,202 @@ test('★ 「我的」页的数据都来自服务端真实字段(不是写死/
|
||||
assert.ok(key.includes('key_token'),
|
||||
'UserKey 要有 key_token —— 密钥全文只在创建响应里出现一次,没有它就没法展示');
|
||||
});
|
||||
|
||||
/* ═══════════════ 设备侧:管理页真的能打开、能操作 ═══════════════ */
|
||||
|
||||
test('★ 设备:管理页能从「我的」页打开,且列表真的渲染出用户', async (t) => {
|
||||
/*
|
||||
* 这条补的是 `run-all.mjs` 的 `STATIC_ONLY` 登记里说的那个缺口:
|
||||
* 「用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点(本机两者都没有)」
|
||||
*
|
||||
* 上面 27 条判的都是**逻辑与接线形态**。它们全绿时,"管理页能不能打开、
|
||||
* 列表能不能渲染"这件事**一句都没验过** —— 而它偏偏最容易坏:
|
||||
* 路由注册对了但入口没接上、接口回来了但列表没渲染、非管理员也能看见入口。
|
||||
*
|
||||
* ★★ 写这条判据时连撞三个坑,每个都是**判据自己的**错(记下来,它们是同类):
|
||||
* ① 锚点文案写错:入口标题是「管理」,我却按「用户管理」找
|
||||
* (后者只是它下面那行说明的一部分)⇒ **永远跳过**。
|
||||
* 而那个跳过看起来像"非管理员、行为正确"—— 最危险的那种静默。
|
||||
* ② 元素在**滚动下方**:dumpLayout 只报可见节点 ⇒ 不滚到底根本搜不到。
|
||||
* ③ 文字节点不可点:`Text('管理')` 的 `clickable=false`,
|
||||
* 点击挂在包住它的容器上(ArkUI 的常态)。
|
||||
*
|
||||
* ⇒ 结论:设备判据里"找不到元素"必须先分清是**功能缺失**还是**判据没摆好现场**。
|
||||
* 前两个坑都是后者,而它们伪装成前者。
|
||||
*/
|
||||
const D = await import('./lib/harmony-device.mjs');
|
||||
const hdc = D.findHdc();
|
||||
if (!hdc || !D.hasTarget(hdc)) {
|
||||
return t.skip('设备不在 —— 本条的设备半边本次不跑(上面静态层仍把住逻辑与接线)');
|
||||
}
|
||||
assert.ok(await D.launchOurApp(hdc), '要能拉起应用并等到它到前台');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const dims = () => {
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
for (const n of D.walk(D.dumpLayout(hdc))) {
|
||||
const m = /\[\d+,\d+\]\[(\d+),(\d+)\]/.exec(n.attributes?.bounds || '');
|
||||
if (m) {
|
||||
w = Math.max(w, Number(m[1]));
|
||||
h = Math.max(h, Number(m[2]));
|
||||
}
|
||||
}
|
||||
return { w, h };
|
||||
};
|
||||
const hasText = (needle) => [...D.walk(D.dumpLayout(hdc))]
|
||||
.some((n) => (n.attributes?.text || '').includes(needle));
|
||||
|
||||
/* 已经在管理页了?那直接用(判据不该强求"必须从某处点进来") */
|
||||
if (!hasText('新建用户')) {
|
||||
const { w: scrW, h: scrH } = dims();
|
||||
const wide = scrW / scrH > 1.2;
|
||||
|
||||
/*
|
||||
* ① 进「我的」
|
||||
*
|
||||
* ★★ 「在「我的」页」**不能用单个文案判**(第一版用「用户名」⇒ 假红):
|
||||
* 本页有 8 个分段、可滚动,而**判据里的滚动会把「用户名」滚出屏幕**
|
||||
* —— 同一个判据第二次跑到这里时(或前面判据把它留在这页且已滚动),
|
||||
* `hasText('用户名')` 为假,于是它以为"不在「我的」页"、又去点一次导航,
|
||||
* 结果页面被滚到别处、后面全乱。
|
||||
*
|
||||
* 改用**分段标记**判定:这些文案分布在本页的不同段落,
|
||||
* 滚动到任何位置都至少能看见一个。
|
||||
*/
|
||||
const inMePane = () => ['用户名', '多账号', '外观', '客户端连接密钥', '修改密码']
|
||||
.some((marker) => hasText(marker));
|
||||
if (!inMePane()) {
|
||||
if (wide) {
|
||||
const avatar = [...D.walk(D.dumpLayout(hdc))].find((n) => {
|
||||
const a = n.attributes || {};
|
||||
if (a.clickable !== 'true') return false;
|
||||
const m = /\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(a.bounds || '');
|
||||
if (!m) return false;
|
||||
const [, , y1, x2, y2] = m.map(Number);
|
||||
return x2 <= scrW * 0.08 && y1 > scrH * 0.5 && (y2 - y1) > 80;
|
||||
});
|
||||
if (!avatar) return t.skip('宽屏侧栏头像找不到 —— 无法进「我的」');
|
||||
const c = D.boundsCenter(avatar.attributes.bounds);
|
||||
assert.ok(D.tap(hdc, c.cx, c.cy), '要能点到侧栏头像');
|
||||
} else {
|
||||
assert.ok(D.tapText(hdc, '我的'), '要能点到「我的」');
|
||||
}
|
||||
let landed = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
if (inMePane()) { landed = true; break; }
|
||||
}
|
||||
assert.ok(landed, '要能进「我的」页');
|
||||
}
|
||||
|
||||
/*
|
||||
* ② 找「管理」入口(在 8 个分段之后,屏幕外)。
|
||||
*
|
||||
* ★★ 滚动必须**小步 + 每步检查**(我前两版都栽在这里):
|
||||
* 大步滑(`0.8h → 0.25h`)会**跳过**那个入口 —— 实测:滚 1~2 次时它在屏外,
|
||||
* 第 3 次才出现(`y=1994`,屏高 2232,贴着底)。而"滑到底再看"
|
||||
* 会因为惯性/回弹停在别处,于是"找不到"——看起来像功能缺失。
|
||||
*
|
||||
* 现在每步只滑 ~1/4 屏,滚动后立刻查一次。
|
||||
*/
|
||||
/*
|
||||
* ★★ 第四版(诊断逼出来的真相):小步滚也会**越过**它。
|
||||
* 实测现场:滚到「系统通知」那一带(`03:50` 附近)时,「管理」**已经在上面**了
|
||||
* —— 它在「客户端连接密钥」之后、「系统通知」之前,而我的步长一次跨过了整个
|
||||
* 区间(每次 ~0.2 屏,而它只占 ~0.04 屏)。
|
||||
*
|
||||
* ⇒ 正确策略是**先滚到底,再从下往上找**(到底之后它一定在屏幕内或其上方,
|
||||
* 再按小步回滚就必然扫过它)。"只管往前找"在不均匀列表上必然漏。
|
||||
*/
|
||||
const scrollDown = async (steps) => {
|
||||
for (let i = 0; i < steps; i++) {
|
||||
D.swipe(hdc, Math.round(scrW * 0.5), Math.round(scrH * 0.7),
|
||||
Math.round(scrW * 0.5), Math.round(scrH * 0.3), 2500);
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
}
|
||||
};
|
||||
/* ① 一口气滚到底(多滚几次确保到底) */
|
||||
await scrollDown(8);
|
||||
/* ② 若「管理」已经在屏上就好了;否则小步**回滚**(内容往下走)扫回来 */
|
||||
let found = hasText('管理');
|
||||
for (let i = 0; i < 8 && !found; i++) {
|
||||
D.swipe(hdc, Math.round(scrW * 0.5), Math.round(scrH * 0.35),
|
||||
Math.round(scrW * 0.5), Math.round(scrH * 0.6), 1500);
|
||||
await new Promise((r) => setTimeout(r, 650));
|
||||
found = hasText('管理');
|
||||
}
|
||||
if (!found) {
|
||||
const seen = [...D.walk(D.dumpLayout(hdc))]
|
||||
.map((n) => (n.attributes?.text || '').trim())
|
||||
.filter(Boolean).slice(-20).join(' | ');
|
||||
return t.skip(`滚到底后仍找不到「管理」入口(非管理员时这是正确行为)—— 末尾读到:${seen}`);
|
||||
}
|
||||
|
||||
/*
|
||||
* ③ 点它。文字节点不可点(`.onClick` 在包住它的容器上),所以:
|
||||
* 先让 `tapText` 试(它会挑同名的**可点**节点),失败再退回点文字中心
|
||||
* (命中测试会冒泡到祖先的 onClick)。
|
||||
*
|
||||
* ★ 兜底那一步要**重新 dump 并当场找**,不能复用上面 `found` 那次的快照 ——
|
||||
* 实测(套件里红、单独跑绿):`found` 为真说明"那一瞬间有",
|
||||
* 而下一步再 dump 时可能正在重绘/滚动的惯性里 ⇒ 快照里没有 ⇒ 假红。
|
||||
* 设备上的两次 dump 之间窗口状态是可以变的,别假设它们一致。
|
||||
*/
|
||||
/*
|
||||
* ★★ 第三版:**直接按文字节点的矩形中心点**,不走 `tapText`。
|
||||
*
|
||||
* 为什么 `tapText` 在这里不可靠(实测:单独跑绿、套件里红):
|
||||
* 它先找"同名且**可点**"的节点;这个入口的 `.onClick` 挂在
|
||||
* **外面那层 `Column`** 上(`SettingsPage.ets:1144`),而那层的
|
||||
* bounds 比文字大一圈、且**没有文字** ⇒ `findByText` 找不到它,
|
||||
* `tapText` 退回点文字中心;但文字节点的中心在**卡片内部**,
|
||||
* 命中测试是否冒泡到那层 Column 取决于层级细节 —— 不稳定。
|
||||
*
|
||||
* 而实测(手动点 `(1592, 2017)`,即文字所在行的中心)**每次都进管理页**。
|
||||
* ⇒ 用文字节点的**矩形中心**直接点,这条路径最接近"人点那一行"。
|
||||
*/
|
||||
/* 诊断入口:`AGENTMAIL_ADMIN_DEBUG=1` 时把现场打出来(排查设备判据专用) */
|
||||
const dbg = process.env.AGENTMAIL_ADMIN_DEBUG
|
||||
? (m) => console.log(`[admin] ${m}`) : () => {};
|
||||
const snap = D.dumpLayout(hdc);
|
||||
const node = [...D.walk(snap)]
|
||||
.find((n) => (n.attributes?.text || '').trim() === '管理');
|
||||
if (!node) {
|
||||
const seen = [...D.walk(snap)]
|
||||
.map((n) => (n.attributes?.text || '').trim()).filter(Boolean).slice(-18).join(' | ');
|
||||
dbg(`找不到「管理」节点;末尾文本:${seen}`);
|
||||
}
|
||||
assert.ok(node, '滚到之后「管理」文字节点应在 dump 里');
|
||||
const c = D.boundsCenter(node.attributes.bounds);
|
||||
assert.ok(c, '要能解析出「管理」的坐标');
|
||||
/*
|
||||
* 点之前**再确认一次前台是我们的应用**:套件里前面跑过的判据可能把前台
|
||||
* 切走(那是"判据间干扰"的另一种形态)。切走了就不是"点不到入口",
|
||||
* 而是"点在了别人的界面上"—— 后者必须显式失败,不能算通过。
|
||||
*/
|
||||
assert.equal(D.foregroundBundle(hdc), D.ourBundle(),
|
||||
'点之前前台必须还是我们的应用(前面判据若切走了前台,这里点的是别人的界面)');
|
||||
const ok = D.tap(hdc, c.cx, c.cy);
|
||||
dbg(`点 (${c.cx},${c.cy}) ⇒ ${ok}`);
|
||||
assert.ok(ok, `要能点到「管理」入口(坐标 ${c.cx},${c.cy})`);
|
||||
}
|
||||
|
||||
/* ④ 等页面真的切过去 */
|
||||
let onAdmin = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
if (hasText('新建用户')) { onAdmin = true; break; }
|
||||
}
|
||||
assert.ok(onAdmin,
|
||||
'★ 点「管理」之后要真的在**管理页**(标志:出现「新建用户」按钮)—— ' +
|
||||
'只断言"点到了入口"证明不了页面切过去了(路由没注册时点击静默无效)');
|
||||
|
||||
/* ⑤ 列表要真的渲染出用户行(抓"接口回来了但列表没接上") */
|
||||
const root = D.dumpLayout(hdc);
|
||||
const texts = [...D.walk(root)].map((n) => (n.attributes?.text || '').trim()).filter(Boolean);
|
||||
const userRows = texts.filter((x) => /用户管理|启用|管理员|用户$/.test(x));
|
||||
assert.ok(userRows.length > 0,
|
||||
'★ 管理页要真的渲染出**用户行**(带角色/状态的那种)—— ' +
|
||||
`只有标题而没有行,说明数据没渲染。实际读到:${texts.slice(0, 25).join(' | ')}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user