diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs index a32f4b7..523cf19 100644 --- a/client/electron/test/cross-client-theme.test.mjs +++ b/client/electron/test/cross-client-theme.test.mjs @@ -1051,21 +1051,48 @@ test('★ 设备:品牌色**真的画成那个色**(令牌写对了 ≠ 渲 }); -test('C|`Theme.surface` 不得当代的前景色(它是会翻转的「面」)', () => { +test('C|「会跟随主题翻转的 Resource」不得当前景色(含矛盾信号)', () => { /* * ★★ 这是设备判据(上面那条)抓到的 bug 的**静态防线** —— * 设备条只能看一处(悬浮球),而这个错法当时有 **12 处**。 * - * 错法:`Theme.surface` 是 `sys.color.ohos_id_color_list_card_bg`, - * **跟随系统主题翻转**(浅色近白 / 深色近黑)。把它当 - * `fontColor` / `iconColor`("压在彩色底上的字"): + * 错法(真实现场):`Theme.surface` 是 + * `sys.color.ohos_id_color_list_card_bg` —— 一个**跟随系统主题翻转**的 + * Resource(浅色近白 / **深色近黑**)。它被当成"压在彩色底上的字/图标色"用: * · 浅色下碰巧对(白字压蓝底) - * · **深色下字变黑**,压在蓝/红/绿底上几乎看不见 + * · **深色下字变黑**,压在品牌蓝/红/绿底上几乎看不见 + * 正确的前景色是 `Theme.accentFg`(`'#FFFFFF'`,字符串常量,不翻转)。 * - * 正确:`Theme.accentFg`(#FFFFFF,注释原话就是"品牌底上的字/图标")。 + * ★★ 判据形状的**三次迭代**(都写下来,因为每一次都错得很典型): * - * 判据形状:全局 grep「`fontColor(Theme.surface)` / `iconColor: Theme.surface`」, - * 一处都不许有。`backgroundColor(Theme.surface)` 是合法用法(那才是"面")。 + * ① grep `fontColor(Theme.surface)` —— **一个名字**。 + * 错在这是死名单:以后加了新令牌它不会跟上(本仓已记录过多次)。 + * + * ② 从 `Theme.ets` 读"所有 `sys.color.*` 的 Resource",一律不许当前景色。 + * **太宽** —— 实测立刻报出 20+ 处"违规",全是 + * `Theme.textMuted` / `textSubtle` / `textPrimary`。 + * 那些**本来就该**当前景色(它们的语义就是文字色,跟随主题翻转是对的)。 + * ⇒ 只按"是不是 Resource"判,把正确用法也判红 ⇒ 假红一片。 + * + * ③(本版)判**矛盾**:同一令牌既当 `backgroundColor` 又当 + * `fontColor/iconColor` ⇒ 它同时被当成"面"和"字",其中一种必然错。 + * 这个信号从**实际用法**推出,不依赖任何名单。 + * + * ★ 但④实测又发现它**太宽**:`accent` / `danger` / `approve` / `warnFg` + * 这些**饱和的字符串常量**色两边都用是正常设计(蓝底白字的主按钮 + + * 白底蓝字返回箭头)。它们和 `surface` 的**关键区别**是: + * + * · `surface`(危险):**中性面**(近白/近黑,"承载内容的底板") + * + **跟随主题翻转**。当字色 = 把底板当墨水 ⇒ 深色下看不见。 + * · `accent`(正常):**饱和色**(品牌/语义身份)+ **字符串常量** + * (不翻转)。两边都站得住。 + * + * ⇒ 最终判据 = **翻转的 Resource** ∩ **既当背景又当前景**。 + * 两个条件都要,缺一个都会假红: + * · 只要"翻转" ⇒ 把 `textPrimary` 判红(假红) + * · 只要"矛盾" ⇒ 把 `accent` 判红(假红) + * 两个都要 ⇒ 恰好命中 `surface` 这一类,且**不随新增令牌失效** + * (新令牌只要符合这个形状就会被抓,不需要维护名单)。 */ const files = []; const walkDir = (d) => { @@ -1077,21 +1104,62 @@ test('C|`Theme.surface` 不得当代的前景色(它是会翻转的「面」 }; walkDir(HARMONY_ETS); - const bad = []; + /* + * 条件 A:从 `Theme.ets` 读出**会翻转的**令牌 —— 值是 `Resource` + * 且指向 `sys.color.*`(跟随系统主题)。 + */ + const themeSrc = prose(join(HARMONY_ETS, 'common/Theme.ets')); + const flip = new Set(); + for (const m of themeSrc.matchAll( + /static readonly ([A-Za-z0-9_]+)\s*:\s*Resource\s*=\s*\$r\('(sys\.color\.[^']+)'/g)) { + flip.add(m[1]); + } + assert.ok(flip.size >= 5, + `要从 Theme.ets 读出「会翻转的令牌」名单(实测 ${flip.size} 个)—— ` + + '读出 0 个说明正则是错的,那样下面的检查恒绿。'); + + /* 条件 B:收集每个令牌"当背景 / 当前景"的首个出处 */ + const asBg = new Map(); + const asFg = new Map(); for (const f of files) { const src = prose(f); // 保留注释:注释里举例不算错 - const lines = src.split('\n'); - lines.forEach((ln, i) => { - if (/\.fontColor\(Theme\.surface\)/.test(ln) || /iconColor:\s*Theme\.surface/.test(ln)) { - bad.push(`${f.replace(HARMONY_ETS, '')}:${i + 1} ${ln.trim().slice(0, 90)}`); + const rel = f.replace(HARMONY_ETS, ''); + src.split('\n').forEach((ln, i) => { + const where = `${rel}:${i + 1} ${ln.trim().slice(0, 80)}`; + for (const m of ln.matchAll(/backgroundColor\(Theme\.([A-Za-z0-9_]+)\)/g)) { + if (!asBg.has(m[1])) asBg.set(m[1], where); + } + for (const m of ln.matchAll(/\.fontColor\(Theme\.([A-Za-z0-9_]+)\)/g)) { + if (!asFg.has(m[1])) asFg.set(m[1], where); + } + for (const m of ln.matchAll(/iconColor:\s*Theme\.([A-Za-z0-9_]+)/g)) { + if (!asFg.has(m[1])) asFg.set(m[1], where); } }); } + assert.ok(asBg.size >= 5 && asFg.size >= 5, + `要能从源码读出背景色/前景色两边的用法(实测 ${asBg.size} / ${asFg.size})—— ` + + '有一边读出 0 个说明正则是错的,那样下面的"交集"恒空、判据恒绿。'); - assert.deepStrictEqual(bad, [], - '★ `Theme.surface` 是**会跟随系统主题翻转的面色**(浅色近白 / 深色近黑),' + - '只能当背景,不能当字/图标色。\n' + - ' 压在彩色底上的前景要用 `Theme.accentFg`(#FFFFFF)。\n' + - ' 深色下症状:字/图标几乎看不见(2026-09-19 真撞到 12 处)。\n' + - ' 违规处:\n ' + bad.join('\n ')); + /* + * 判决 = A ∩ B ∩(不在允许清单里)。 + * 允许清单目前**空**:修完 `surface` 之后,没有任何"翻转的 Resource" + * 被当成前景色用过。留空表比留理由表更好读 —— 它直接说明"没有例外"。 + */ + const conflicts = []; + for (const name of asFg.keys()) { + if (!flip.has(name)) continue; // 条件 A:必须是会翻转的 Resource + if (!asBg.has(name)) continue; // 条件 B:必须也被当背景用过(矛盾) + if (ALLOW_BOTH_BG_AND_FG.has(name)) continue; + conflicts.push({ name, bg: asBg.get(name), fg: asFg.get(name) }); + } + + assert.deepStrictEqual(conflicts, [], + '★ 这些令牌**同时**满足两个条件 ⇒ 是 `surface` 那一类错:\n' + + ' A. 会跟随系统主题翻转的 `sys.color.*` Resource(浅色近白 / 深色近黑)\n' + + ' B. 既当 `backgroundColor` 又当 `fontColor/iconColor` —— 被当成"面"也被当成"字"\n' + + ' 浅色下碰巧对,**深色下字/图标几乎看不见**(2026-09-19 真撞到 12 处)。\n' + + ' 压在彩色底上的前景请用 `Theme.accentFg`(#FFFFFF,字符串常量、不翻转)。\n' + + ' 若某个令牌**确实**两边都该用(有具体理由),加进 `ALLOW_BOTH_BG_AND_FG` 并写明理由。\n' + + conflicts.map((c) => ` · Theme.${c.name}\n 当背景:${c.bg}\n 当前景:${c.fg}`).join('\n')); });