test(harmony): 判据按 pi 复核意见补强 —— 手写色登记表、玻璃叠用形状、按标题取小节、遮罩必须被用、废弃 API 清单从 SDK 生成
pi 逐行读了 `cross-client-theme.test.mjs` 与 `Theme.ets` 后指出五处(第一处是真缺口), 外加一条建议。全部处理,并且**每一处都用变异验证过**。 ## 一(真缺口):枚举 11 个"必须是系统资源"的名字,挡不住第 12 个新写死的手写色 `static readonly brandSecondary: string = '#123456'` 这种新增**三条判据都碰不到**: A(不在名单里)、B(六位、不是半透明)、裸色值那条(只管 `pages/`)。 原理与当初 14 处 Google 色逃出去是同一条 —— **枚举挡实例,类才挡漂移**, 只是这次枚举的单位是**名字**。补 `A2` 条: - 枚举 `Theme.ets` 里所有 `static readonly X: string = '#……'`,未登记的 → 红; - 名单里已不存在的名字 → 红(名单不能烂成化石); - `Theme.ets` 里的「手写色登记表」段必须逐个列出这些名字(理由不能只存在于记忆里); - 自检:把一个**新的**手写色塞进源码字符串,确认这条抓得到。 `Theme.ets` 因此新增登记表(17 项,每项一行理由,按品牌 / 业务语义 / 档位胶囊分组)。 变异:Theme.ets 加 `brandSecondary` → 红。 ## 二:C 的"玻璃只有一处"**说错了自己断言的东西** `assert.equal(glassCalls.length, 1)` 断言的是"全仓共一处",**不是**"没有嵌套": 同页两个**并列**玻璃面(没问题)会让它红,真嵌套它没在判。P5 正是悬浮玻璃导航, 到时若把 1 改成 2 就等于不判。改成形状判据: - 收集每个调用点所作用的**组件块**(按括号配对回溯;注意 ArkUI 修饰符是链式的, `X.blur(A).blur(B)` 前面是 `)` 不是 `}` —— 第一版只看一个字符,对这种写法**静默失效**, 是判据自检抓出来的); - 判两种叠法:同一组件上叠多次(同一块)、以及套在另一层玻璃的子树里; - 每一处玻璃都要在 `GLASS_REGISTRY` 里登记(附一句为什么),名单里的位置若已不存在 → 红。 变异:链式叠两层 → 红;通信页多开一处未登记玻璃 → 红;导航条块内嵌玻璃 → 红。 ## 三:文档小节用 `indexOf('有意差异')` 会**拿错段落** 别的段落正文里出现这四个字,切片就从那里开始,后面所有断言都在**别的段落**上判。 改成**按标题**定位(正则匹配 `^#{2,4}…有意差异…$`),并加了表头列名断言 (WebUI / 鸿蒙 / 为什么)—— 拿错段落时表头不会是这个形状,于是它自己会红。 顺带修掉一个**我自己的**同类毛病:品牌色那行原来用"关键词 + 80 字符窗口"判, 窗口宽度在赌表格单元格字符数(该行两格之和 > 80)。改成**按行取**那一行再断言。 另外把偏移算术的切片换成**按行**切片(偏移差一个字符就会把最后一行拦腰截断, 现象是"品牌色那行只剩 57 字符"这种看着像文案、其实像切片的怪事)。 变异:文件前面插入含「有意差异」的段落 → 仍绿(按标题定位生效); 再改坏真表里的品牌色行 → 红(确实读的是那一张表)。 ## 四:`Theme.overlay` 只判了"存在",没判"被用" 没使用点的令牌是自证。补断言:它必须在 `Theme.ets` **之外**有真实使用点 (实测 `SettingsPage.ets` / `MailDetailPage.ets` 两处自绘弹层在用), 并把 `overlayColor`/`overlayAlpha` 消失的理由写进 §7.12 —— 否则下一个人会当成漏改补回来。 ## 五:材质档次是这次替换里唯一"判据绿但可能观感错"的地方 `COMPONENT_THICK` 的依据(对 THIN / BACKGROUND_* / ULTRA_THICK 的取舍)写进 §7.12, 并把"材质档次在导航条上的实际观感"列为**模拟器起来后第一个要看的项**(间距是数字,材质是判断)。 ## 六(建议):废弃 API 判据**类化** —— 清单从 SDK 生成 原来是手写"不得再用全局 `promptAction.showToast`"(只挡已踩过的那个)。 现在从 SDK 生成:顶层(花括号深度 0)被标 `@deprecated` 的 `declare function` —— 实测 68 个名字(含 `animateTo` / `getContext` / `px2vp`),配一份**空**的 allow-list。 深度判定是必要的:`declare namespace fileIo { declare function open() }` 里的 `open` 是命名空间成员,算进来会造一堆假红。只算全局调用(排除 `x.name(`)。 变异:调 `px2vp(10)` → 红;`this.px2vp(10)`(成员调用)→ 绿(假阳性自检)。 ## 验证 `hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0 (11 个判据文件全绿 + vitest 258/258)。cross-client-theme 13 → 14 条,harmony-system-api 4 → 5 条。
This commit is contained in:
@ -37,12 +37,34 @@ const rawColors = src => code(src).match(RAW_COLOR) || [];
|
||||
* (来源必须是系统资源 / 品牌色必须是那个值),文档只做第三只手。
|
||||
*/
|
||||
const plan = readFileSync(join(ROOT, 'docs/HARMONY-ALIGN-PLAN.md'), 'utf8');
|
||||
/**
|
||||
* 取「有意差异」那一小节。
|
||||
*
|
||||
* ⚠️ **按标题定位,不按关键词**(pi 读出来的第三条):
|
||||
* 原来写的是 `plan.indexOf('有意差异')` —— 只要别的段落正文里出现过这四个字,
|
||||
* 切片就从**那一处**开始,后面的 `includes('圆角')`、行数断言全是在**别的段落**上判,
|
||||
* 可能照样绿。这个坑在本仓库记过一次:`index.css` 的注释里写了深色选择器字面量,
|
||||
* `theme.test.mjs` 的 `indexOf` 就被提前截断(那条注释现在还留着当教训)。
|
||||
*/
|
||||
const diffSection = () => {
|
||||
const at = plan.indexOf('有意差异');
|
||||
assert.ok(at > 0, '文档里应有「有意差异」表(§7.12)');
|
||||
const rest = plan.slice(at);
|
||||
const end = rest.search(/\n#{2,3} /);
|
||||
return end === -1 ? rest : rest.slice(0, end);
|
||||
const lines = plan.split('\n');
|
||||
const start = lines.findIndex(l => /^#{2,4}\s/.test(l) && l.includes('有意差异'));
|
||||
assert.ok(start >= 0, '文档里应有「有意差异」小节(§7.12),且要是一行标题');
|
||||
const headLevel = (lines[start].match(/^#+/) || ['#'])[0].length;
|
||||
/*
|
||||
* 用**行**切,不用字符偏移。
|
||||
*
|
||||
* 第一版是算偏移的(`acc += 每行长度`,再 `slice`),而偏移算术一旦差一个字符
|
||||
* 就会把最后一行**拦腰截断** —— 表现是"品牌色那一行只剩 57 个字符、找不到'不允许差异'"
|
||||
* 这种看着像文案问题、其实是切片问题的怪现象。行切法没有这种自由度。
|
||||
*/
|
||||
const body = [];
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
const h = lines[i].match(/^(#{1,6}) /);
|
||||
if (h && h[1].length <= headLevel) break;
|
||||
body.push(lines[i]);
|
||||
}
|
||||
return body.join('\n');
|
||||
};
|
||||
|
||||
/** 递归收集鸿蒙源码(.ets / .ts) */
|
||||
@ -176,6 +198,72 @@ test('A|系统拥有的维度,鸿蒙侧的唯一来源是系统资源(不
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Theme.ets 里**允许自己写**的色值名单(与文件里的「手写色登记表」逐字一致)。
|
||||
*
|
||||
* pi 读出来的真缺口:A 条只枚举了"**必须**来自系统资源"的 11 个名字,
|
||||
* 于是新加一个手写色(`static readonly brandSecondary: string = '#123456'`)
|
||||
* **三条判据都碰不到它** —— A(不在名单里)、B(六位、不是半透明)、
|
||||
* 裸色值那条(只管 `pages/` 目录)。
|
||||
*
|
||||
* 「枚举挡实例,类才挡漂移」:这次的枚举单位是**名字**,所以名单本身就是防线。
|
||||
* 想加一个手写色 → 先登记在这里 + 在 `Theme.ets` 的登记表里写一行理由;
|
||||
* 否则它本来就该走 `$r('sys.*')`。
|
||||
*/
|
||||
const SELF_OWNED_COLORS = [
|
||||
// 品牌(跨客户端身份,必须与 WebUI 逐字一致的那一个 + 它的前景/浅底/深色变体)
|
||||
'accent', 'accentFg', 'accentSoft', 'accentStrong',
|
||||
// 业务语义色:系统没有对应物(同意 / 拒绝 / 警示)
|
||||
'approve', 'danger', 'approveBg', 'approveFg', 'dangerBg', 'warnBg', 'warnFg',
|
||||
// 权限档位与预算档位的胶囊配色(档位是产品语义,系统不认识"plan/workspace/full")
|
||||
'chipNeutralBg', 'chipNeutralFg', 'chipSpentBg', 'chipSpentFg', 'chipWarnBg', 'chipWarnFg'
|
||||
];
|
||||
|
||||
test('A2|Theme.ets 里"自己写的色"必须**登记过**:新写死一个色不该默默通过', () => {
|
||||
const themeSrc = code(harmony);
|
||||
const declared = [...themeSrc.matchAll(/static readonly (\w+): string = '(#[0-9A-Fa-f]{6,8})'/g)].map(m => m[1]);
|
||||
assert.ok(declared.length >= 10, `要从 Theme.ets 里读到那些手写色,实际读到 ${declared.length} 个`);
|
||||
|
||||
// ① 未登记的手写色 → 红(这是补上的那一枪)
|
||||
const extra = declared.filter(n => !SELF_OWNED_COLORS.includes(n));
|
||||
assert.deepEqual(extra, [],
|
||||
`Theme.ets 里出现未登记的手写色:${extra.join('、')} —— ` +
|
||||
'属于品牌/业务语义色就登记进 SELF_OWNED_COLORS 并在 Theme.ets 的登记表里写一行理由,否则该走 $r(\'sys.*\')');
|
||||
|
||||
// ② 名单不能烂成"曾经"的化石:登记了却已经不存在的名字 → 红
|
||||
const stale = SELF_OWNED_COLORS.filter(n => !declared.includes(n));
|
||||
assert.deepEqual(stale, [], `名单里这些名字在 Theme.ets 里已不存在(名单要跟着改):${stale.join('、')}`);
|
||||
|
||||
/*
|
||||
* ③ 登记表本身要写在文件里(理由靠记忆是不可靠的,要靠登记):
|
||||
* 每个登记项都要在那一段**注释**里出现 —— 所以这里读**原文**(`harmony`),
|
||||
* 不是剥过注释的源码(剥注释会把登记表本身剥掉,第一版就踩了这个:
|
||||
* `indexOf` 返回 -1,切片拿到一段不相干的东西)。
|
||||
*/
|
||||
const regStart = harmony.indexOf('手写色**登记表**');
|
||||
assert.ok(regStart > 0, 'Theme.ets 里要有「手写色登记表」那一段');
|
||||
const regEnd = harmony.indexOf('品牌色:跨客户端身份', regStart);
|
||||
assert.ok(regEnd > regStart, '登记表那一段要有明确的结束边界(下一个分节标题)');
|
||||
const registry = harmony.slice(regStart, regEnd);
|
||||
assert.ok(registry.length > 200, `登记表太短,可能切错了段落(${registry.length} 字符)`);
|
||||
for (const name of SELF_OWNED_COLORS) {
|
||||
assert.ok(registry.includes(name), `登记表里要提到 ${name}(否则理由只存在于写它那个人的记忆里)`);
|
||||
}
|
||||
|
||||
/*
|
||||
* 自检:把一个**新的**手写色塞进去,确认①真的抓得到。
|
||||
* 这条自检是"判据能判红"的证据(不依赖外部变异测试也能看出它有效)。
|
||||
*/
|
||||
const mutated = themeSrc.replace(
|
||||
"static readonly accent: string = '#2563EB';",
|
||||
"static readonly accent: string = '#2563EB';\n static readonly brandSecondary: string = '#123456';"
|
||||
);
|
||||
assert.notEqual(mutated, themeSrc, '自检:变异没打上');
|
||||
const mutatedNames = [...mutated.matchAll(/static readonly (\w+): string = '(#[0-9A-Fa-f]{6,8})'/g)].map(m => m[1]);
|
||||
const mutatedExtra = mutatedNames.filter(n => !SELF_OWNED_COLORS.includes(n));
|
||||
assert.deepEqual(mutatedExtra, ['brandSecondary'], '自检:新加的手写色必须被判据抓到');
|
||||
});
|
||||
|
||||
test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与 WebUI 绑死的圆角数字', () => {
|
||||
const codeOnly = code(harmony);
|
||||
/*
|
||||
@ -198,27 +286,148 @@ test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与
|
||||
assert.ok(!/radiusControl: number = 8/.test(codeOnly), 'radiusControl 又变回写死的 8 了');
|
||||
});
|
||||
|
||||
test('C|玻璃位置必须用系统材质,而且只出现在一层', () => {
|
||||
/**
|
||||
* 允许出现玻璃的位置**名单**(改判据时一起改这里)。
|
||||
*
|
||||
* pi 读出来的第二条:原来那条判据写的是 `assert.equal(glassCalls.length, 1)`,
|
||||
* 断言的是"全仓一共一处 `backgroundBlurStyle`" —— 它**不是**"没有嵌套":
|
||||
* 同一页面上两个**并列**的玻璃面(不嵌套,没问题)会让它红,而真正的嵌套它没在判。
|
||||
* 现在只有导航条一处,所以红得对;但 P5 正是"悬浮玻璃导航",很可能撞上第二处。
|
||||
*
|
||||
* 到那时要**改判定形状**,不是把 1 改成 2 —— 改成 2 这条就退化成"最多两处"(等于不判)。
|
||||
* 所以现在就把形状改成它真正想说的两件事:
|
||||
* ① **不许嵌套**(模糊叠模糊,视觉上互相打架、性能也白花);
|
||||
* ② 每一处玻璃都要**登记**(新开一处玻璃面必须显式过一道,而不是悄悄多出来)。
|
||||
*/
|
||||
const GLASS_REGISTRY = [
|
||||
// 文件(相对 ets 根) + 组件/Builder 名:为什么这里可以有一层系统材质
|
||||
{ file: 'pages/MainPage.ets', provider: 'TabBarBuilder', why: '底部导航栏要浮在内容之上(P5 的悬浮导航)' }
|
||||
];
|
||||
|
||||
/**
|
||||
* 往回找"这次材质作用在哪个组件块上",返回块尾 `}` 的下标(找不到返回 -1)。
|
||||
*
|
||||
* ⚠️ 这里**不能只看前一个字符是不是 `}`**:ArkUI 的修饰符是链式的,
|
||||
* `Row() { ... }.backgroundColor(x).backgroundBlurStyle(A)` 里 `backgroundBlurStyle` 前面是 `)`。
|
||||
* 第一版就只看了一个字符,于是这种写法下"嵌套"检查**静默失效**(判 get 到 null 就放过去了)——
|
||||
* 这正是"断言形状和它声称的东西不是一回事"那一类毛病,判据自检把它抓了出来。
|
||||
* 现在往回扫时跳过成对的括号组(含修饰符参数),遇到 `}` 才算块尾。
|
||||
*/
|
||||
const blockEndBefore = (src, at) => {
|
||||
let i = at - 1;
|
||||
let depth = 0;
|
||||
while (i >= 0) {
|
||||
const c = src[i];
|
||||
if (c === ')') { depth++; i--; continue; }
|
||||
if (c === '(') { depth--; i--; continue; }
|
||||
if (depth === 0) {
|
||||
if (c === '}') return i;
|
||||
if (c === '{' || c === ';') return -1; // 走到了别的结构:这次材质没作用在块上
|
||||
}
|
||||
i--;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/** 用花括号配对取 span(剥过注释的源码上做;字符串里的花括号在这份代码里不出现) */
|
||||
const braceSpans = (src) => {
|
||||
const spans = [];
|
||||
const stack = [];
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (c === '{') stack.push(i);
|
||||
else if (c === '}' && stack.length) spans.push([stack.pop(), i]);
|
||||
}
|
||||
return spans;
|
||||
};
|
||||
|
||||
test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(形状判据,不是数数)', () => {
|
||||
/*
|
||||
* 「用系统方案」里最容易被写歪的一处:`#B8FFFFFF` 看着也能出玻璃效果,
|
||||
* 但它不跟随深色模式、也不跟随系统的模糊半径。所以钉两件事:
|
||||
* ① 材质档次来自 `BlurStyle`(系统枚举),② 应玻璃化的位置真的调了 `backgroundBlurStyle`。
|
||||
* 另外**只许一层** —— 嵌套各加一层模糊是 pi 点名要避免的(视觉上会互相打架)。
|
||||
* 但它不跟随深色模式、也不跟随系统的模糊半径。所以钉三件事:
|
||||
* ① 材质档次来自 `BlurStyle`(系统枚举);② 该玻璃化的位置真的调了 `backgroundBlurStyle`;
|
||||
* ③ **不许叠**(同一组件叠两次 / 套在另一层玻璃的子树里)+ 每一处都登记。
|
||||
*
|
||||
* ③ 的形状是 pi 读出来的第二条:原来写的是 `assert.equal(glassCalls.length, 1)` ——
|
||||
* 那断言的是"全仓一共一处",**不是**"没有嵌套":同一页面两个**并列**玻璃面会让它红
|
||||
* (并列本身没问题),而真正的嵌套它没在判。P5 正是"悬浮玻璃导航",很可能撞上第二处;
|
||||
* 到那时若把 1 改成 2,这条就退化成"最多两处"(等于不判)。所以现在就把形状改对。
|
||||
*/
|
||||
const codeOnly = code(harmony);
|
||||
assert.match(codeOnly, /static readonly navMaterial: BlurStyle = BlurStyle\.[A-Z_]+/, '导航材质要声明成系统材质档次');
|
||||
const navMaterial = codeOnly.match(/navMaterial: BlurStyle = BlurStyle\.([A-Z_]+)/)[1];
|
||||
assert.notEqual(navMaterial, 'NONE', 'NONE 等于没有材质,"玻璃"就名存实亡');
|
||||
|
||||
const ets = collectEts(join(ROOT, 'client/harmony/entry/src/main/ets'));
|
||||
const glassCalls = [];
|
||||
for (const f of ets) {
|
||||
const etsRoot = join(ROOT, 'client/harmony/entry/src/main/ets');
|
||||
/** 每个调用点:哪一处(文件#组件)、调用下标、它作用的**组件块**(配对出来的) */
|
||||
const found = [];
|
||||
for (const f of collectEts(etsRoot)) {
|
||||
const src = code(readFileSync(f, 'utf8'));
|
||||
for (const m of src.matchAll(/backgroundBlurStyle\(/g)) glassCalls.push(f);
|
||||
const spans = braceSpans(src);
|
||||
for (const m of src.matchAll(/backgroundBlurStyle\(/g)) {
|
||||
const at = m.index;
|
||||
const bEnd = blockEndBefore(src, at);
|
||||
const span = bEnd >= 0 ? spans.find(sp => sp[1] === bEnd) : undefined;
|
||||
const owner = [...src.slice(0, at).matchAll(/(?:struct|@Builder\s+)\s*(\w+)/g)].pop();
|
||||
found.push({
|
||||
key: `${f.slice(etsRoot.length + 1)}#${owner ? owner[1] : '(未识别)'}`,
|
||||
at,
|
||||
block: span ? { start: span[0], end: span[1] } : null
|
||||
});
|
||||
}
|
||||
assert.equal(glassCalls.length, 1, `玻璃应只出现在一处,实际 ${glassCalls.length} 处:${glassCalls.join('、')}`);
|
||||
}
|
||||
assert.ok(found.length > 0, '至少导航条要用系统材质(不是手写 alpha)');
|
||||
|
||||
/*
|
||||
* 叠用判定(两种形态都判红):
|
||||
* · 同一组件:两处调用解析到**同一个块**(`X.blur(A).blur(B)` 就是这种,ArkUI 的修饰符链
|
||||
* 作用在同一个节点上 —— 注意它前面是 `)` 不是 `}`,所以按字符相邻判会漏);
|
||||
* · 子树:一处的调用点落在另一处的块**内部**。
|
||||
*/
|
||||
const doubled = new Set();
|
||||
const nested = new Set();
|
||||
for (const c of found) {
|
||||
for (const o of found) {
|
||||
if (o === c) continue;
|
||||
if (c.block && o.block && c.block.start === o.block.start && c.block.end === o.block.end) {
|
||||
const pair = [c.at, o.at].sort((a, b) => a - b).join('-');
|
||||
doubled.add(`${c.key}@${pair}`);
|
||||
} else if (c.block && o.at > c.block.start && o.at < c.block.end) {
|
||||
nested.add(`${c.key}(内含 ${o.key} 的玻璃)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...doubled], [], `同一个组件上不该叠多层模糊:${[...doubled].join('、')}`);
|
||||
assert.deepEqual([...nested], [], `玻璃不该套在另一层玻璃的子树里:${[...nested].join('、')}`);
|
||||
|
||||
// 每一处都要登记;名单里也不能有已经不存在的位置(否则名单会烂成化石)
|
||||
const keys = found.map(c => c.key);
|
||||
const registered = GLASS_REGISTRY.map(g => `${g.file}#${g.provider}`);
|
||||
const unregistered = keys.filter(k => !registered.includes(k));
|
||||
assert.deepEqual(unregistered, [],
|
||||
`这些位置开了玻璃但没登记:${unregistered.join('、')} —— ` +
|
||||
'要开新的玻璃面就在 GLASS_REGISTRY 里登记(附一句为什么),否则该用普通系统背景色');
|
||||
const stale = registered.filter(k => !keys.includes(k));
|
||||
assert.deepEqual(stale, [], `名单里这些位置已经没有玻璃了(名单要跟着改):${stale.join('、')}`);
|
||||
for (const g of GLASS_REGISTRY) {
|
||||
assert.ok(g.why && g.why.length >= 8, `GLASS_REGISTRY 里 ${g.file}#${g.provider} 要写一句为什么可以在这里开玻璃`);
|
||||
}
|
||||
|
||||
// 导航条那一处必须真的还在(形状判定之外,位置本身也要在)
|
||||
const main = code(readFileSync(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets'), 'utf8'));
|
||||
assert.match(main, /\.backgroundBlurStyle\(Theme\.navMaterial\)/, '导航条要用系统材质(不是手写 alpha)');
|
||||
|
||||
/*
|
||||
* 自检:造一次**链式叠用**(`X.blur().blur()`),确认上面的判定抓得到。
|
||||
* 这一枪放在判据里,是为了以后改这段扫描逻辑时它自己会被检验 ——
|
||||
* 第一版只看"前一个字符是不是 }",对这种写法**静默失效**,正是这条自检抓出来的。
|
||||
*/
|
||||
const sample = 'Row() { Text("x") }\n .backgroundBlurStyle(Theme.navMaterial)\n .backgroundBlurStyle(Theme.navMaterial)';
|
||||
const sampleEnds = [...sample.matchAll(/backgroundBlurStyle\(/g)].map(m => blockEndBefore(sample, m.index));
|
||||
assert.ok(sampleEnds.every(e => e >= 0), '自检:链式写法要能解析出所作用的块');
|
||||
assert.equal(new Set(sampleEnds).size, 1, '自检:同一组件的两处调用必须解析到同一个块(否则叠用判不出来)');
|
||||
const sampleSpans = braceSpans(sample);
|
||||
assert.ok(sampleSpans.some(sp => sp[1] === sampleEnds[0]), '自检:块的配对要能对上');
|
||||
});
|
||||
|
||||
test('★ 品牌色防线:主操作色不得退化成系统强调色', () => {
|
||||
@ -308,6 +517,23 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在
|
||||
// 不许再自己维护"色 + 透明度"两个常量(那正是系统已经替我们做掉的事)
|
||||
assert.ok(!/overlayColor: string/.test(code(harmony)), '遮罩色不该再由我们自己定');
|
||||
assert.ok(!/overlayAlpha: number/.test(code(harmony)), '遮罩透明度不该再由我们自己定');
|
||||
|
||||
/*
|
||||
* pi 读出来的第四条:判据只断言 `overlay` **存在**,没断言它**被用** ——
|
||||
* 立一个没人用的令牌是自证(判据只能验"它还在",验不了它有用)。
|
||||
* 所以这里补一条:它必须在 `Theme.ets` **之外**有真实使用点,
|
||||
* 否则要么删掉、要么说明它为什么该留着。
|
||||
*/
|
||||
const etsRoot = join(ROOT, 'client/harmony/entry/src/main/ets');
|
||||
const themePath = join(HARMONY_ETS, 'common/Theme.ets');
|
||||
const overlayUsers = collectEts(etsRoot)
|
||||
.filter(f => f !== themePath)
|
||||
.filter(f => /Theme\.overlay\b/.test(code(readFileSync(f, 'utf8'))))
|
||||
.map(f => f.slice(etsRoot.length + 1));
|
||||
assert.ok(overlayUsers.length > 0,
|
||||
'Theme.overlay 声明了却没有任何使用点 —— 那就是个死令牌(要么删掉,要么写出它的使用处)');
|
||||
// 用它的必须是**自绘遮罩**的地方(系统自带遮罩的弹窗不需要它)
|
||||
assert.ok(overlayUsers.every(f => f.endsWith('.ets')), `遮罩使用点应该是页面:${overlayUsers.join('、')}`);
|
||||
assert.ok(!/#[0-9A-Fa-f]{8}/.test(code(harmony)), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
|
||||
// WebUI 侧同构:颜色两套(浅/深,同一个变量名换向)+ 透明度独立
|
||||
assert.match(web, /--bg-scrim: 255 255 255/, 'WebUI 浅色遮罩色');
|
||||
@ -328,7 +554,21 @@ test('C|「有意差异」表列全了允许不同的维度,并点名品牌
|
||||
for (const dim of ['圆角', '材质', '动效', '遮罩']) {
|
||||
assert.ok(section.includes(dim), `「有意差异」表里要列 ${dim}(它现在是"允许不同"的维度)`);
|
||||
}
|
||||
assert.match(section, /品牌色[\s\S]{0,80}不允许差异/, '品牌色必须被点名"不允许差异",否则下一个人会以为它也在表内');
|
||||
/*
|
||||
* 品牌色那一行:**按行取**,不用"关键词 + 窗口"。
|
||||
* 原来写的是 `/品牌色[\s\S]{0,80}不允许差异/` —— 窗口宽度是在赌表格单元格的字符数
|
||||
* (品牌色行里"品牌色"与"不允许差异"隔着 WebUI/鸿蒙 两格,正好 > 80)。
|
||||
* 窗口型断言和 `indexOf` 是同一类毛病:看着断言了,其实在赌排版。
|
||||
*/
|
||||
const brandRow = section.split('\n').find(l => /^\|\s*\**品牌色/.test(l));
|
||||
assert.ok(brandRow, '「有意差异」表里应有品牌色一行(它要显式写明"不允许差异")');
|
||||
assert.match(brandRow, /不允许差异/, '品牌色必须被点名"不允许差异",否则下一个人会以为它也在表内');
|
||||
// 表头列名要对(这也让"拿错段落"自己红出来:拿错段落时表头不会是这个形状)
|
||||
const header = section.split('\n').find(l => l.startsWith('|') && !l.includes('---'));
|
||||
assert.ok(header, '差异表要有表头');
|
||||
for (const col of ['WebUI', '鸿蒙', '为什么']) {
|
||||
assert.ok(header.includes(col), `差异表表头要有「${col}」列,实际:${header}`);
|
||||
}
|
||||
// 反向对照:表里必须真的写了"为什么允许不同",不是只列个名字
|
||||
const rows = section.split('\n').filter(l => l.startsWith('|') && !l.includes('---'));
|
||||
assert.ok(rows.length >= 5, `差异表应有表头 + 至少 4 行,实际 ${rows.length} 行`);
|
||||
|
||||
@ -173,3 +173,108 @@ test('替换成系统方案时要用的那批系统色,先在这里核过(
|
||||
assert.equal(table.get(ghost), undefined, `${ghost} 竟然存在了?那这条边界要重新核一遍`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* ─────────── 废弃 API:**清单从 SDK 生成**,不手写 ───────────
|
||||
*
|
||||
* pi 的建议(2026-09-14):与其一条条手写"不得再用全局 `promptAction.showToast`",
|
||||
* 不如从 SDK 的 `@deprecated` 标记**生成**一份清单,再配一份人工 allow-list ——
|
||||
* 新增一条默认判红,除非有人显式放行。这与 `$r('sys.*')` 名字表的做法是同一个套路。
|
||||
*
|
||||
* 手写清单的死法是"只挡已经踩过的那一个":这次修了 23 处,下一个人写个新的废弃 API
|
||||
* 照样全绿 —— 而 SDK 自己知道哪些废弃了,问它就行。
|
||||
*/
|
||||
|
||||
/** 递归收集 SDK 的 .d.ts(含 api/ 与 component/) */
|
||||
function sdkDtsFiles() {
|
||||
const roots = [
|
||||
join(CLT, 'sdk/default/openharmony/ets/component'),
|
||||
join(CLT, 'sdk/default/openharmony/ets/api')
|
||||
];
|
||||
const out = [];
|
||||
const walk = (dir) => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, e.name);
|
||||
if (e.isDirectory()) walk(full);
|
||||
else if (e.name.endsWith('.d.ts')) out.push(full);
|
||||
}
|
||||
};
|
||||
for (const r of roots) if (existsSync(r)) walk(r);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶层(不在 namespace/class 里)被标 `@deprecated` 的 `declare function` 名字。
|
||||
*
|
||||
* "顶层"这件事必须判:`declare namespace fileIo { declare function open(...) }` 里的
|
||||
* `open` 是**命名空间成员**,全局调 `open(...)` 并不会走到那个废弃实现 ——
|
||||
* 把它算进来会造出一堆假红(这一版就是按花括号深度筛出来的)。
|
||||
*/
|
||||
function deprecatedGlobalFunctions() {
|
||||
const names = new Set();
|
||||
for (const f of sdkDtsFiles()) {
|
||||
const src = readFileSync(f, 'utf8');
|
||||
// 先切出 depth == 0 的片段
|
||||
let depth = 0;
|
||||
let segStart = 0;
|
||||
const segs = [];
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (c === '{') {
|
||||
if (depth === 0) segs.push([segStart, i]);
|
||||
depth++;
|
||||
} else if (c === '}') {
|
||||
depth--;
|
||||
if (depth === 0) segStart = i + 1;
|
||||
}
|
||||
}
|
||||
if (depth === 0) segs.push([segStart, src.length]);
|
||||
const top = segs.map(([a, b]) => src.slice(a, b)).join('\n');
|
||||
for (const m of top.matchAll(/\/\*\*((?:(?!\*\/)[\s\S])*?)\*\/\s*declare function (\w+)\s*\(/g)) {
|
||||
if (m[1].includes('@deprecated')) names.add(m[2]);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
test('废弃 API:清单**从 SDK 生成**,源码里不得调用(新增一条默认判红)', () => {
|
||||
if (missingSdk.length) return; // 没有 SDK:这一条无从判起(由文件顶部统一报)
|
||||
const deprecated = deprecatedGlobalFunctions();
|
||||
assert.ok(deprecated.size > 20, `要从 SDK 读到一批废弃的全局函数,实际 ${deprecated.size} 个`);
|
||||
// 清单真的抓到了我们关心的东西(否则"生成"这件事本身没生效)
|
||||
for (const must of ['animateTo', 'getContext', 'px2vp']) {
|
||||
assert.ok(deprecated.has(must), `清单里应该有 ${must}(它已被 SDK 标记废弃)`);
|
||||
}
|
||||
// 全局 showToast 不是 `declare function`(它是命名空间成员),由 harmony-logic 那条单钉
|
||||
|
||||
/**
|
||||
* 允许的例外:**每条都要写理由**。
|
||||
* 空名单就是"一处都不许"—— 想加就得在这里写明为什么非用不可。
|
||||
*/
|
||||
const ALLOW = [];
|
||||
|
||||
const walk = (dir, acc = []) => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, e.name);
|
||||
if (e.isDirectory()) walk(full, acc);
|
||||
else if (/\.(ets|ts)$/.test(e.name)) acc.push(full);
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
const files = walk(ETS_DIR);
|
||||
const hits = [];
|
||||
for (const f of files) {
|
||||
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const name of deprecated) {
|
||||
// 只算**全局调用**:排除成员调用 `x.name(`(`.animateTo(` 是我们要求的新写法)
|
||||
const re = new RegExp(`(?<![.\\w])${name}\\s*\\(`, 'g');
|
||||
if (re.test(src)) hits.push(`${f.slice(ETS_DIR.length + 1)}: ${name}`);
|
||||
}
|
||||
}
|
||||
const allowed = hits.filter(h => ALLOW.some(a => h.includes(a.name)));
|
||||
const bad = hits.filter(h => !ALLOW.some(a => h.includes(a.name)));
|
||||
assert.deepEqual(bad, [],
|
||||
`这些地方调了 SDK 已标记废弃的**全局**函数:${bad.join('、')} —— ` +
|
||||
'换成新写法(如 UIContext 上的同名方法),或把它登记进 ALLOW 并写明理由');
|
||||
assert.ok(allowed.length === ALLOW.length || allowed.length >= 0, 'allow-list 命中统计');
|
||||
});
|
||||
|
||||
@ -78,6 +78,48 @@ export class Theme {
|
||||
/** 控件圆角:系统"按钮"圆角 */
|
||||
static readonly radiusControl: Resource = $r('sys.float.ohos_id_corner_radius_button');
|
||||
|
||||
/*
|
||||
* ─────────────── 手写色**登记表**(改这里要一起改判据) ───────────────
|
||||
*
|
||||
* 为什么需要这张表:本文件里"自己写的色值"是有理由的(品牌身份 + 系统没有对应物的
|
||||
* 业务语义色),但**理由不能靠记忆**。判据(`cross-client-theme.test.mjs` 的
|
||||
* A2 条)会枚举本文件里所有 `static readonly X: string = '#……'`,
|
||||
* 发现**未登记的名字就判红** —— 于是"新写死一个色"必须显式过一道:
|
||||
* 要么登记在这里并写清理由,要么它本来就该走 `$r('sys.*')`。
|
||||
*
|
||||
* 这条是 pi 读出来的真缺口:只枚举 11 个"必须是系统资源"的名字,挡不住
|
||||
* 第 12 个**新加的手写色**(它不在名单里,于是 A/B/裸色值三条都碰不到它)。
|
||||
* 「枚举挡实例,类才挡漂移」—— 这次枚举的是**名字**,所以要有名单。
|
||||
*
|
||||
* 登记项(17 个,名字与判据里的 SELF_OWNED_COLORS 逐字一致 —— 逐个列出而不是缩写,
|
||||
* 这样 grep 一个名字就能找到它的理由):
|
||||
*
|
||||
* 品牌(跨客户端身份,系统给不了):
|
||||
* · accent #2563EB 与 WebUI 的 --c-blue-600 逐字一致
|
||||
* · accentFg #FFFFFF 品牌底上的文字
|
||||
* · accentSoft #EFF6FF 品牌浅底(选中态背景)
|
||||
* · accentStrong #1D4ED8 品牌深色变体(选中文字/边框)
|
||||
*
|
||||
* 业务语义(系统只有 list/warning/alert 这一档,没有"同意/拒绝"):
|
||||
* · approve #15803D 同意(绿)
|
||||
* · approveBg #F0FDF4 同意底
|
||||
* · approveFg #15803D 同意文字
|
||||
* · danger #B91C1C 拒绝/危险(红)
|
||||
* · dangerBg #FEF2F2 拒绝底
|
||||
* · warnBg #FFFBEB 警示底(对应 WebUI --c-amber-50)
|
||||
* · warnFg #B45309 警示文字(对应 WebUI --c-amber-700)
|
||||
*
|
||||
* 档位胶囊(档位是产品语义:系统不认识 plan / workspace / full):
|
||||
* · chipNeutralBg #F3F4F6 plan 档底(最宽档但不报警)
|
||||
* · chipNeutralFg #5A6270 plan 档文字
|
||||
* · chipSpentBg #FEE2E2 预算耗尽底
|
||||
* · chipSpentFg #B91C1C 预算耗尽文字
|
||||
* · chipWarnBg #FFEDD5 预算紧张底
|
||||
* · chipWarnFg #C2410C 预算紧张文字
|
||||
*
|
||||
* 不在这里的手写色只有一种合法去处:`$r('sys.*')`(跟随系统/深色模式)。
|
||||
*/
|
||||
|
||||
// ─────────────── 品牌色:跨客户端身份,必须自己写 ───────────────
|
||||
|
||||
/**
|
||||
|
||||
@ -501,6 +501,21 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排
|
||||
| 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 |
|
||||
| **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 |
|
||||
|
||||
**关于 `overlayColor` / `overlayAlpha` 消失**(pi 要求把删除理由记在这里,否则下一个人会当成漏改补回来):
|
||||
鸿蒙这边的模态走**系统弹窗**(`bindSheet` / 自绘 `Stack` 只做位置,遮罩本身用系统遮罩色),
|
||||
所以"遮罩色 + 遮罩透明度"这两个自定值**整类**都不需要了 —— 系统遮罩色自带随主题换向
|
||||
(浅色洗白 / 深色压黑)。保留的是**一个** `Theme.overlay`(值 = `sys.color.ohos_id_color_mask_regular`),
|
||||
它是自绘弹层唯一还需要引用的那一个令牌;判据钉两件事:值来自系统、且**在 `Theme.ets` 之外确有使用点**
|
||||
(只有声明没有使用 = 死令牌,pi 点过这条)。
|
||||
|
||||
**材质为什么是 `COMPONENT_THICK`**(pi 指出:判据只能钉"来自系统枚举",钉不了"选对没选对",
|
||||
所以理由要写下来,免得后人以为随便挑的):底部导航栏是**内容之上的一层**,要挡住滚动内容
|
||||
又不至于把内容糊没 —— `COMPONENT_*` 系列是"组件材质"(作用于一个组件表面),
|
||||
其中 THIN 在浅色壁纸上几乎看不出分层(导航条会像没浮起来);BACKGROUND_* 系列是
|
||||
**整窗背景**用的(会把下方内容整体重绘,这里是叠一层而不是换背景,用它会与页面底色打架);
|
||||
ULTRA_THICK 会把导航条底下的内容糊成一块。所以取 COMPONENT_THICK。
|
||||
**这条只有真机能判**,见下面「模拟器起来后第一个要看的项」。
|
||||
|
||||
### 7.13 系统方案替换(第一批)+ 跨端判据从"取值"改"意图"
|
||||
|
||||
**改了什么**(鸿蒙侧):
|
||||
@ -604,7 +619,7 @@ SDK 里写着:`@ohos.promptAction.d.ts` 的全局 `showToast` 标 `@deprecated
|
||||
`COMM_TABS[1.5]` → `undefined`(表现是"点哪都不亮")。范围检查挡不住非整数,
|
||||
已加 `Number.isInteger`。
|
||||
|
||||
**未验**:底部/内部页签在真机上的观感、悬浮加号的位置、徽标与文字的排版 ——
|
||||
**未验**(**模拟器起来后第一个要看的项**:§7.12 里那个材质档次 —— 间距是数字,材质是判断):底部/内部页签在真机上的观感、悬浮加号的位置、徽标与文字的排版 ——
|
||||
仍然只有真机(或模拟器,需人在命令行启动)能看。已验证:构建成功、28 条判据全绿、
|
||||
六种变异都能判红。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user