fake-timers 先 clearTimeout 再 setTimeout 报读取 push 失败
fake-timers 15.2.0 清理早于首次安排时,timerHeap 可能没有初始化,随后 setTimeout 报读取 push 失败。正式 15.2.1 已统一初始化,检查实际工具加载的依赖副本。
fake-timers 升级到 15.2.0 后,测试还没安排第一个 timeout,清理代码先调用了 clearTimeout;接下来一次 setTimeout 却报:
TypeError: Cannot read properties of undefined (reading 'push')
如果堆栈停在 fake-timers 的 addTimer,应检查这一版本的内部初始化回归,不要先认定业务数组没有初始化。
clear 路径先创建了一半状态
15.2.0 的 addTimer 在 clock.timers 不存在时,同时创建 timers 映射和 timerHeap。clearTimer 却可以先单独创建 timers。第一次安排 timer 时,映射已经存在,addTimer 跳过初始化,随后调用还不存在的 timerHeap.push,错误就出现了。
因此“先安排过一个定时器再清理”可能正常,“清理先发生”才失败。组件卸载、测试前清理以及第三方 UI 组件的收尾,都可能改变这个先后顺序。
如果清理的是在安装 fake clock 之前创建的原生 timer,还要分别处理原生与伪造 timer 的所有权。库可能打印原生 timer 清理警告;这项检查与 timerHeap 未初始化不是同一个问题,不宜为了消除警告就让所有清理无条件转发给原生实现。
使用包含完整初始化的正式版本
15.2.1 已将两条路径接到 ensureTimerState:分别确认映射和 heap 存在,缺少 heap 时建立它,并纳入已有映射项。正式 15.4.0 也保留完整的状态初始化方式。
沿项目现有的依赖更新流程更换受影响副本,保留锁文件差异。Jest、Sinon 等工具可能间接依赖 fake-timers,只改应用直接安装的一份,不保证失败的工具也加载同一版。
不建议在每个测试开始时塞一个没有业务意义的 timeout,再立刻清掉,只为抢先初始化内部状态。它会让测试依赖一个回归的偶然顺序,也可能改变 timer 数量和自动推进逻辑。更不要直接给 clock.timerHeap 填一个数组;实际实现需要 heap 的排序与移除行为。
验收原来的清理顺序
保留原来“清理先于首次安排”的入口,确认之后的 timeout 能登记和执行;再覆盖已有 timer 的正常清理、重复清理,以及 interval 的相应行为。测试应沿原有框架运行,不能只在单独脚本里看到 setTimeout 没抛错就算整个套件恢复。
如果更新后仍出现读取 push 失败,继续核对堆栈与实际包路径。另一个业务对象上的同名方法,不能仅凭错误文字相同就归到这次 15.2.0 回归。


