/* * 预设的**行为**判据:每一档都真的画得出来。 * * 为什么需要它(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 { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; /* * ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。 * * 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果: * 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**, * 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。 * 两层后果,第二层最糟: * ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛); * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状, * 这次长在判据自己身上。**"规则进来了,对象没进来"**。 * 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。 */ const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..', '..'); 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} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`); } }); /* * ★ 静默兜底必须留下**可观测的痕迹**(pi 2026-09-14)。 * * §10 登记了"未知 id 静默换成 aurora"这条缺省语义,但**登记只防"被误报成 bug"**, * 防不住"没人知道它正在发生" —— 真实环境里它不留任何痕迹。所以返回值里带 * `presetSubstitutedFrom`:换过就是原来那个 id,没换过是空串,页面据此打一行日志。 * * 这条判据同时钉住两件事:① 替换**必须**留痕;② 没替换时**不许**留痕 * (否则痕迹会退化成噪声,等于没有)。 */ test('★ 静默兜底留痕:换过要带出原 id,没换过必须是空串', () => { const unknown = 'ocean-v2-这个档还不存在'; const bad = W.resolveBackground('preset', unknown, 0.2, false, false); assert.equal(bad.presetId, 'aurora', `自检:未知 id 应当被换成默认档(实际 ${bad.presetId})`); assert.equal(bad.presetSubstitutedFrom, unknown, '认不出的 id 被静默替换了,却没留下痕迹 ⇒ 版本偏移时无法确认它在发生(只能猜)'); for (const id of IDS) { for (const dark of THEMES) { const plan = W.resolveBackground('preset', id, 0.2, false, dark); assert.equal(plan.presetSubstitutedFrom, '', `${id}(${themeName(dark)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`); } } }); /* ── 模糊档的搬运与归一(P4c 起壁纸真的会糊了) ── */ test('模糊档:计划**搬得动**这个值,且 0~40 归一(边界与 Appearance.ts 的 clamp 同值)', () => { /* * ★ 这条是变异测试补上的:把 `plan.blurPx = normalizeBlur(blurPx)` 改成 `= 0` * (计划根本不搬运模糊值)时,appearance 那几条**全绿** —— * 它们判的是"页面把值传进来了"和"页面拿它去糊了",中间这一段没人判。 */ assert.equal(typeof W.normalizeBlur, 'function', 'Wallpaper 要提供 normalizeBlur'); assert.equal(W.MAX_BLUR_PX, 40, '上限 40(与 model/Appearance.ts 的 clampNumber(bgBlur,0,40,4) 同值)'); for (const [input, want] of [[0, 0], [4, 4], [40, 40], [41, 40], [999, 40], [-5, 0], [-0.4, 0]]) { assert.equal(W.normalizeBlur(input), want, `normalizeBlur(${input}) 应当是 ${want}`); } assert.equal(W.normalizeBlur(4.6), 5, '小数要四舍五入到整数 px'); assert.equal(W.normalizeBlur(NaN), 0, 'NaN 要归 0(不能是 NaN,否则喂给 blur() 是未定义行为)'); // 搬运:三档都要带上,而且值就是传进去的那个 for (const kind of ['preset', 'image', 'none']) { const plan = W.resolveBackground(kind, 'aurora', 0.2, kind === 'image', false, 12); assert.equal(plan.blurPx, 12, `\`${kind}\` 档没有把模糊值搬进计划(计划里是 ${plan.blurPx})`); } // 缺省入参:老调用点(不传第 6 个参数)语义不变 ⇒ 0 const legacy = W.resolveBackground('preset', 'aurora', 0.2, false, false); assert.equal(legacy.blurPx, 0, '不传模糊档时应当是 0(老调用点语义不变)'); // 越界要**归一**再搬,而不是把 999 原样带出去 assert.equal(W.resolveBackground('image', 'aurora', 0.2, true, false, 999).blurPx, 40, '越界的模糊值要在搬运时就归一(否则页面会把 999 直接喂给 blur())'); });