'use strict'; // 呈现时间层的纯用例(不需要 Electron、不需要显示器): // node test/presented-time.test.js // 「什么时候该动画、动画到哪、被打断怎么办」是这次跳转动画最容易写错的地方, // 所以把它从渲染代码里拆出来,先在 node 里钉死,Windows 上就只剩「观感」要试。 const presented = require('../renderer/presented-time'); const { JUMP_THRESHOLD_MS, EASE_MS } = presented; let passed = 0; const failures = []; function check(name, ok, detail) { if (ok) { passed += 1; console.log(`PASS ${name}${detail ? ` [${detail}]` : ''}`); } else { failures.push(name); console.log(`FAIL ${name}${detail ? ` [${detail}]` : ''}`); } } const T0 = 1_700_000_000_000; // 固定基准时刻,测试不依赖真实时钟 const SECOND = 1000; const MINUTE = 60 * SECOND; // 走时参数就是主进程下发的那个快照,这里照原样造 const snap = (baseMs, baseRealMs, rate) => ({ baseMs, baseRealMs, rate }); console.log('--- 平时不动画 ---'); { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); check('首个参数直接对齐(没有"上一个值"可以缓动)', presented.valueAt(layer, T0) === T0); check('走 1 秒后呈现时间 = 目标时间,没有动画', presented.valueAt(layer, T0 + SECOND) === T0 + SECOND); check('走 1 小时后依然相等(不会被动画层拖后腿)', presented.valueAt(layer, T0 + 3600 * SECOND) === T0 + 3600 * SECOND); } { // 首个参数哪怕离当前显示时间很远(例如时钟窗口后打开),也不该滚一次 const layer = presented.create(); presented.accept(layer, T0, snap(T0 + 10 * MINUTE, T0, 1)); check('首个参数:直接对齐,不做动画', presented.valueAt(layer, T0) === T0 + 10 * MINUTE); } console.log(`--- 大跳变(> ${JUMP_THRESHOLD_MS}ms)缓动 ${EASE_MS}ms ---`); { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); const target = T0 + 30 * MINUTE; presented.accept(layer, T0, snap(target, T0, 1)); // 快进 +30 分钟 check('跳变当帧显示的还是跳变前的时间(数字从这里开始滚)', presented.valueAt(layer, T0) === T0, String(presented.valueAt(layer, T0))); const half = presented.valueAt(layer, T0 + EASE_MS / 2); check('半程时停在两者之间', half > T0 && half < target, `${Math.round(half - T0)}ms`); check('缓出:半程已经走完一大半路程(不是匀速)', half - T0 > (target - T0) / 2, `${Math.round(((half - T0) / (target - T0)) * 100)}%`); let previous = -Infinity; let monotonic = true; for (let t = 0; t <= EASE_MS; t += 25) { const value = presented.valueAt(layer, T0 + t); if (value < previous) monotonic = false; previous = value; } check('整段缓动单调向前(不会中途倒退)', monotonic); // 结束时要比的是「那一刻的目标时间」—— 目标本身还在按 ×1 继续走 const settled = target + EASE_MS; check(`满 ${EASE_MS}ms 精确落在目标上(不留残余,否则数字永远差几百毫秒)`, presented.valueAt(layer, T0 + EASE_MS) === settled, String(presented.valueAt(layer, T0 + EASE_MS) - settled)); check('之后一直等于目标时间', presented.valueAt(layer, T0 + 5000) === target + 5000); } { // 往回跳(设定到更早的时间)同样缓动,只是方向相反 const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); const target = T0 - 10 * MINUTE; presented.accept(layer, T0, snap(target, T0, 1)); check('负向跳变当帧仍在原处', presented.valueAt(layer, T0) === T0); let previous = Infinity; let monotonic = true; for (let t = 0; t <= EASE_MS; t += 25) { const value = presented.valueAt(layer, T0 + t); if (value > previous) monotonic = false; previous = value; } check('负向跳变整段单调往回', monotonic); check('负向跳变落在目标上', presented.valueAt(layer, T0 + EASE_MS) === target + EASE_MS); } console.log('--- 小跳变直接跳 ---'); { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); presented.accept(layer, T0, snap(T0 + JUMP_THRESHOLD_MS, T0, 1)); check(`正好 ${JUMP_THRESHOLD_MS}ms:直接跳,不缓动`, presented.valueAt(layer, T0) === T0 + JUMP_THRESHOLD_MS); const other = presented.create(); presented.accept(other, T0, snap(T0, T0, 1)); presented.accept(other, T0, snap(T0 + JUMP_THRESHOLD_MS + 1, T0, 1)); check(`超过 ${JUMP_THRESHOLD_MS}ms:开始缓动(当帧还在原处)`, presented.valueAt(other, T0) === T0); } console.log('--- 改倍率不是跳变 ---'); { const before = snap(T0, T0, 1); const visible = presented.targetAt(before, T0 + 5000); // 改倍率那一刻的显示时间 const after = snap(visible, T0 + 5000, 60); // 模型先固化显示时间再换倍率 check('模型锚点式换倍率:同一时刻新旧参数算出的时间相等', presented.jumpBetween(before, after) === 0, String(presented.jumpBetween(before, after))); const layer = presented.create(); presented.accept(layer, T0, before); presented.accept(layer, T0 + 5000, after); check('换倍率后画面不缓动,当帧就是固化下来的那个值', presented.valueAt(layer, T0 + 5000) === visible); check('换倍率后立刻按新倍率走(不会被动画拦住)', presented.valueAt(layer, T0 + 5016) === visible + 16 * 60, String(presented.valueAt(layer, T0 + 5016) - visible)); } { const before = snap(T0, T0, 1); const after = snap(T0 + 5 * SECOND, T0, 1); // 一次真实的 +5 秒快进 check('快进 +5 秒:参数比较出来的跳变幅度就是 5 秒', presented.jumpBetween(before, after) === 5 * SECOND, String(presented.jumpBetween(before, after))); check('没有上一个参数时不判定跳变', presented.jumpBetween(null, after) === 0); } console.log('--- 缓动中又跳一次:从当前呈现值重新出发 ---'); { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); presented.accept(layer, T0, snap(T0 + 30 * MINUTE, T0, 1)); const mid = presented.valueAt(layer, T0 + 200); check('中途确实停在两者之间', mid > T0 && mid < T0 + 30 * MINUTE, `${Math.round(mid - T0)}ms`); // 还没滚完又点了一次 +1 分钟:模型的目标变成 31 分钟 presented.accept(layer, T0 + 200, snap(T0 + 31 * MINUTE, T0 + 200, 1)); check('重定向当帧接着当前呈现值,不跳回起点', presented.valueAt(layer, T0 + 200) === mid, `${Math.round(presented.valueAt(layer, T0 + 200) - mid)}ms`); check('重定向后不会倒退', presented.valueAt(layer, T0 + 201) >= mid); check('重定向后仍然精确落在新目标上', presented.valueAt(layer, T0 + 200 + EASE_MS) === T0 + 31 * MINUTE + EASE_MS); } console.log('--- 暂停不缓动 ---'); { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); presented.accept(layer, T0, snap(T0 + 30 * MINUTE, T0, 1)); const mid = presented.valueAt(layer, T0 + 100); check('跳变后 100ms 正在缓动', mid > T0 && mid < T0 + 30 * MINUTE, `${Math.round(mid - T0)}ms`); // 暂停:模型把显示时间固化在暂停那一刻(也就是刚跳到的目标) presented.accept(layer, T0 + 100, snap(T0 + 30 * MINUTE, T0 + 100, 0)); check('暂停当帧就落到暂停时刻的准确时间(不会挂着走不完的中间值)', presented.valueAt(layer, T0 + 100) === T0 + 30 * MINUTE); check('暂停后画面纹丝不动', presented.valueAt(layer, T0 + 9000) === presented.valueAt(layer, T0 + 100)); } { const layer = presented.create(); presented.accept(layer, T0, snap(T0, T0, 1)); presented.accept(layer, T0, snap(T0, T0, 0)); check('暂停状态下快进也是直接落到新值(暂停画面必须是精确的暂停时间)', presented.valueAt(layer, T0) === T0); presented.accept(layer, T0 + 5000, snap(T0 + 5 * MINUTE, T0 + 5000, 0)); check('暂停后快进:当帧就是新值,不滚', presented.valueAt(layer, T0 + 5000) === T0 + 5 * MINUTE); check('暂停后快进:之后继续定格在该值', presented.valueAt(layer, T0 + 60 * SECOND) === T0 + 5 * MINUTE); } console.log(''); if (failures.length === 0) { console.log(`全部通过:${passed}/${passed}`); process.exit(0); } else { console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`); process.exit(1); }