// @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: NodeJS.UnhandledRejectionListener[] = []; 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([]); });