拒绝桥合成事件带上promise

- unhandledRejectionBridge:合成事件优先用 PromiseRejectionEvent(jsdom 没这个构造器时退回普通 Event),再把 reason / promise 都挂成实例访问器,jsdom 分支下 promise 也可读
- 判定调整为读 reason 或 promise 任一个都算 window 侧接手(原来只认 reason)
- 补一条用例:事件带 promise 且读 promise 也算被消费
This commit is contained in:
2026-10-02 13:48:26 +08:00
parent a512786996
commit 27f7375b51
2 changed files with 57 additions and 9 deletions
@@ -86,3 +86,20 @@ test('交回原监听器时 this 绑成 process,和 Node EventEmitter 一致',
expect(receivers).toEqual([process]);
});
test('合成事件同时带 reason 与 promise,读任一个都算被消费', () => {
const forwarded = trackProcessListener();
let received: unknown = null;
const listener = (event: Event) => {
received = (event as Event & { promise?: unknown }).promise;
};
windowListeners.push(listener);
window.addEventListener('unhandledrejection', listener);
installed.push(installUnhandledRejectionBridge());
const promise = Promise.resolve();
process.emit('unhandledRejection', new Error('带 promise 的拒绝'), promise);
expect(received).toBe(promise);
expect(forwarded).toEqual([]);
});
@@ -4,10 +4,10 @@
* 生产 WebView 里,`void` 掉的 async handler(`onSubmit` / `onClick`)抛出的拒绝会派发
* `unhandledrejection`,被 `AuthenticatedClient` 的全局 handler 交给错误池。jsdom 既不派发
* 该事件,vitest 又会把未处理的拒绝当成用例失败;需要观察这条链路的用例用它接管 Node
* 监听器,再合成一个带 `reason` 的 window 事件。
* 监听器,再合成一个与 WebView `PromiseRejectionEvent` 同形(`reason` + `promise`)的 window 事件。
*
* 只抑制**被 window 侧消费**的拒绝:合成事件的 `reason` 是访问器,任何监听方读过它就算
* 有人接手;没人读说明这条拒绝是意外的,交回安装前的监听器(含 vitest 的失败上报),
* 只抑制**被 window 侧消费**的拒绝:合成事件的 `reason` / `promise` 是访问器,任何监听方读过
* 任一个就算有人接手;没人读说明这条拒绝是意外的,交回安装前的监听器(含 vitest 的失败上报),
* 不会被静默吞掉。
*
* 安装前用 `rawListeners` 取原始监听器:`process.once` 注册的包装(`.listener`)在拆卸时
@@ -32,6 +32,41 @@ function invokeRejectionListener(
Reflect.apply(listener.listener ?? listener, process, [reason, promise]);
}
/**
* 合成一个与 WebView `unhandledrejection` 同形的 window 事件。
*
* 浏览器里本来就是 `PromiseRejectionEvent`(带 `reason` / `promise`),有构造器就直接用它;
* jsdom 不实现这个类时退回普通 `Event`。两种来源都再把 `reason` / `promise` 覆盖成实例访问器:
* 既保证 jsdom 分支下 `promise` 同样可读,也让「读过任一个字段」都能记成有人接手。
*/
function createRejectionEvent(
reason: unknown,
promise: Promise<unknown>,
markConsumed: () => void,
): Event {
const event =
typeof PromiseRejectionEvent === 'function'
? new PromiseRejectionEvent('unhandledrejection', { promise, reason })
: new Event('unhandledrejection');
Object.defineProperties(event, {
reason: {
get() {
markConsumed();
return reason;
},
configurable: true,
},
promise: {
get() {
markConsumed();
return promise;
},
configurable: true,
},
});
return event;
}
export function installUnhandledRejectionBridge() {
const originalListeners = process.rawListeners(
'unhandledRejection',
@@ -39,12 +74,8 @@ export function installUnhandledRejectionBridge() {
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;
},
const event = createRejectionEvent(reason, promise, () => {
consumed = true;
});
window.dispatchEvent(event);
if (!consumed) {