unhandledrejection 测试桥只吞被 window 侧消费的拒绝
- 合成事件的 reason 改为访问器:有监听方读过就算有人接手;没人读则交回安装前的监听器(含 vitest 失败上报) - 新增 unhandledRejectionBridge.test.ts 钉住两条路径:无人接手会交回、window 读过则不再交回 - 避免桥把无关的未处理拒绝静默吞掉
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// @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([]);
|
||||
});
|
||||
@@ -6,16 +6,31 @@
|
||||
* 该事件,vitest 又会把未处理的拒绝当成用例失败;需要观察这条链路的用例用它接管 Node
|
||||
* 监听器,再合成一个带 `reason` 的 window 事件。
|
||||
*
|
||||
* 只抑制**被 window 侧消费**的拒绝:合成事件的 `reason` 是访问器,任何监听方读过它就算
|
||||
* 有人接手;没人读说明这条拒绝是意外的,交回安装前的监听器(含 vitest 的失败上报),
|
||||
* 不会被静默吞掉。
|
||||
*
|
||||
* 用法:用例内 `const uninstall = installUnhandledRejectionBridge()`,在 `finally` 或
|
||||
* `afterEach` 里 `uninstall()`。
|
||||
*/
|
||||
export function installUnhandledRejectionBridge() {
|
||||
const originalListeners = process.listeners('unhandledRejection');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
const handleRejection = (reason: unknown) => {
|
||||
const handleRejection = (reason: unknown, promise: Promise<unknown>) => {
|
||||
let consumed = false;
|
||||
const event = new Event('unhandledrejection');
|
||||
Object.defineProperty(event, 'reason', { value: reason });
|
||||
Object.defineProperty(event, 'reason', {
|
||||
get() {
|
||||
consumed = true;
|
||||
return reason;
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
if (!consumed) {
|
||||
for (const listener of originalListeners) {
|
||||
listener(reason, promise);
|
||||
}
|
||||
}
|
||||
};
|
||||
process.on('unhandledRejection', handleRejection);
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user