Files
MailUI4Agents/client/electron/test/cross-client-theme.test.mjs
JianFeeeee e0c68d8bb2 test(cross-client): 预算条三个档位的令牌也和 WebUI 对齐 + 记录模拟器权限门
## 判据

P2a 新加的 6 个预算条令牌(`chipNeutral*` / `chipSpent*` / `chipWarn*`)此前只有鸿蒙侧
自己在用,没有任何判据钉住它们与 WebUI `BudgetChip` 的 gray-100/gray-500、
red-100/red-700、orange-100/orange-700 是**同一批值** —— 于是"鸿蒙那边自己挑了个接近的红"
没人会发现。补进 `cross-client-theme.test.mjs`(8 → 8 条,第 7 条扩了 6 个断言),
并加了反向对照(差一个色阶必须判红,实测判红)。

注意 `--c-gray-100` 等在 `.dark` 段有第二处定义,`cssVar()` 取第一处(`:root` 浅色一套),
与 `Theme.ets` 的浅色令牌对应。

## 文档

`docs/HARMONY-ALIGN-PLAN.md` §7.6:把"鸿蒙视觉/点击未验"查到根上 —— DevEco CLI 说明本机
**有**可启动的模拟器(起来后 `devecocli ui layout/click/screenshot` 能做点击级验证),
但 `harmony-emu start` 要写 `/run`、`/root/.Huawei`(工作区之外),按规矩升级重试一次后
被拒:**「权限询问无法送达:该任务链上没有人类用户」**。
即:鸿蒙的点击级验证不是"还没做",是"当前做不到"——需要人在命令行跑一次
`harmony-emu start`。此前所有阶段只能按"逻辑有可执行判据 + 接线有判据 + 观感未验"验收。
2026-09-14 13:38:35 +08:00

