From 4d226be056a2969c5c7dd17393023aa1b54dd85e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 13:56:43 +0800 Subject: [PATCH] =?UTF-8?q?test(harmony):=20=E3=80=8C=E7=94=A8=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=96=B9=E6=A1=88=E3=80=8D=E7=9A=84=E5=9C=B0=E5=9F=BA?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E7=B3=BB=E7=BB=9F=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=90=8D=E4=B8=8E=20BlurStyle=20=E5=8F=96=E5=80=BC=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E5=8F=AF=E6=A0=A1=E9=AA=8C=EF=BC=88=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E5=85=88=E4=BA=8E=E6=9B=BF=E6=8D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jianf 追加要求「鸿蒙要求用系统方案」(§二·五)。难点是**没有设备**: `$r('sys.color.写错了')` 编译期不报、只有真机运行到那一行才炸 —— 这块会变成谁都验不了的区域。 SDK 里其实带着答案:`sdk/default/openharmony/toolchains/id_defined.json` 列了全部系统资源名 及其类型(本机 API 26:7826 条,color 1059 条);BlurStyle 取值在 `component/common.d.ts` 的 `declare enum BlurStyle` 里。两边都能离线查,所以"名字写对没有"可以变成构建期判据。 新增 `test/harmony-system-api.test.mjs`(4 条,已接进 run-all): 1. SDK 名表/组件声明找不到时**判红并给出替代做法**(会静默跳过的判据等于没有这条判据 —— 本仓库已栽过四次同类问题); 2. 源码里每个 `$r('sys..')` 必须存在且**类型相符**; 3. 每个 `BlurStyle.` 必须在 SDK enum 里; 4. 替换计划要用的那批系统色**先核过再写代码**,并且钉住一条边界:名表里没有 `brand`/`confirm`/`success` —— 权限三档、预算三档这些**业务语义色没有系统对应物**, 继续用自定义令牌,不许"为了系统化"把 plan 档画成 warning 色(那是丢语义换形式)。 变异验证:拼错色名 `ohos_id_color_list_cad_bg` → 报「查无此名」并指出文件; 写错 `BlurStyle.COMPONENT_不存在` → 报出可用取值。顺带修掉解析精度问题: 早先的宽松正则把文档里的 `T`、`R` 也当成了枚举成员,现在精确到 `,`/`=` 分隔符。 验证:`npm test` 退出码 0(10 个判据文件全绿 + vitest 258/258)。 --- .../electron/test/harmony-system-api.test.mjs | 175 ++++++++++++++++++ client/electron/test/run-all.mjs | 1 + docs/HARMONY-ALIGN-PLAN.md | 20 ++ 3 files changed, 196 insertions(+) create mode 100644 client/electron/test/harmony-system-api.test.mjs diff --git a/client/electron/test/harmony-system-api.test.mjs b/client/electron/test/harmony-system-api.test.mjs new file mode 100644 index 0000000..fa38149 --- /dev/null +++ b/client/electron/test/harmony-system-api.test.mjs @@ -0,0 +1,175 @@ +/** + * 「用系统方案」的可验证部分 —— 系统资源名与系统材质取值,**不需要设备**。 + * + * 背景:jianf 追加要求「鸿蒙也同步,但是鸿蒙要求用系统方案」(详见 + * `docs/HARMONY-ALIGN-PLAN.md` §二·五 / §7.10)。方向很清楚:能交给系统的就交给系统 —— + * `backgroundBlurStyle(BlurStyle.*)` 取代手写模糊、`$r('sys.color.*')` 语义色取代自切变量。 + * + * 难点在于**这台机器上没有设备**(模拟器需要人在命令行启动,本沙箱起不来): + * `$r('sys.color.写错了')` 这种错**编译期不报**,只有真机运行到那一行才炸。 + * 于是"用系统方案"会变成一块谁都验不了的区域 —— 那正是本仓库一直在防的东西。 + * + * 解决办法是 SDK 自带的那张表:`sdk/default/openharmony/toolchains/id_defined.json` + * 列了**全部系统资源名及其类型**(本机 API 26:7826 条,其中 color 1059 条)。 + * BlurStyle 的取值则在 `component/common.d.ts` 的 `declare enum BlurStyle` 里。 + * 两边都能离线查 —— 所以"名字写对没有"可以变成构建期判据。 + */ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, 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 ETS_DIR = join(ROOT, 'client/harmony/entry/src/main/ets'); + +/** DevEco CLI 的 SDK 根。可用 HARMONY_CLT 覆盖(换机器时不用改判据)。 */ +const CLT = process.env.HARMONY_CLT || '/opt/huawei/command-line-tools'; +const ID_TABLE = process.env.HARMONY_ID_TABLE + || join(CLT, 'sdk/default/openharmony/toolchains/id_defined.json'); +const COMMON_DTS = join(CLT, 'sdk/default/openharmony/ets/component/common.d.ts'); + +/** + * SDK 名表不在就没法验 —— 这时**必须判红**,不能"跳过"。 + * 一条会自己静默跳过的判据,等于没有这条判据(本仓库已经栽过四次同类问题)。 + */ +const missingSdk = []; +if (!existsSync(ID_TABLE)) missingSdk.push(ID_TABLE); +if (!existsSync(COMMON_DTS)) missingSdk.push(COMMON_DTS); + +test('SDK 的系统资源名表与组件声明要在本机找得到(找不到就明说,不许静默跳过)', () => { + assert.deepEqual( + missingSdk, + [], + `找不到 SDK 文件,这条判据什么都验不了:\n ${missingSdk.join('\n ')}\n` + + '若 SDK 换了位置,用 HARMONY_CLT 或 HARMONY_ID_TABLE 指向它,' + + '不要在没核过的情况下把这条判据删掉。' + ); +}); + +/** 收集鸿蒙源码(.ets / .ts) */ +const collectSources = (dir, acc = []) => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, e.name); + if (e.isDirectory()) collectSources(full, acc); + else if (/\.(ets|ts)$/.test(e.name)) acc.push(full); + } + return acc; +}; + +/** SDK 名表:name → type */ +const loadIdTable = () => { + const recs = JSON.parse(readFileSync(ID_TABLE, 'utf8')).record; + const byName = new Map(); + for (const r of recs) byName.set(r.name, r.type); + return byName; +}; + +/** SDK 的 BlurStyle 成员 */ +const loadBlurStyleMembers = () => { + const src = readFileSync(COMMON_DTS, 'utf8'); + const at = src.indexOf('declare enum BlurStyle'); + assert.ok(at > 0, 'SDK 里应能定位到 declare enum BlurStyle'); + // 枚举体到第一个顶格 `}` 为止;成员形如 ` Thin,` 或 ` COMPONENT_THIN = 6,` + // (早先的写法把文档里的 `T`、`R` 也算成了成员 —— 名字解析必须精确到分隔符) + const block = src.slice(at, src.indexOf('\n}', at)); + return new Set( + [...block.matchAll(/^ {2,}([A-Za-z][A-Za-z_0-9]*)\s*[,=]/gm)].map(m => m[1]) + ); +}; + +/** 扫描 `$r('sys..')` 用法 → [{file, type, name}] */ +const scanSysResources = (files) => { + const found = []; + for (const f of files) { + const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, ''); + for (const m of src.matchAll(/\$r\(\s*'sys\.([a-z]+)\.([A-Za-z0-9_]+)'/g)) { + found.push({ file: f.slice(ROOT.length + 1), type: m[1], name: m[2] }); + } + } + return found; +}; + +/** 扫描 `BlurStyle.` 用法 → [{file, member}](注释已剥离) */ +const scanBlurStyles = (files) => { + const found = []; + for (const f of files) { + const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, ''); + for (const m of src.matchAll(/BlurStyle\.([A-Z][A-Z_0-9]*)/g)) { + found.push({ file: f.slice(ROOT.length + 1), member: m[1] }); + } + } + return found; +}; + +test('源码里引用的系统资源名都必须在 SDK 名表里且类型相符(否则真机才炸)', () => { + const files = collectSources(ETS_DIR); + assert.ok(files.length >= 10, `应扫到至少 10 个源文件,实际 ${files.length}`); + + const table = loadIdTable(); + assert.ok(table.size > 1000, `SDK 名表应有一千条以上,实际 ${table.size}`); + + const used = scanSysResources(files); + const bad = used.filter(u => table.get(u.name) !== u.type); + assert.deepEqual( + bad.map(u => `${u.file}: sys.${u.type}.${u.name}(表里是 ${table.get(u.name) ?? '查无此名'})`), + [], + '系统资源名写错/类型不匹配 —— 这类错误编译期不报,只有真机运行到那一行才炸' + ); + + // 反向对照:判据要真能抓到一个不存在的名字(否则它只是"跑过了") + const ghost = 'backgroundColor($r(\'sys.color.ohos_id_color_这个词不存在\'))'; + const ghostName = ghost.match(/sys\.color\.([A-Za-z0-9_]+)/)[1]; + assert.notEqual(table.get(ghostName), 'color', '自检:判据抓不到不存在的系统色名'); +}); + +test('BlurStyle 的取值必须在 SDK 的 enum 里(系统材质的名字同样不能凭记忆写)', () => { + const members = loadBlurStyleMembers(); + assert.ok(members.size >= 8, `BlurStyle 应有 8 个以上成员,实际 ${members.size}`); + + const used = scanBlurStyles(collectSources(ETS_DIR)); + const bad = used.filter(u => !members.has(u.member)); + assert.deepEqual( + bad.map(u => `${u.file}: BlurStyle.${u.member}`), + [], + `这些 BlurStyle 取值在 SDK 里不存在,可用:${[...members].join(' / ')}` + ); + assert.ok(members.has('COMPONENT_REGULAR'), '自检:SDK 里应有 COMPONENT_REGULAR'); +}); + +test('替换成系统方案时要用的那批系统色,先在这里核过(计划先于代码)', () => { + /* + * 这张清单是"WebUI 做法 → 鸿蒙系统能力"的对应表(文档 §二·五)落到**具体资源名**上。 + * 先在这里核一遍再动代码:名字我核过了,替换时就不会一边写一边猜。 + * + * ⚠️ 注意表里**没有** brand / confirm / success 这类名字 —— 系统只给"情绪色" + * (warning / alert)。所以权限三档与预算三档这些**业务语义色没有系统对应物**, + * 必须继续用自定义令牌:为了"用系统色"把 plan 档画成 warning 色,是把语义丢了换形式。 + */ + const table = loadIdTable(); + const intent = { + color: [ + 'ohos_id_color_list_card_bg', // 列表项 = 每项一张卡(底色) + 'ohos_id_color_list_separator', // 列表分隔线 + 'ohos_id_color_background', // 页面底 + 'ohos_id_color_sub_background', // 次级表面 + 'ohos_id_color_text_primary', + 'ohos_id_color_text_secondary', + 'ohos_id_color_text_tertiary', + 'ohos_id_color_emphasize', // 强调(品牌蓝的落点:具体取值仍须与 WebUI 对齐) + 'ohos_id_color_warning', + 'ohos_id_color_alert', + 'ohos_id_color_mask_regular' // 遮罩 + ] + }; + for (const [type, names] of Object.entries(intent)) { + for (const n of names) { + assert.equal(table.get(n), type, `计划要用的 sys.${type}.${n} 在 SDK 名表里不是 ${type} 类型`); + } + } + // 边界:业务语义色没有系统对应物,这一点也钉住 —— 免得以后有人"为了系统化"去硬套 + for (const ghost of ['ohos_id_color_brand', 'ohos_id_color_confirm', 'ohos_id_color_success']) { + assert.equal(table.get(ghost), undefined, `${ghost} 竟然存在了?那这条边界要重新核一遍`); + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index b712422..a359a2b 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -39,6 +39,7 @@ const SUITE = [ ['test/background.test.mjs', []], ['test/cross-client-theme.test.mjs', ['--test']], ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']], + ['test/harmony-system-api.test.mjs', ['--test']], ['test/build-stamp.test.mjs', ['--test']], ['test/packaging.test.mjs', []] ]; diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index 201f478..53e757a 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -463,3 +463,23 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 **玻璃只出现在一层**(不嵌套各自加模糊)—— 收件箱现在的通栏行 + 分隔线要改成卡片/气泡,这项排在系统材质替换之前做, 因为它决定组件结构。 + +### 7.11 「用系统方案」的第一块地基:系统资源名**离线可校验**(判据先于替换) + +没有设备,`$r('sys.color.写错了')` 编译期不报、只有真机运行到那一行才炸 —— +"用系统方案"会变成一块谁都验不了的区域。所以先把校验立起来: +`client/electron/test/harmony-system-api.test.mjs`(4 条,已接进 `run-all`): + +- 源码里每个 `$r('sys..')` 都必须在 SDK 名表 + `sdk/default/openharmony/toolchains/id_defined.json` 里**存在且类型相符**; +- 每个 `BlurStyle.` 都必须在 SDK 的 `declare enum BlurStyle` 里; +- 替换计划里要用的那批系统色**先核过再写代码**(`ohos_id_color_list_card_bg`、 + `_list_separator`、`_background`/`_sub_background`、`_text_primary/secondary/tertiary`、 + `_emphasize`、`_warning`、`_alert`、`_mask_regular`); +- 名表里没有 `brand` / `confirm` / `success` —— 这条**边界也钉住**: + 权限三档、预算三档这些**业务语义色没有系统对应物**,继续用自定义令牌, + 不许"为了系统化"把 plan 档画成 warning 色。 + +两条纪律写进判据本身:SDK 名表找不到时**判红并说明**(会静默跳过的判据等于没有), +以及变异验证(拼错色名 `list_cad_bg` → 报"查无此名";写错 `BlurStyle.COMPONENT_不存在` → 报可用取值; +名字解析精确到 `,`/`=` 分隔符 —— 早先的宽松写法把文档里的 `T`、`R` 也算成了枚举成员)。