From 3123d83979934b09ba0f9517bcbc9431e36261e4 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 19:39:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A4=E6=8D=AE:=20=E6=8A=8A=E3=80=8C?= =?UTF-8?q?=E9=9D=A2=E5=BD=93=E5=AD=97=E3=80=8D=E7=9A=84=E9=98=B2=E7=BA=BF?= =?UTF-8?q?=E4=BB=8E=E6=AD=BB=E5=90=8D=E5=8D=95=E6=94=B9=E6=88=90=E5=88=A4?= =?UTF-8?q?=E7=9F=9B=E7=9B=BE=EF=BC=88=E4=B8=89=E6=AC=A1=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=E9=83=BD=E8=AE=B0=E4=B8=8B=E6=9D=A5=E4=BA=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版加的静态防线只 grep `fontColor(Theme.surface)` —— **一个名字**。 那是死名单:以后加了新令牌它不会跟上(本仓已记录过多次这类错法)。 改成从源码推: **条件 A**:值是 `Resource` 且指向 `sys.color.*` ⇒ **跟随系统主题翻转** **条件 B**:既当 `backgroundColor` 又当 `fontColor/iconColor` ⇒ **被当成「面」也被当成「字」** 判决 = **A ∩ B**,不需要维护任何名单。 ## 三次迭代(每次错得很典型,所以都写进注释了) - **① 死名单**(只盯 `surface`):新增令牌不会跟上。 - **② 只要 A**("是翻转的 Resource 就不许当前景色"):**太宽** —— 实测立刻 报出 20+ 处"违规",全是 `textMuted` / `textSubtle` / `textPrimary`。 那些**本来就该**当前景色(语义就是文字色,跟随主题翻转是对的)⇒ 假红一片。 - **④ 只要 B**("矛盾就报"):**也宽** —— `accent` / `danger` / `approve` / `warnFg` 两边都用是正常设计(蓝底白字主按钮 + 白底蓝字返回箭头)。 和 `surface` 的**关键区别**:那些是**饱和的字符串常量**(不翻转、语义是"色"), 而 `surface` 是**中性面 + 跟随主题翻转**(当字色 = 把底板当墨水,深色下必然看不见)。 - **最终 = A ∩ B**:恰好命中 `surface` 这一类,且**不随新增令牌失效**。 允许清单现在是**空表**(修完 `surface` 后没有例外), 并写明「已审过、不需要进这张表的」那几个令牌与理由。 ## 变异验证(两个都做了,第二个是关键) 1. 把一处 `.accentFg` 改回 `.surface` ⇒ **判红**;还原 ⇒ 绿。 2. **造一个判据从没见过的新令牌**(`surfaceAlt`,同样 `Resource` + `sys.color.*`), 让它同时当背景与前景 ⇒ **判红**(报文里点名 `Theme.surfaceAlt`);还原 ⇒ 绿。 —— 这条比 ① 重要:它证明判据**不是靠记住 `surface` 这个名字**在工作。 ## 顺带确认 `pageBg` / `surfaceMuted` / `wallpaperScrim` / `border` / `overlay` 这五个 同类"会翻转的面"**一处都没被当字色用过** —— 这一类现在全清了。 `run-all.mjs` → `checks=513 pass=513 fail=0 skip=0 red=0 broken=0 unreported=0`。 --- .../electron/test/cross-client-theme.test.mjs | 106 ++++++++++++++---- 1 file changed, 87 insertions(+), 19 deletions(-) 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')); });