From 84f78c14356bab5332f638a55ba457e20fd7a3e9 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Fri, 11 Jul 2025 11:58:13 -0700 Subject: [PATCH] Split GlobalErrors into portable and platform-specific parts --- lib/jasmine-core/jasmine.js | 198 +++++++++++++++++++--------------- spec/core/GlobalErrorsSpec.js | 15 ++- src/core/GlobalErrors.js | 198 +++++++++++++++++++--------------- 3 files changed, 230 insertions(+), 181 deletions(-) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index e50e5fca..0bea85bb 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -4303,68 +4303,36 @@ getJasmineRequireObj().formatErrorMsg = function() { getJasmineRequireObj().GlobalErrors = function(j$) { class GlobalErrors { - #global; + #adapter; #handlers; - #originalHandlers; - #jasmineHandlers; #overrideHandler; #onRemoveOverrideHandler; - #onBrowserError; - #onBrowserRejection; - #onNodeError; - #onNodeRejection; constructor(global) { - this.#global = global || j$.getGlobal(); + global = global || j$.getGlobal(); + const dispatchError = this.#dispatchError.bind(this); + + if ( + global.process && + global.process.listeners && + j$.isFunction_(global.process.on) + ) { + this.#adapter = new NodeAdapter(global, dispatchError); + } else { + this.#adapter = new BrowserAdapter(global, dispatchError); + } this.#handlers = []; this.#overrideHandler = null; this.#onRemoveOverrideHandler = null; - - this.#onBrowserError = event => this.#dispatchError(event.error, event); - this.#onBrowserRejection = event => this.#browserRejectionHandler(event); - - this.#onNodeError = error => - this.#handleNodeEvent(error, 'uncaughtException', 'Uncaught exception'); - this.#onNodeRejection = error => - this.#handleNodeEvent( - error, - 'unhandledRejection', - 'Unhandled promise rejection' - ); - - this.#originalHandlers = {}; - this.#jasmineHandlers = {}; } install() { - if ( - this.#global.process && - this.#global.process.listeners && - j$.isFunction_(this.#global.process.on) - ) { - this.#installNodeHandler('uncaughtException', this.#onNodeError); - this.#installNodeHandler('unhandledRejection', this.#onNodeRejection); - } else { - this.#global.addEventListener('error', this.#onBrowserError); - - this.#global.addEventListener( - 'unhandledrejection', - this.#onBrowserRejection - ); - } + this.#adapter.install(); } uninstall() { - if ( - this.#global.process && - this.#global.process.listeners && - j$.isFunction_(this.#global.process.on) - ) { - this.#nodeUninstall(); - } else { - this.#browserUninstall(); - } + this.#adapter.uninstall(); } // The listener at the top of the stack will be called with two arguments: @@ -4404,7 +4372,99 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.#onRemoveOverrideHandler = null; } - #nodeUninstall() { + // Either error or event may be undefined + #dispatchError(error, event) { + if (this.#overrideHandler) { + // See discussion of spyOnGlobalErrorsAsync in base.js + this.#overrideHandler(error); + return; + } + + const handler = this.#handlers[this.#handlers.length - 1]; + + if (handler) { + handler(error, event); + } else { + throw error; + } + } + } + + class BrowserAdapter { + #global; + #dispatchError; + #onError; + #onUnhandledRejection; + + constructor(global, dispatchError) { + this.#global = global; + this.#dispatchError = dispatchError; + this.#onError = event => this.#dispatchError(event.error, event); + this.#onUnhandledRejection = this.#unhandledRejectionHandler.bind(this); + } + + install() { + this.#global.addEventListener('error', this.#onError); + + this.#global.addEventListener( + 'unhandledrejection', + this.#onUnhandledRejection + ); + } + + uninstall() { + this.#global.removeEventListener('error', this.#onError); + this.#global.removeEventListener( + 'unhandledrejection', + this.#onUnhandledRejection + ); + } + + #unhandledRejectionHandler(event) { + if (j$.isError_(event.reason)) { + event.reason.jasmineMessage = + 'Unhandled promise rejection: ' + event.reason; + this.#dispatchError(event.reason, event); + } else { + this.#dispatchError( + 'Unhandled promise rejection: ' + event.reason, + event + ); + } + } + } + + class NodeAdapter { + #global; + #dispatchError; + #originalHandlers; + #jasmineHandlers; + #onError; + #onUnhandledRejection; + + constructor(global, dispatchError) { + this.#global = global; + this.#dispatchError = dispatchError; + + this.#jasmineHandlers = {}; + this.#originalHandlers = {}; + + this.#onError = error => + this.#eventHandler(error, 'uncaughtException', 'Uncaught exception'); + this.#onUnhandledRejection = error => + this.#eventHandler( + error, + 'unhandledRejection', + 'Unhandled promise rejection' + ); + } + + install() { + this.#installHandler('uncaughtException', this.#onError); + this.#installHandler('unhandledRejection', this.#onUnhandledRejection); + } + + uninstall() { const errorTypes = Object.keys(this.#originalHandlers); for (const errorType of errorTypes) { this.#global.process.removeListener( @@ -4423,45 +4483,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } } - #browserUninstall() { - this.#global.removeEventListener('error', this.#onBrowserError); - this.#global.removeEventListener( - 'unhandledrejection', - this.#onBrowserRejection - ); - } - - // Either error or event may be undefined - #dispatchError(error, event) { - if (this.#overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - this.#overrideHandler(error); - return; - } - - const handler = this.#handlers[this.#handlers.length - 1]; - - if (handler) { - handler(error, event); - } else { - throw error; - } - } - - #browserRejectionHandler(event) { - if (j$.isError_(event.reason)) { - event.reason.jasmineMessage = - 'Unhandled promise rejection: ' + event.reason; - this.#dispatchError(event.reason, event); - } else { - this.#dispatchError( - 'Unhandled promise rejection: ' + event.reason, - event - ); - } - } - - #installNodeHandler(errorType, handler) { + #installHandler(errorType, handler) { this.#originalHandlers[errorType] = this.#global.process.listeners( errorType ); @@ -4471,7 +4493,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.#global.process.on(errorType, handler); } - #handleNodeEvent(error, errorType, jasmineMessage) { + #eventHandler(error, errorType, jasmineMessage) { if (j$.isError_(error)) { error.jasmineMessage = jasmineMessage + ': ' + error; } else { diff --git a/spec/core/GlobalErrorsSpec.js b/spec/core/GlobalErrorsSpec.js index d83e3a8a..eae2bef9 100644 --- a/spec/core/GlobalErrorsSpec.js +++ b/spec/core/GlobalErrorsSpec.js @@ -161,7 +161,7 @@ describe('GlobalErrors', function() { dispatchEvent(globals.listeners, 'unhandledRejection', new Error('bar')); - expect(handler).toHaveBeenCalledWith(new Error('bar')); + expect(handler).toHaveBeenCalledWith(new Error('bar'), undefined); expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe( 'Unhandled promise rejection: Error: bar' ); @@ -206,7 +206,8 @@ describe('GlobalErrors', function() { 'Unhandled promise rejection with no error or message\n' + '(Tip: to get a useful stack trace, use ' + 'Promise.reject(new Error(...)) instead of Promise.reject().)' - ) + ), + undefined ); }); }); @@ -276,7 +277,10 @@ describe('GlobalErrors', function() { dispatchEvent(globals.listeners, 'uncaughtException', 17); - expect(handler).toHaveBeenCalledWith(new Error('Uncaught exception: 17')); + expect(handler).toHaveBeenCalledWith( + new Error('Uncaught exception: 17'), + undefined + ); }); it('substitutes a descriptive message when the error is falsy', function() { @@ -290,7 +294,8 @@ describe('GlobalErrors', function() { dispatchEvent(globals.listeners, 'uncaughtException', undefined); expect(handler).toHaveBeenCalledWith( - new Error('Uncaught exception with no error or message') + new Error('Uncaught exception with no error or message'), + undefined ); }); }); @@ -344,7 +349,7 @@ describe('GlobalErrors', function() { dispatchEvent(globals.listeners, 'uncaughtException', new Error('bar')); expect(overrideHandler).not.toHaveBeenCalled(); - expect(handler1).toHaveBeenCalledWith(new Error('bar')); + expect(handler1).toHaveBeenCalledWith(new Error('bar'), undefined); }); it('handles unhandled promise rejections in browsers', function() { diff --git a/src/core/GlobalErrors.js b/src/core/GlobalErrors.js index 80f486f3..03b5bccd 100644 --- a/src/core/GlobalErrors.js +++ b/src/core/GlobalErrors.js @@ -1,67 +1,35 @@ getJasmineRequireObj().GlobalErrors = function(j$) { class GlobalErrors { - #global; + #adapter; #handlers; - #originalHandlers; - #jasmineHandlers; #overrideHandler; #onRemoveOverrideHandler; - #onBrowserError; - #onBrowserRejection; - #onNodeError; - #onNodeRejection; constructor(global) { - this.#global = global || j$.getGlobal(); + global = global || j$.getGlobal(); + const dispatchError = this.#dispatchError.bind(this); + + if ( + global.process && + global.process.listeners && + j$.isFunction_(global.process.on) + ) { + this.#adapter = new NodeAdapter(global, dispatchError); + } else { + this.#adapter = new BrowserAdapter(global, dispatchError); + } this.#handlers = []; this.#overrideHandler = null; this.#onRemoveOverrideHandler = null; - - this.#onBrowserError = event => this.#dispatchError(event.error, event); - this.#onBrowserRejection = event => this.#browserRejectionHandler(event); - - this.#onNodeError = error => - this.#handleNodeEvent(error, 'uncaughtException', 'Uncaught exception'); - this.#onNodeRejection = error => - this.#handleNodeEvent( - error, - 'unhandledRejection', - 'Unhandled promise rejection' - ); - - this.#originalHandlers = {}; - this.#jasmineHandlers = {}; } install() { - if ( - this.#global.process && - this.#global.process.listeners && - j$.isFunction_(this.#global.process.on) - ) { - this.#installNodeHandler('uncaughtException', this.#onNodeError); - this.#installNodeHandler('unhandledRejection', this.#onNodeRejection); - } else { - this.#global.addEventListener('error', this.#onBrowserError); - - this.#global.addEventListener( - 'unhandledrejection', - this.#onBrowserRejection - ); - } + this.#adapter.install(); } uninstall() { - if ( - this.#global.process && - this.#global.process.listeners && - j$.isFunction_(this.#global.process.on) - ) { - this.#nodeUninstall(); - } else { - this.#browserUninstall(); - } + this.#adapter.uninstall(); } // The listener at the top of the stack will be called with two arguments: @@ -101,7 +69,99 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.#onRemoveOverrideHandler = null; } - #nodeUninstall() { + // Either error or event may be undefined + #dispatchError(error, event) { + if (this.#overrideHandler) { + // See discussion of spyOnGlobalErrorsAsync in base.js + this.#overrideHandler(error); + return; + } + + const handler = this.#handlers[this.#handlers.length - 1]; + + if (handler) { + handler(error, event); + } else { + throw error; + } + } + } + + class BrowserAdapter { + #global; + #dispatchError; + #onError; + #onUnhandledRejection; + + constructor(global, dispatchError) { + this.#global = global; + this.#dispatchError = dispatchError; + this.#onError = event => this.#dispatchError(event.error, event); + this.#onUnhandledRejection = this.#unhandledRejectionHandler.bind(this); + } + + install() { + this.#global.addEventListener('error', this.#onError); + + this.#global.addEventListener( + 'unhandledrejection', + this.#onUnhandledRejection + ); + } + + uninstall() { + this.#global.removeEventListener('error', this.#onError); + this.#global.removeEventListener( + 'unhandledrejection', + this.#onUnhandledRejection + ); + } + + #unhandledRejectionHandler(event) { + if (j$.isError_(event.reason)) { + event.reason.jasmineMessage = + 'Unhandled promise rejection: ' + event.reason; + this.#dispatchError(event.reason, event); + } else { + this.#dispatchError( + 'Unhandled promise rejection: ' + event.reason, + event + ); + } + } + } + + class NodeAdapter { + #global; + #dispatchError; + #originalHandlers; + #jasmineHandlers; + #onError; + #onUnhandledRejection; + + constructor(global, dispatchError) { + this.#global = global; + this.#dispatchError = dispatchError; + + this.#jasmineHandlers = {}; + this.#originalHandlers = {}; + + this.#onError = error => + this.#eventHandler(error, 'uncaughtException', 'Uncaught exception'); + this.#onUnhandledRejection = error => + this.#eventHandler( + error, + 'unhandledRejection', + 'Unhandled promise rejection' + ); + } + + install() { + this.#installHandler('uncaughtException', this.#onError); + this.#installHandler('unhandledRejection', this.#onUnhandledRejection); + } + + uninstall() { const errorTypes = Object.keys(this.#originalHandlers); for (const errorType of errorTypes) { this.#global.process.removeListener( @@ -120,45 +180,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } } - #browserUninstall() { - this.#global.removeEventListener('error', this.#onBrowserError); - this.#global.removeEventListener( - 'unhandledrejection', - this.#onBrowserRejection - ); - } - - // Either error or event may be undefined - #dispatchError(error, event) { - if (this.#overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - this.#overrideHandler(error); - return; - } - - const handler = this.#handlers[this.#handlers.length - 1]; - - if (handler) { - handler(error, event); - } else { - throw error; - } - } - - #browserRejectionHandler(event) { - if (j$.isError_(event.reason)) { - event.reason.jasmineMessage = - 'Unhandled promise rejection: ' + event.reason; - this.#dispatchError(event.reason, event); - } else { - this.#dispatchError( - 'Unhandled promise rejection: ' + event.reason, - event - ); - } - } - - #installNodeHandler(errorType, handler) { + #installHandler(errorType, handler) { this.#originalHandlers[errorType] = this.#global.process.listeners( errorType ); @@ -168,7 +190,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.#global.process.on(errorType, handler); } - #handleNodeEvent(error, errorType, jasmineMessage) { + #eventHandler(error, errorType, jasmineMessage) { if (j$.isError_(error)) { error.jasmineMessage = jasmineMessage + ': ' + error; } else {