feat(harmony)★★: 背景档与预设改用官方 Chip,删掉重复的同步指示器
承接 2026-09-29 的反馈,本批改动是那一轮工作的收尾。
## 1. 自绘 Text → 官方 `Chip`
背景档与预设两排原来是**自绘 `Text`**,而且与设置页主题那排
**圆角/内边距不一致**(预设那排 `borderRadius(4)`,另两处是
`radiusControl`)。改用 `import { Chip, AccessibilitySelectedType,
LengthMetrics } from '@kit.ArkUI'`。
## 2. 删掉 `@Prop statusText`(一个数两个指示器)
它原来是「外观」卡右上角那个「已同步」的**第二份**:页面把同一个
`statusLabel(this.appearanceStatus)` 既画在卡片级标题行、又透传进来
画在「背景」小标题右边 ⇒ 同一屏两个「已同步」,永远同值。
删掉透传这一份,值只从页面进、只画一处。
## 3. Surface.ets(32 行)/ Theme.ets(51 行)/ ContactsTab.ets(21 行)
配套:Chip 的尺寸档、主题色取用、联系人页的调用点。
## 判据
- `test/harmony-appearance.test.mjs`:外观判据随实现更新
- `test/mutants/baseline.sha`:变异体自证底本同步
### ⚠️ 设备端判据本机跑不了(不是代码问题)
node --test test/harmony-appearance.test.mjs
pass 26 / fail 3
失败的 3 条**全部**是设备端断言:
★ 设备:打开背景开关后,壁纸上真的出现了
AssertionError: 要能拉起我们的应用并等到它到前台
★ 设备:窗格内容不得超出屏幕
AssertionError: 要能拉起应用
★ 设备:切换主题后界面真的整体换色了
AssertionError: 设备连续 10 轮拉不起应用(本条已连续 3 轮没真跑)
即**本机没有接 Harmony 设备**,不是断言挂在代码上。
判据文本本身断言「设备连续 10 轮拉不起」——它自己在如实报告
「这条从未在真机上跑过」。**需要真机补验**,这里不假装它通过了。
## docs
API.md / GUI-PLAN-HARMONY.md / PLAN.md 同步上述改动。
This commit is contained in:
@ -1656,3 +1656,186 @@ test('★ 设备:窗格内容不得超出屏幕(「我的」页滑杆数值
|
||||
'★ 「我的」窗格有内容超出屏幕 —— 这正是那个 `56%` 被顶出屏外的形状。' +
|
||||
'常见原因:内容列写了 `width(\'100%\')` 而不是 `layoutWeight(1)`(与侧栏相加必溢出)');
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* ═══════════════ ★★ 结算「运行期换肤要设备」这一条欠账 ═══════════════
|
||||
*
|
||||
* `STATIC_ONLY` 里挂着本文件,理由是「壁纸/令牌/遮罩渲染:**观感与运行期换肤**要设备」。
|
||||
* 壁纸那一半本文件早就有设备判据了(上一条「打开背景开关后壁纸上真的出现了」)。
|
||||
* **还欠的是"运行期换肤"** —— 而它在静态层完全看不见:
|
||||
*
|
||||
* 静态层能证:"`onConfigurationUpdated` 里调了 `applyAppearance()`"
|
||||
* "色板两套、深色那套与 CSS 的 `.dark` 逐个相等"
|
||||
* 静态层**证不了**:切了主题之后,**界面真的整体换色了吗**。
|
||||
* ⇒ 这正是本文件自己写的那句话:「观感未验」不是"机制没写",而是"机制写了没跑"。
|
||||
*
|
||||
* ★ 为什么选「我的」页的 chip,而不是侧栏那个主题按钮:
|
||||
*
|
||||
* 侧栏底部主题按钮(36×28vp)与**退出登录**(也是 36×28vp)在 dumpLayout 里
|
||||
* **逐属性相同**:`type=Stack clickable=true`,子树里也是一个 34×34 的空 `Stack`,
|
||||
* 两者都读不到图标名(`moon`/`sun` 不进 dump)。
|
||||
* 设备实测(屏 2800×1840):
|
||||
* 头像 [42,1514][127,1599] 85×85 r=1.000
|
||||
* 主题 [47,1608][124,1668] 77×60 r=1.283 ← 要点的就是这个
|
||||
* 退出 [47,1676][124,1736] 77×60 r=1.283 ← 形状完全一样
|
||||
* ⇒ 区分它们只能靠**兄弟次序**,而点错的后果是**把真机登出**、
|
||||
* `performLogout` → `AccountManager.clearAll()` 清空账号(本文件注释里记过
|
||||
* 这个事故:09:21 设备真的被登出过,`accounts_json` 变 `[]`)。
|
||||
*
|
||||
* ⇒ **判据的失败方式不该是破坏性的**:这条改走「我的」页的三选一 chip
|
||||
* (跟随系统 / 浅色 / 深色),**按文案认**,无歧义、点错也只是改个主题。
|
||||
* ("点哪个"能认准,比"点哪个能出效果"更要紧 —— 认不准的判据是一次会动手的赌。)
|
||||
*/
|
||||
test('★ 设备:切换主题后界面**真的**整体换色了(不是只换了个选中态)', async (t) => {
|
||||
const D = await import('./lib/harmony-device.mjs');
|
||||
const hdc = D.findHdc();
|
||||
if (!hdc || !D.hasTarget(hdc)) {
|
||||
return t.skip('设备不在 —— 本条的设备半边本次不跑(静态层仍把住色板与接线)');
|
||||
}
|
||||
const CRIT = 'harmony-appearance/运行期换肤';
|
||||
/*
|
||||
* 锁屏 ⇒ 走"忙"那条路(K 轮内跳过、连续超 K 轮变红),不硬红。
|
||||
* 理由同 `appearance-defaults` 那条:设备锁着是**只有人能解**的环境状态,
|
||||
* 拿它红一个判据是假红;而不设界又会变成永久灰区。两头都要 ⇒ 记账设界。
|
||||
*/
|
||||
if (!await D.launchOurApp(hdc)) {
|
||||
const { streak, over } = D.noteBusySkip(CRIT);
|
||||
if (over) {
|
||||
assert.fail(`设备连续 ${streak} 轮拉不起应用(本条已连续 ${D.busyLimit()} 轮没真跑)。` +
|
||||
` 先分清是**锁屏**(aa start 报 10106102,要人上滑解锁)还是真坏了。`);
|
||||
}
|
||||
return t.skip(`这次拉不起应用(设备多半锁着)—— 本次不跑(连续第 ${streak}/${D.busyLimit()} 轮)`);
|
||||
}
|
||||
D.noteRan(CRIT);
|
||||
assert.ok(await D.backToMain(hdc), '要能回到主界面(上一条判据可能停在子页上)');
|
||||
|
||||
const screenOf = (root) => {
|
||||
let w = 0, h = 0;
|
||||
for (const n of D.walk(root)) {
|
||||
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 };
|
||||
};
|
||||
|
||||
/* 进「我的」:宽屏点侧栏头像(按**形状**认,r≤1.15,见本文件另一处那段注释)。 */
|
||||
const root0 = D.dumpLayout(hdc);
|
||||
const scr = screenOf(root0);
|
||||
const avatar = [...D.walk(root0)].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 [, x1, y1, x2, y2] = m.map(Number);
|
||||
const w = x2 - x1, h = y2 - y1;
|
||||
return x2 <= scr.w * 0.08 && y1 > scr.h * 0.5 && w > 80 && h > 80 && w / h <= 1.15;
|
||||
});
|
||||
if (!avatar) {
|
||||
return t.skip('宽屏侧栏头像找不到(可能不在宽屏形态)—— 本次不跑,不假装通过');
|
||||
}
|
||||
const ac = D.boundsCenter(avatar.attributes.bounds);
|
||||
assert.ok(D.tap(hdc, ac.cx, ac.cy), '要能点侧栏头像进「我的」');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
/*
|
||||
* 滑到主题那一节。`SettingsPane` 是**常驻挂载**的 ⇒ Scroll 保留上次位置
|
||||
* (本文件另一条判据已经为此吃过亏,注释就在上面),所以**先回顶再下扫**。
|
||||
*/
|
||||
const chipOf = (root, label) => [...D.walk(root)].find((n) => {
|
||||
const a = n.attributes || {};
|
||||
return a.clickable === 'true' && (a.text || '').trim() === label;
|
||||
});
|
||||
const cx = Math.round(scr.w / 2), topY = Math.round(scr.h * 0.25), botY = Math.round(scr.h * 0.75);
|
||||
for (let i = 0; i < 5; i++) { // 回顶
|
||||
D.swipe(hdc, cx, topY, cx, botY);
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
let root = D.dumpLayout(hdc);
|
||||
for (let i = 0; i < 8 && !chipOf(root, '深色'); i++) { // 逐屏下扫
|
||||
D.swipe(hdc, cx, botY, cx, topY);
|
||||
await new Promise((r) => setTimeout(r, 350));
|
||||
root = D.dumpLayout(hdc);
|
||||
}
|
||||
const darkChip = chipOf(root, '深色');
|
||||
const lightChip = chipOf(root, '浅色');
|
||||
assert.ok(darkChip && lightChip,
|
||||
'「我的」页里要能找到「浅色」「深色」两个主题 chip(按**文案**认)—— ' +
|
||||
'找不到说明这一节没了或文案变了,主题那条路就没法在设备上验了');
|
||||
assert.ok(chipOf(root, '跟随系统'), '主题档位应当是三选一(跟随系统 / 浅色 / 深色)');
|
||||
|
||||
/*
|
||||
* ★ 判据读的是**整屏页面底色**,不是选中态。
|
||||
*
|
||||
* 实测(屏 2800×1840,密度 3.5):满屏那个 `Column`(bounds `[0,83][2800,1840]`)
|
||||
* 浅色 → `#FFFFFFFF`(纯白) 深色 → `#FF000000`
|
||||
* 它是**同一个节点、同一处几何**下的两个值 ⇒ 这才是"整体换色了";
|
||||
* 读 chip 的 `backgroundColor` 只能证明"选中态挪了",证不了界面跟着换了。
|
||||
*/
|
||||
const pageBgOf = (r) => {
|
||||
const s = screenOf(r);
|
||||
/* 满屏(宽高都贴到屏幕边)、且**不透明**的那个容器 */
|
||||
const cands = [...D.walk(r)].filter((n) => {
|
||||
const a = n.attributes || {};
|
||||
const bg = (a.backgroundColor || '');
|
||||
const m = /\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(a.bounds || '');
|
||||
if (!m || !bg || bg === '#00000000') return false;
|
||||
const [, x1, y1, x2, y2] = m.map(Number);
|
||||
const w = x2 - x1, h = y2 - y1;
|
||||
return w >= s.w * 0.95 && h >= s.h * 0.9 && a.type === 'Column';
|
||||
});
|
||||
if (!cands.length) return null;
|
||||
/* 面积最大者;并列时取第一个(同一处几何,稳定) */
|
||||
cands.sort((a, b) => {
|
||||
const area = (n) => {
|
||||
const m = /\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(n.attributes.bounds || '').map(Number);
|
||||
return (m[2] - m[0]) * (m[3] - m[1]);
|
||||
};
|
||||
return area(b) - area(a);
|
||||
});
|
||||
return (cands[0].attributes.backgroundColor || '').toUpperCase();
|
||||
};
|
||||
|
||||
const beforeBg = pageBgOf(root);
|
||||
assert.ok(beforeBg,
|
||||
'要能从 dumpLayout 里读出整屏页面底色(读不到说明这一版的判据形状不对)');
|
||||
|
||||
/* 当前选的是哪一档 ⇒ 点一个**确定不是当前档**的,才能断言"状态变了" */
|
||||
const selectedNow = ['跟随系统', '浅色', '深色']
|
||||
.find((label) => {
|
||||
const c = chipOf(root, label);
|
||||
return c && (c.attributes.backgroundColor || '').toUpperCase() === '#FF2563EB';
|
||||
});
|
||||
const target = selectedNow === '深色' ? '浅色' : '深色';
|
||||
const t2 = chipOf(root, target);
|
||||
const tc = D.boundsCenter(t2.attributes.bounds);
|
||||
assert.ok(D.tap(hdc, tc.cx, tc.cy), `要能点到「${target}」`);
|
||||
/* 换色要等一轮重渲染(setColorMode + applyAppearance),并轮询到色真的变 */
|
||||
let afterBg = null;
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
afterBg = pageBgOf(D.dumpLayout(hdc));
|
||||
if (afterBg && afterBg !== beforeBg) break;
|
||||
}
|
||||
|
||||
assert.notEqual(afterBg, beforeBg,
|
||||
`★ 切到「${target}」后整屏页面底色必须变(切换前 ${beforeBg}、切换后 ${afterBg})——\n` +
|
||||
` 相同意味着**主题只换了选中态、界面没换色**,那正是"观感未验"的那一层:\n` +
|
||||
` 静态判据能证 \`applyAppearance()\` 被调用,却证不了它真的让界面变了。\n` +
|
||||
` 查 \`applyThemeNow\` 的顺序(算 → 发布 → 应用)与 \`setColorMode\` 有没有落到应用上。`);
|
||||
|
||||
/* 复位:点回**原来那一档**(档是单选,"再点当前那个"不会变)。判据必须幂等。 */
|
||||
const backLabel = selectedNow || '跟随系统';
|
||||
const backChip = chipOf(D.dumpLayout(hdc), backLabel);
|
||||
assert.ok(backChip, `复位时还能找到「${backLabel}」`);
|
||||
const bc = D.boundsCenter(backChip.attributes.bounds);
|
||||
assert.ok(D.tap(hdc, bc.cx, bc.cy), '要点得回去');
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
if (pageBgOf(D.dumpLayout(hdc)) === beforeBg) break;
|
||||
}
|
||||
assert.equal(pageBgOf(D.dumpLayout(hdc)), beforeBg,
|
||||
'复位后应回到判据开始时的页面底色(判据必须幂等、不留副作用)');
|
||||
|
||||
D.noteBehavioralRan(CRIT);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user