## 判据 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`。此前所有阶段只能按"逻辑有可执行判据 + 接线有判据 + 观感未验"验收。
164 lines
9.2 KiB
JavaScript
164 lines
9.2 KiB
JavaScript
/**
|
||
* 两个客户端必须用**同一份设计词表**。
|
||
*
|
||
* 用户下一步要求:「同步 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')"), '自检:正则抓不到焊死的单值');
|
||
});
|