From 5e74e0312eef437bc7a35a39368533d656e6bdc8 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 17:05:15 +0800 Subject: [PATCH] =?UTF-8?q?test(=E9=A2=84=E8=AE=BE):=20=E8=A1=A5=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E5=88=A4=E6=8D=AE=20=E2=80=94=E2=80=94=20"=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E9=87=8C=E6=9C=89=E3=80=81=E6=B8=B2=E6=9F=93=E5=99=A8?= =?UTF-8?q?=E6=B2=A1=E6=9C=89"=E5=BF=85=E9=A1=BB=E7=BA=A2=EF=BC=88?= =?UTF-8?q?=E5=80=BC=E5=88=A4=E6=8D=AE=E5=8F=AA=E6=8C=A1=E4=BD=8F=E4=B8=80?= =?UTF-8?q?=E5=8D=8A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-14 指出:我那条"两端预设清单 id/顺序逐项相等"只挡得住**一半** —— 它挡"清单漏一档",挡不住"清单有、渲染器没有"(第 7 档两边都加、忘了给 `layersFor` 加分支 ⇒ 值判据全绿,用户看到的却是 aurora)。症状与"信息对等缺口" 一模一样,只是路径换了。核对:他说得对,`layersFor` 的兜底就是 `aurora`, 所以丢分支是**静默**的。 新增 `test/harmony-presets.test.mjs`(4 条,`--experimental-strip-types` 直接执行 纯逻辑 `model/Wallpaper.ts` —— 与 `harmony-logic` 同一先例,**能真跑**): 1. 六档 × 两主题:不抛异常、非空、每层 kind/色标/网格字段有效; 2. ★ **清单里有、渲染器没有 → 红**:手法是拿一个不存在的 id 调一次当作"兜底长相", 任何一档丢了分支就会与兜底逐字节相同 ⇒ 红。**并写明已知边界**:`aurora` 自己 就是兜底档,它"丢分支"在行为上不可观测(由值判据 + 第 3 条兜住); 3. 六档两两不同(复制分支没改参数 → 红); 4. 深色档必须与浅色档不同("深色没换色" → 红)。 **不需要设备 ⇒ 按 pi 的提醒不登记进 `static` 欠账**(那会虚增余额)。 两条变异都验过:删 `mesh` 分支 → 行为判据报"画出来的和兜底档一模一样"; 清单加第 7 档不加分支 → **值判据与行为判据同时红**(两条一起才闭合)。 安装相位 14/14 绿。 另按 pi 要求处理两处登记: · §7.12 那行"px ↔ 档位"改成**核实后的真相**:`bg_blur` 在鸿蒙侧只有搬运没有消费 (`Appearance.ts` clamp 存下、`AppearanceStore` 同步,**没有任何页面读它**), 所以不存在映射表可钉 —— 钉一张不存在的表是假判据;并写明"若将来开始消费它, 那时必须补映射判据",作为"人工约定、当前无判据"的登记处。 · 预设 id/顺序那条**标清钉的是产品要求**(顺序=用户看到的选项次序),理由写进判据注释。 --- client/electron/test/harmony-presets.test.mjs | 107 ++++++++++++++++++ client/electron/test/run-all.mjs | 3 + docs/HARMONY-ALIGN-PLAN.md | 16 ++- 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 client/electron/test/harmony-presets.test.mjs diff --git a/client/electron/test/harmony-presets.test.mjs b/client/electron/test/harmony-presets.test.mjs new file mode 100644 index 0000000..41cb2e3 --- /dev/null +++ b/client/electron/test/harmony-presets.test.mjs @@ -0,0 +1,107 @@ +/* + * 预设的**行为**判据:每一档都真的画得出来。 + * + * 为什么需要它(pi 2026-09-14 指出):`appearance-defaults.test.mjs` 那条 + * "鸿蒙 `PRESET_IDS` 与 WebUI `PRESETS` 的 id 与顺序逐项一致" 只挡得住**一半** —— + * 它挡"清单漏一档",**挡不住"清单有、渲染器没有"**:将来两边都加了第 7 档、 + * 却忘了给 `layersFor` 加分支,那条判据**全绿**,而用户看到的是"无背景" + * ——正是"信息对等缺口"的同一个症状,只是路径换了。 + * + * 所以这里是**两条配对**:值判据管"清单对不对",行为判据管"每一档真的画得出来"。 + * 这与命中区那处(数值在 `NavItems.ts`、判据直接 import)是同一个配对方式。 + * + * **能真跑**,不是形态判据:`model/Wallpaper.ts` 是纯逻辑(无 `@ohos` 依赖、 + * 类型可擦除),用 node 的 `--experimental-strip-types` 直接执行它 —— 与 + * `harmony-logic.test.mjs` 同一个先例。因此它**不需要设备**, + * 也**不该**登记进 `static` 欠账(那会虚增余额)。 + */ +import { join } from 'node:path'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { pathToFileURL } from 'node:url'; + +const ROOT = '/home/program/agentmail'; +const WALLPAPER_TS = join(ROOT, 'client/harmony/entry/src/main/ets/model/Wallpaper.ts'); + +// 直接执行 `.ets` 树里的**纯逻辑** `.ts`(有 `@ohos` 依赖的文件不能这样跑) +const W = await import(pathToFileURL(WALLPAPER_TS).href); + +const IDS = W.PRESET_IDS; +const THEMES = [false, true]; +const themeName = d => (d ? '深色' : '浅色'); + +/** 层栈的稳定序列化(只取有意义字段,便于比较与报错) */ +const shapeOf = layers => JSON.stringify(layers.map(l => ({ + kind: l.kind, + cx: l.cx, cy: l.cy, radius: l.radius, angle: l.angle, + colors: l.colors, stops: l.stops, + lineColor: l.lineColor, lineAlpha: l.lineAlpha, step: l.step +}))); + +test('六档 × 两种主题:都画得出来(不抛异常、非空、每层字段有效)', () => { + assert.ok(Array.isArray(IDS) && IDS.length >= 6, `预设清单要能取到(实际 ${IDS?.length})`); + for (const id of IDS) { + for (const dark of THEMES) { + let layers; + assert.doesNotThrow(() => { layers = W.layersFor(id, dark); }, + `${id}(${themeName(dark)})调用就抛了 —— 用户看到的是"无背景"`); + assert.ok(Array.isArray(layers) && layers.length >= 1, + `${id}(${themeName(dark)})返回空层栈 ⇒ 这一档画出来是空白`); + for (const [i, l] of layers.entries()) { + assert.ok(['radial', 'linear', 'grid'].includes(l.kind), + `${id}(${themeName(dark)})第 ${i} 层的 kind 认不出:${l.kind}`); + if (l.kind === 'grid') { + assert.ok(typeof l.step === 'number' && l.step > 0, `${id} 网格档缺 step`); + assert.ok(typeof l.lineColor === 'string' && l.lineColor.length > 0, `${id} 网格档缺线色`); + } else { + assert.ok(Array.isArray(l.colors) && l.colors.length >= 2 && l.colors.every(c => typeof c === 'string' && c.length > 0), + `${id}(${themeName(dark)})第 ${i} 层的色标不完整:${JSON.stringify(l.colors)}`); + } + } + } + } +}); + +/* + * ★ 这条是 pi 那半边的直接对应物:**"清单里有、渲染器里没有"必须红。** + * + * 手法:拿一个**不存在的 id** 调一次 —— `normalizePreset` 会把它落到默认档 + * (aurora),于是"兜底长什么样"就有了可比较的对象。任何一档如果**丢了分支**, + * 它会静默落到同一个兜底上 ⇒ 与兜底逐字节相同 ⇒ 红。 + * + * **已知边界(写出来,不假装全覆盖)**:`aurora` 自己**就是**那个兜底档, + * 所以它"丢了分支"在行为上不可观测(结果一样)。这条判据能测到的是 + * **另外五档**;aurora 的存在性由值判据(清单相等)与下面那条"六档两两不同"兜住。 + */ +test('★ 清单里有、渲染器里没有 → 红(每一档都必须与"兜底档"不同)', () => { + for (const dark of THEMES) { + const fallback = shapeOf(W.layersFor('__这个 id 不存在__', dark)); + const aurora = shapeOf(W.layersFor('aurora', dark)); + assert.equal(fallback, aurora, `自检:认不出的 id 应当落到默认档 aurora(${themeName(dark)})`); + for (const id of IDS) { + if (id === 'aurora') continue; // 见上面的"已知边界" + assert.notEqual(shapeOf(W.layersFor(id, dark)), fallback, + `${id}(${themeName(dark)})画出来的和"兜底档"一模一样 ⇒ 它没有自己的渲染分支` + + `(清单里有、渲染器没有:用户看到的是 aurora,不是他选的那档)`); + } + } +}); + +test('六档两两不同(复制粘贴分支没改参数 → 红)', () => { + for (const dark of THEMES) { + const seen = new Map(); + for (const id of IDS) { + const s = shapeOf(W.layersFor(id, dark)); + assert.ok(!seen.has(s), + `${id} 与 ${seen.get(s)} 在${themeName(dark)}下画出来完全相同 —— 复制分支后忘了改参数`); + seen.set(s, id); + } + } +}); + +test('深色档必须与浅色档不同("深色没换色" → 红)', () => { + for (const id of IDS) { + assert.notEqual(shapeOf(W.layersFor(id, true)), shapeOf(W.layersFor(id, false)), + `${id} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`); + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index c90e67e..0de7067 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -64,6 +64,9 @@ const SUITE = [ ['test/theme.test.mjs', [], 30], ['test/background.test.mjs', [], 42], ['test/cross-client-theme.test.mjs', [], 15], + // 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。 + // 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。 + ['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4], ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], ['test/harmony-system-api.test.mjs', [], 5], // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index 57ae129..e194235 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -101,6 +101,20 @@ WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递 | 第 2 步 | **圆角 + 能裁剪 + 给对边** | ✅ **可验收,且不需要设备** —— 只读 `.ets` 的层结构。**不许登记成 `static` 欠账**(那会虚增余额,而它本来就能判) | | 第 3 步 | 交互:点某天/翻页/滑动阈值/ics | ⚠️ **必然进 `static` 欠账**(要设备:能装、能点)。到期前提见套件的探针表 | +**预设清单那条判据钉的是"集合"还是"产品要求"(pi §2 要求标清)**: +钉的是**产品要求** —— WebUI 的 `PRESETS` 顺序就是预设选择器里用户看到的顺序(`backgroundStore.ts` +用它渲染列表),鸿蒙若顺序不同,用户在两端看到的选项次序不一致、会以为"选错了"。 +所以"逐项一致(含顺序)"要**保留**,理由写在判据注释里;将来无害重排也必须**两边同时改** +(两边都改时判据仍绿 —— 它比的是两份清单,不是某一份的历史)。 + +**行为判据(pi 2026-09-14 指出"清单相等只挡住一半")**:清单相等是"能画出来"的**必要**条件, +挡不住"**清单里有、渲染器没有**"(第 7 档两边都加、忘了给 `layersFor` 加分支 ⇒ 值判据全绿, +用户看到的却是 aurora)。已补可跑的行为判据 `client/electron/test/harmony-presets.test.mjs` +(4 条,`--experimental-strip-types` 直接执行纯逻辑 `model/Wallpaper.ts`,**不需要设备 ⇒ 不进欠账**): +每档两主题都非空且字段有效 / **每一档必须与"兜底档"不同**(丢分支 ⇒ 红)/ 六档两两不同 / 深色档必须与浅色档不同。 +两条变异都验过:删 `mesh` 分支 → 行为判据命中"清单里有、渲染器没有";清单加第 7 档不加分支 → +**值判据与行为判据同时红**(这就是"两条一起才闭合")。 + **手势阈值(pi §2:先证明那个值在 WebUI 侧被钉着,否则"一致"只是当下的巧合)**: 核实结果 —— **WebUI 侧没有被钉**:`client/electron/src/components/CalendarView.tsx:204` 是裸字面量 `Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy) * 1.5 || !fast`(`fast` = `<600ms`), @@ -537,7 +551,7 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 |---|---|---|---| | 圆角 | 自声明 `--radius-card: 0.875rem` | 系统 `sys.float.ohos_id_corner_radius_card/button` | 系统圆角会随设备/主题/无障碍设置变;跟着系统才是"系统方案" | | 材质(玻璃) | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter` | 系统 `backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` | 系统材质自带深浅两套颜色与模糊半径,手写 alpha 跟不了深色 | -| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有对应物**:栏上的系统**材质档位**(`BlurStyle.*`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量**:`bg_blur=8px` 与"材质档位"对不上**不是 bug**,是映射 —— 不写清就会有人当 bug 报(pi 2026-09-14 指出) | +| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有被消费**:栏上的系统**材质档位**(`BlurStyle.COMPONENT_THICK`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量,而且这边根本没有映射表**。2026-09-14 核实:`bg_blur` 在鸿蒙侧只有"搬运"没有"消费" —— `Appearance.ts` 把它 clamp 到 0~40 存进 `bgBlur`、`AppearanceStore` 同步它,但**没有任何页面/组件读它**(材质档位是固定枚举)。所以不存在"px ↔ 档位"的值映射可钉,也就**没有可判的性质**(pi 问的"映射表有没有判据":没有,且当前不该有 —— 钉一张不存在的表是假判据)。**改一端的含义**:改 WebUI 的 `bg_blur` 语义**不影响鸿蒙**;若将来鸿蒙开始消费它(例如按 px 选不同 `BlurStyle`),**那时**必须补一条映射判据,并更新本行 —— 这是"人工约定、当前无判据"的登记处(CRITERIA.md §12 的同一族)。 | | 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") | | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 |