跨端: 三级文字**浅色也一样不够**(12 处 2.85:1)+ 判据不再只验一半

## 一、这是"我自己推的理由"被实测推翻

上一条我把 `textSubtleFor()` 写成**只改深色**,理由是:

> 「浅色下这些令牌本来就是为浅底配的,量它们没有信息量」

**那个理由是错的。** 切到浅色主题复扫,立刻抓到 12 处:

```
"用户名 / 显示名 / 角色 / 状态 / 创建时间 / 最后登录"  2.85:1
"已同步" / "37%" / "3px"(外观区)                    2.85:1
"这一天没有日程"                                      2.85:1
```
`ink rgb(153,153,153)` 压白底 —— 全是 `textSubtle`。

对照 WebUI 浅色(`index.css` `:root`):
- `gray-400` = `107 114 128` = **4.83:1**
- `gray-500` = `90 98 112`  = **6.15:1**

⇒ 系统三级色**两个主题下都不够**。它的语义只是"比二级更淡",
**不保证任何可读性下限**;WebUI 那边两套主题都做过适配,这边一档都没有。

修:`textSubtleFor()` 两个主题都返回 `textMuted`
(系统二级色,语义就是"次要文字",且**跟随主题** ——
不必再维护两个手写常量、不必登记)。

## 二、判据从"只验深色"改成"两个主题都验"

原来那条设备判据**在浅色下直接 skip**,skip 的理由正是上面那句错话。
现在两个主题都扫,断言消息里带上主题名(`深色主题下…` / `浅色主题下…`)。

★ 纪律:**色令牌的可读性是两个主题各自的事,不能只验一半。**
  "另一半没问题"若没量过,就只是推测。

**变异验证**:把 `textSubtleFor()` 改回"两主题都用 textSubtle"
(此时设备正在浅色主题)⇒ **判红**;还原 ⇒ 绿。

## 三、设备复扫(浅色主题)

```
[通信]   扫 41 段,低对比 0
[日历]   扫 84 段,低对比 0
[联系人] 扫 50 段,低对比 0
[我的]   扫 40 段,低对比 0
```
**215 段文字全部 ≥3:1。**

两个主题合起来:深色 219 段 + 浅色 215 段,全绿。

## 四、顺手改掉的

测试名从「深色下短文本必须都读得动」改成「**当前主题**下…」
—— 名字得跟着它实际判的东西走(否则下一个人会以为它只管深色)。

`run-all.mjs` → `checks=515 pass=515 fail=0 skip=0 red=0 broken=0 unreported=0`。

**当前设备主题**:浅色(本轮为验另一半切过去的;服务端 `theme=light`)。
This commit is contained in:
2026-09-19 20:58:59 +08:00
parent 1da4e15a7b
commit 110de79401
2 changed files with 31 additions and 9 deletions

View File

@ -1318,7 +1318,7 @@ test('C|「会跟随主题翻转的 Resource」不得当前景色(含矛盾
});
test('★ 设备:深色下短文本必须都读得动(扫一屏,不只查一个元素)', async (t) => {
test('★ 设备:当前主题下短文本必须都读得动(扫一屏,不只查一个元素)', async (t) => {
/*
* ★★ 这条是上面"品牌色真的画成那个色"的**一般化**。
*
@ -1357,16 +1357,22 @@ test('★ 设备:深色下短文本必须都读得动(扫一屏,不只查
await new Promise((r) => setTimeout(r, 1500));
/*
* ★ 只在**深色**下判。浅色下这些令牌本来就是为浅底配的,
* 量它们没有信息量(也不会红),但会拖慢判据。
* ★★ 两个主题**都要判**(第一版只判深色,理由写的是"浅色下这些令牌本来就是
* 为浅底配的,量它们没有信息量" —— **那个理由是错的**)。
*
* 切到浅色复扫才发现的实例:浅色 `textSubtle` = `rgb(153,153,153)` 压白底
* = **2.85:1**,而 WebUI 浅色的 gray-400 是 4.83:1。
* ⇒ "浅色下没问题"是我自己推的,不是量出来的。
*
* ★ 纪律:**色令牌的可读性是两个主题各自的事**,不能只验一半。
* 现在把当前主题**记下来**,两个主题都扫;判据只看"有没有低对比",
* 不关心是哪个主题(两种都是错)。
*/
const img0 = D.readPixels(await D.screenshot(hdc, '/tmp/dark-legibility.png'));
const img0 = D.readPixels(await D.screenshot(hdc, '/tmp/theme-legibility.png'));
assert.ok(img0, '要能截屏并解码');
const corner = img0.at(4, 4);
const isDark = corner && corner.r < 128 && corner.g < 128 && corner.b < 128;
if (!isDark) {
return t.skip(`当前不是深色主题(左上角像素 rgb(${corner?.r},${corner?.g},${corner?.b}))—— 本条只在深色下有意义`);
}
const themeName = isDark ? '深色' : '浅色';
const lum = (c) => {
const f = (v) => { const x = v / 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; };
@ -1467,7 +1473,7 @@ test('★ 设备:深色下短文本必须都读得动(扫一屏,不只查
'太少说明遍历或尺寸过滤写错了,那样下面的检查恒绿。');
assert.deepStrictEqual(bad, [],
`★ 深色下这些文字读不动(对比度 <3:1):\n ${bad.join('\n ')}\n` +
`★ ${themeName}主题下这些文字读不动(对比度 <3:1):\n ${bad.join('\n ')}\n` +
' 深色下品牌色/面色都要换深色变体:\n' +
' · 文字/图标用 `Theme.accentFor()`(深色自动给 `#80AFF9`,' +
'对齐 WebUI 的 `.dark --c-blue-600`)\n' +