27f7375b51
- unhandledRejectionBridge:合成事件优先用 PromiseRejectionEvent(jsdom 没这个构造器时退回普通 Event),再把 reason / promise 都挂成实例访问器,jsdom 分支下 promise 也可读 - 判定调整为读 reason 或 promise 任一个都算 window 侧接手(原来只认 reason) - 补一条用例:事件带 promise 且读 promise 也算被消费
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* jsdom `unhandledrejection` 桥的「只吞被消费的拒绝」语义。
|
|
*
|
|
* 桥接管 Node 监听器是为了让组件能收到 window 事件;但没人消费的拒绝必须交回原监听器,
|
|
* 否则用例会静默吞掉真实的未处理拒绝。
|
|
*/
|
|
import { afterEach, expect, test } from 'vitest';
|
|
|
|
import { installUnhandledRejectionBridge } from './unhandledRejectionBridge';
|
|
|
|
const installed: Array<() => void> = [];
|
|
const processListeners: Array<(reason: unknown, promise: unknown) => void> = [];
|
|
const windowListeners: Array<(event: Event) => void> = [];
|
|
let parkedListeners: Array<(reason: unknown, promise: unknown) => void> = [];
|
|
|
|
afterEach(() => {
|
|
while (installed.length > 0) installed.pop()?.();
|
|
while (processListeners.length > 0) {
|
|
process.removeListener('unhandledRejection', processListeners.pop()!);
|
|
}
|
|
while (windowListeners.length > 0) {
|
|
window.removeEventListener('unhandledrejection', windowListeners.pop()!);
|
|
}
|
|
for (const listener of parkedListeners) {
|
|
process.on('unhandledRejection', listener);
|
|
}
|
|
parkedListeners = [];
|
|
});
|
|
|
|
/** 停放 vitest 自己的监听器,好观察「交回原监听器」而不触发用例失败上报。 */
|
|
function parkProcessListeners() {
|
|
parkedListeners = process.listeners('unhandledRejection');
|
|
process.removeAllListeners('unhandledRejection');
|
|
}
|
|
|
|
function trackProcessListener() {
|
|
const forwarded: unknown[] = [];
|
|
const listener = (reason: unknown) => {
|
|
forwarded.push(reason);
|
|
};
|
|
processListeners.push(listener);
|
|
process.on('unhandledRejection', listener);
|
|
return forwarded;
|
|
}
|
|
|
|
test('没人读 reason 的拒绝会交回原监听器', () => {
|
|
parkProcessListeners();
|
|
const forwarded = trackProcessListener();
|
|
installed.push(installUnhandledRejectionBridge());
|
|
|
|
const reason = new Error('意外的拒绝');
|
|
process.emit('unhandledRejection', reason, Promise.resolve());
|
|
|
|
expect(forwarded).toEqual([reason]);
|
|
});
|
|
|
|
test('window 侧读过 reason 的拒绝不再交回原监听器', () => {
|
|
const forwarded = trackProcessListener();
|
|
const consumed: unknown[] = [];
|
|
const listener = (event: Event) => {
|
|
consumed.push((event as Event & { reason?: unknown }).reason);
|
|
};
|
|
windowListeners.push(listener);
|
|
window.addEventListener('unhandledrejection', listener);
|
|
installed.push(installUnhandledRejectionBridge());
|
|
|
|
const reason = new Error('已接手的拒绝');
|
|
process.emit('unhandledRejection', reason, Promise.resolve());
|
|
|
|
expect(consumed).toEqual([reason]);
|
|
expect(forwarded).toEqual([]);
|
|
});
|
|
|
|
test('交回原监听器时 this 绑成 process,和 Node EventEmitter 一致', () => {
|
|
parkProcessListeners();
|
|
const receivers: unknown[] = [];
|
|
const listener = function (this: unknown) {
|
|
receivers.push(this);
|
|
};
|
|
processListeners.push(listener);
|
|
process.on('unhandledRejection', listener);
|
|
installed.push(installUnhandledRejectionBridge());
|
|
|
|
process.emit('unhandledRejection', new Error('需要交回'), Promise.resolve());
|
|
|
|
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([]);
|
|
});
|