Files
Genarrative/apps/ai-game-creator-shell/tests/unhandledRejectionBridge.ts
T
k88936 7528ba0116 unhandledrejection桥接保留once语义
- 用 rawListeners 记录原始监听器,拆卸时把 once 包装按 process.once 装回,普通监听器仍按 process.on 装回
- 未被 window 消费时调用 once 包装包裹的原函数,避免脱离 emitter 直调包装
- 注释记录安装期间新增监听器排在恢复监听器之前的既有边界
2026-10-02 11:35:13 +08:00

67 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 把 Node 的 `unhandledRejection` 桥接成 jsdom `window` 的 `unhandledrejection` 事件。
*
* 生产 WebView 里,`void` 掉的 async handler(`onSubmit` / `onClick`)抛出的拒绝会派发
* `unhandledrejection`,被 `AuthenticatedClient` 的全局 handler 交给错误池。jsdom 既不派发
* 该事件,vitest 又会把未处理的拒绝当成用例失败;需要观察这条链路的用例用它接管 Node
* 监听器,再合成一个带 `reason` 的 window 事件。
*
* 只抑制**被 window 侧消费**的拒绝:合成事件的 `reason` 是访问器,任何监听方读过它就算
* 有人接手;没人读说明这条拒绝是意外的,交回安装前的监听器(含 vitest 的失败上报),
* 不会被静默吞掉。
*
* 安装前用 `rawListeners` 取原始监听器:`process.once` 注册的包装(`.listener`)在拆卸时
* 按 `once` 装回去,不会退化成每次拒绝都触发的普通监听器;安装期间新增的监听器仍排在
* 恢复出来的监听器之前。
*
* 用法:用例内 `const uninstall = installUnhandledRejectionBridge()`,在 `finally` 或
* `afterEach` 里 `uninstall()`。
*/
type RejectionListener = (reason: unknown, promise: Promise<unknown>) => void;
type RawRejectionListener = RejectionListener & {
listener?: RejectionListener;
};
function invokeRejectionListener(
listener: RawRejectionListener,
reason: unknown,
promise: Promise<unknown>,
) {
// once 包装必须调它包裹的原函数:脱离 emitter 直接调用包装会碰到 this 为空。
(listener.listener ?? listener)(reason, promise);
}
export function installUnhandledRejectionBridge() {
const originalListeners = process.rawListeners(
'unhandledRejection',
) as RawRejectionListener[];
process.removeAllListeners('unhandledRejection');
const handleRejection = (reason: unknown, promise: Promise<unknown>) => {
let consumed = false;
const event = new Event('unhandledrejection');
Object.defineProperty(event, 'reason', {
get() {
consumed = true;
return reason;
},
});
window.dispatchEvent(event);
if (!consumed) {
for (const listener of originalListeners) {
invokeRejectionListener(listener, reason, promise);
}
}
};
process.on('unhandledRejection', handleRejection);
return () => {
process.removeListener('unhandledRejection', handleRejection);
for (const listener of originalListeners) {
if (listener.listener) {
process.once('unhandledRejection', listener.listener);
} else {
process.on('unhandledRejection', listener);
}
}
};
}