164 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 两个客户端必须用**同一份设计词表**。
*
* 用户下一步要求:「同步 ui 设计到客户端」。同步的第一件事不是把每个页面重画一遍,
* 而是两边共用同一套令牌(颜色/圆角/玻璃透明度)—— 否则每加一个页面就重抄一遍色值,
* 两个客户端会越走越远,而且这种漂移**没有任何判据会红**。
*
* 这里只断言"两边对同一件事的取值一致"不断言实现方式WebUI 用 CSS 变量、
* 鸿蒙用 ArkTS 常量,本来就该不同)。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..', '..');
const web = readFileSync(join(ROOT, 'client/electron/src/index.css'), 'utf8');
const harmony = readFileSync(
join(ROOT, 'client/harmony/entry/src/main/ets/common/Theme.ets'),
'utf8'
);
const hex = h => h.toUpperCase();
test('品牌蓝一致', () => {
// WebUI: --c-blue-600 应该是 37 99 235 = #2563EB
const webBlue = web.match(/--c-blue-600:\s*(\d+)\s+(\d+)\s+(\d+)/);
assert.ok(webBlue, 'WebUI 要有 --c-blue-600');
const asHex = hex(
'#' +
[webBlue[1], webBlue[2], webBlue[3]]
.map(n => Number(n).toString(16).padStart(2, '0'))
.join('')
);
const harmonyBlue = harmony.match(/accent: string = '(#[0-9A-Fa-f]{6})'/);
assert.ok(harmonyBlue, '鸿蒙要有 accent');
assert.equal(hex(harmonyBlue[1]), asHex, `品牌蓝不一致WebUI ${asHex} vs 鸿蒙 ${harmonyBlue[1]}`);
});
test('卡片圆角一致WebUI 0.875rem = 14px', () => {
assert.match(web, /--radius-card:\s*0\.875rem/);
assert.match(harmony, /radiusCard: number = 14/);
});
test('导航玻璃不透明度一致(都是 0.72', () => {
assert.match(web, /--nav-bg:\s*255 255 255 \/ 0\.72/);
// 0.72 × 255 ≈ 184 = 0xB8鸿蒙用 #AARRGGBB顺序与 CSS 不同)
assert.match(harmony, /navBgLight: string = '#B8FFFFFF'/);
});
test('★ 判据自检:把鸿蒙的品牌蓝改成别的必须判红', () => {
const mutated = harmony.replace(/accent: string = '#2563EB'/, "accent: string = '#FF0000'");
const m = mutated.match(/accent: string = '(#[0-9A-Fa-f]{6})'/);
assert.notEqual(hex(m[1]), '#2563EB');
});
test('鸿蒙的令牌文件说明了与 WebUI 的对应关系(不是凭空一套)', () => {
assert.match(harmony, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/);
});
test('★ 鸿蒙页面里不得出现任何裸色值(枚举挡不住漂移,"类"才能挡)', () => {
/*
* 上一版这条判据只列了 8 个旧色值(#1A73E8 / #333333…于是判据全绿的
* 同时pages/ 里还留着 14 处**另一套**写死的色Google/Material 的
* #E8F0FE、#E8F5E9、#FFF3E0、#D93025 与 #777777/#555555/#444444/
* #cccccc/#aaaaaa、遮罩 #80000000、透明 #00000000。
* 枚举只能挡住"列出来的实例",挡不住漂移本身 —— 改成按类挡:
* pages/ 下一个裸色值都不许有(含 8 位 #AARRGGBB颜色只能来自 Theme.ets。
*
* 页面清单也一并改成扫目录:旧版硬编码 7 个文件名,**新加的页面会自动
* 逃出判据** —— 而发件箱/授权/日历正是接下来要加的页面。
*/
const dir = join(ROOT, 'client/harmony/entry/src/main/ets/pages');
const files = readdirSync(dir).filter(f => f.endsWith('.ets')).sort();
assert.ok(files.length >= 8, `pages/ 下只扫到 ${files.length} 个 .ets判据大概扫错了目录`);
const literal = /#[0-9A-Fa-f]{6,8}\b/g;
for (const f of files) {
const hits = readFileSync(join(dir, f), 'utf8').match(literal);
assert.equal(hits, null, `${f} 里有裸色值 ${hits ? hits.join('、') : ''}(应改用 Theme 令牌)`);
}
// 反向对照:判据要真能抓到裸色值(否则写错了正则也是一片绿)
assert.deepEqual("fontColor('#E8F0FE')".match(literal), ['#E8F0FE']);
assert.deepEqual("color('#80000000')".match(literal), ['#80000000']);
// 令牌文件本身必须是**唯一**的颜色来源,否则上面那条会因为"哪儿都没有色值"而空转
assert.ok(/#[0-9A-Fa-f]{6,8}/.test(harmony), 'Theme.ets 里应该有真正的色值');
});
test('权限档位徽标两边同一套色plan=蓝 / workspace=绿 / full=琥珀)', () => {
// WebUI 的映射写在 PermissionChip.tsx 的类名里;鸿蒙的映射是 Theme.permBg/permFg。
const chip = readFileSync(
join(ROOT, 'client/electron/src/components/PermissionChip.tsx'),
'utf8'
);
assert.match(chip, /plan'[\s\S]{0,80}bg-blue-50 text-blue-700/, 'WebUI plan 档应是蓝');
assert.match(chip, /full'[\s\S]{0,80}bg-amber-50 text-amber-700/, 'WebUI full 档应是琥珀');
assert.match(chip, /bg-green-50 text-green-700/, 'WebUI workspace 档应是绿');
// 鸿蒙侧取的是同一批值 —— 直接和 WebUI 的 :root 变量比,防的是"看起来差不多"
const cssVar = (name, src) => {
const m = src.match(new RegExp(`--${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)`));
assert.ok(m, `WebUI 要有 --${name}`);
return hex('#' + [m[1], m[2], m[3]].map(n => Number(n).toString(16).padStart(2, '0')).join(''));
};
const token = name => {
const m = harmony.match(new RegExp(`${name}: string = '(#[0-9A-Fa-f]{6})'`));
assert.ok(m, `鸿蒙要有令牌 ${name}`);
return hex(m[1]);
};
assert.equal(token('accentSoft'), cssVar('c-blue-50', web), 'plan 底色应与 blue-50 一致');
assert.equal(token('accentStrong'), cssVar('c-blue-700', web), 'plan 字色应与 blue-700 一致');
assert.equal(token('approveBg'), cssVar('c-green-50', web), 'workspace 底色应与 green-50 一致');
assert.equal(token('approveFg'), cssVar('c-green-700', web), 'workspace 字色应与 green-700 一致');
assert.equal(token('warnBg'), cssVar('c-amber-50', web), 'full 底色应与 amber-50 一致');
assert.equal(token('warnFg'), cssVar('c-amber-700', web), 'full 字色应与 amber-700 一致');
/*
* 往返预算条的三个档位P2a 新加)。
*
* WebUI 的 `BudgetChip` 用的是 gray-100/gray-500普通、red-100/red-700用尽
* orange-100/orange-700将尽鸿蒙的卡片视图要显示同一条预算
* 就得取**同一批值** —— 而且这批值只在这条判据里和 WebUI 对齐,
* 否则"鸿蒙那边自己挑了个接近的红"没人会发现。
*
* ⚠️ 注意index.css 里 `--c-gray-100` 等变量在 `.dark` 段里还有第二处定义,
* 上面的 cssVar 取的是**第一处**`:root`,浅色主题)—— 与 `Theme.ets` 是浅色一套对应。
*/
assert.equal(token('chipNeutralBg'), cssVar('c-gray-100', web), '预算普通档底色应与 gray-100 一致');
assert.equal(token('chipNeutralFg'), cssVar('c-gray-500', web), '预算普通档字色应与 gray-500 一致');
assert.equal(token('chipSpentBg'), cssVar('c-red-100', web), '预算用尽底色应与 red-100 一致');
assert.equal(token('chipSpentFg'), cssVar('c-red-700', web), '预算用尽字色应与 red-700 一致');
assert.equal(token('chipWarnBg'), cssVar('c-orange-100', web), '预算将尽底色应与 orange-100 一致');
assert.equal(token('chipWarnFg'), cssVar('c-orange-700', web), '预算将尽字色应与 orange-700 一致');
// 反向对照:判据要真能抓到"自己挑了个接近的颜色"
const off = harmony.replace("chipSpentBg: string = '#FEE2E2'", "chipSpentBg: string = '#FEE3E3'");
assert.notEqual(
hex(off.match(/chipSpentBg: string = '(#[0-9A-Fa-f]{6})'/)[1]),
cssVar('c-red-100', web),
'自检:差一个色阶判据却还是绿的'
);
});
test('鸿蒙的遮罩是「色 + 透明度」两段式(照 WebUI 的 --bg-scrim + --bg-dim', () => {
/*
* WebUI 的遮罩是**两段**`--bg-scrim`(颜色:浅色=白、深色=黑)+
* `--bg-dim`(透明度),见 `index.css` 的 `.app-backdrop::after`。
* 理由:遮罩色要能**随主题换向** —— 浅色主题用白把图案洗淡,深色主题必须换黑,
* 否则浅色照片在深色界面里糊成一块亮斑、正文读不动WebUI 侧实测踩过)。
* 鸿蒙侧原先写成一个焊死的 `#80000000`,换向时只能再写一个常量 —— 又变成枚举。
*/
assert.match(harmony, /overlayColor: string = '#000000'/, '鸿蒙的遮罩**色**要单独成令牌');
assert.match(harmony, /overlayAlpha: number = 0\.5/, '鸿蒙的遮罩**透明度**要单独成令牌');
// 只查**代码**不查注释Theme.ets 的注释里正当地引用了旧值来解释为什么拆开
const harmonyCode = harmony.replace(/\/\*[\s\S]*?\*\//g, '');
assert.ok(!/#80000000/.test(harmonyCode), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
// WebUI 侧同构:颜色两套(浅/深,同一个变量名换向)+ 透明度独立
assert.match(web, /--bg-scrim: 255 255 255/, 'WebUI 浅色遮罩色');
assert.match(web, /--bg-scrim: 0 0 0/, 'WebUI 深色遮罩色');
assert.match(web, /--bg-dim:/, 'WebUI 的透明度是独立变量');
// 反向对照:判据要真能抓到"焊死单值"
assert.ok(/#80000000/.test("backgroundColor('#80000000')"), '自检:正则抓不到焊死的单值');
});