From d53d2ff3eb5210cdf4e9091553bba4a3d9e093b1 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Fri, 11 Jul 2025 06:49:09 -0700 Subject: [PATCH] Convert GlobalErrors to an ES6 class --- eslint.config.mjs | 4 +- lib/jasmine-core/jasmine.js | 315 ++++++++++++++++++++---------------- src/core/GlobalErrors.js | 315 ++++++++++++++++++++---------------- 3 files changed, 359 insertions(+), 275 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index afe200f2..48c20a2d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,7 +22,9 @@ export default defineConfig([{ ...globals.node, }, - ecmaVersion: 2018, + // 2022 isn't exactly right, but it's the earliest version that allows + // private properties. + ecmaVersion: 2022, sourceType: "commonjs", }, diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 08c45085..82a294df 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -4302,135 +4302,71 @@ getJasmineRequireObj().formatErrorMsg = function() { }; getJasmineRequireObj().GlobalErrors = function(j$) { - function GlobalErrors(global) { - global = global || j$.getGlobal(); + class GlobalErrors { + #global; + #handlers; + #originalHandlers; + #jasmineHandlers; + #overrideHandler; + #onRemoveOverrideHandler; + #onBrowserError; + #onBrowserRejection; + #onNodeError; + #onNodeRejection; - const handlers = []; - let overrideHandler = null, - onRemoveOverrideHandler = null; + constructor(global) { + this.#global = global || j$.getGlobal(); - function onBrowserError(event) { - dispatchBrowserError(event.error, event); + this.#handlers = []; + this.#overrideHandler = null; + this.#onRemoveOverrideHandler = null; + + this.#onBrowserError = event => + this._dispatchBrowserError(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 = {}; } - function dispatchBrowserError(error, event) { - if (overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - overrideHandler(error); - return; - } - - const handler = handlers[handlers.length - 1]; - - if (handler) { - handler(error, event); - } else { - throw error; - } - } - - this.originalHandlers = {}; - this.jasmineHandlers = {}; - this.installOne_ = function installOne_(errorType, jasmineMessage) { - function taggedOnError(error) { - if (j$.isError_(error)) { - error.jasmineMessage = jasmineMessage + ': ' + error; - } else { - let substituteMsg; - - if (error) { - substituteMsg = jasmineMessage + ': ' + error; - } else { - substituteMsg = jasmineMessage + ' with no error or message'; - } - - if (errorType === 'unhandledRejection') { - substituteMsg += - '\n' + - '(Tip: to get a useful stack trace, use ' + - 'Promise.reject(new Error(...)) instead of Promise.reject(' + - (error ? '...' : '') + - ').)'; - } - - error = new Error(substituteMsg); - } - - const handler = handlers[handlers.length - 1]; - - if (overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - overrideHandler(error); - return; - } - - if (handler) { - handler(error); - } else { - throw error; - } - } - - this.originalHandlers[errorType] = global.process.listeners(errorType); - this.jasmineHandlers[errorType] = taggedOnError; - - global.process.removeAllListeners(errorType); - global.process.on(errorType, taggedOnError); - - this.uninstall = function uninstall() { - const errorTypes = Object.keys(this.originalHandlers); - for (const errorType of errorTypes) { - global.process.removeListener( - errorType, - this.jasmineHandlers[errorType] - ); - - for (let i = 0; i < this.originalHandlers[errorType].length; i++) { - global.process.on(errorType, this.originalHandlers[errorType][i]); - } - delete this.originalHandlers[errorType]; - delete this.jasmineHandlers[errorType]; - } - }; - }; - - this.install = function install() { + install() { if ( - global.process && - global.process.listeners && - j$.isFunction_(global.process.on) + this.#global.process && + this.#global.process.listeners && + j$.isFunction_(this.#global.process.on) ) { - this.installOne_('uncaughtException', 'Uncaught exception'); - this.installOne_('unhandledRejection', 'Unhandled promise rejection'); + this._installNodeHandler('uncaughtException', this.#onNodeError); + this._installNodeHandler('unhandledRejection', this.#onNodeRejection); } else { - global.addEventListener('error', onBrowserError); + this.#global.addEventListener('error', this.#onBrowserError); - const browserRejectionHandler = function browserRejectionHandler( - event - ) { - if (j$.isError_(event.reason)) { - event.reason.jasmineMessage = - 'Unhandled promise rejection: ' + event.reason; - dispatchBrowserError(event.reason, event); - } else { - dispatchBrowserError( - 'Unhandled promise rejection: ' + event.reason, - event - ); - } - }; - - global.addEventListener('unhandledrejection', browserRejectionHandler); - - this.uninstall = function uninstall() { - global.removeEventListener('error', onBrowserError); - global.removeEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - }; + this.#global.addEventListener( + 'unhandledrejection', + this.#onBrowserRejection + ); } - }; + } + + uninstall() { + if ( + this.#global.process && + this.#global.process.listeners && + j$.isFunction_(this.#global.process.on) + ) { + this._nodeUninstall(); + } else { + this._browserUninstall(); + } + } // The listener at the top of the stack will be called with two arguments: // the error and the event. Either of them may be falsy. @@ -4439,35 +4375,140 @@ getJasmineRequireObj().GlobalErrors = function(j$) { // browsers but will be falsy in Node. // Listeners that are pushed after spec files have been loaded should be // able to just use the error parameter. - this.pushListener = function pushListener(listener) { - handlers.push(listener); - }; + pushListener(listener) { + this.#handlers.push(listener); + } - this.popListener = function popListener(listener) { + popListener(listener) { if (!listener) { throw new Error('popListener expects a listener'); } - handlers.pop(); - }; + this.#handlers.pop(); + } - this.setOverrideListener = function(listener, onRemove) { - if (overrideHandler) { + setOverrideListener(listener, onRemove) { + if (this.#overrideHandler) { throw new Error("Can't set more than one override listener at a time"); } - overrideHandler = listener; - onRemoveOverrideHandler = onRemove; - }; + this.#overrideHandler = listener; + this.#onRemoveOverrideHandler = onRemove; + } - this.removeOverrideListener = function() { - if (onRemoveOverrideHandler) { - onRemoveOverrideHandler(); + removeOverrideListener() { + if (this.#onRemoveOverrideHandler) { + this.#onRemoveOverrideHandler(); } - overrideHandler = null; - onRemoveOverrideHandler = null; - }; + this.#overrideHandler = null; + this.#onRemoveOverrideHandler = null; + } + + _nodeUninstall() { + const errorTypes = Object.keys(this.#originalHandlers); + for (const errorType of errorTypes) { + this.#global.process.removeListener( + errorType, + this.#jasmineHandlers[errorType] + ); + + for (let i = 0; i < this.#originalHandlers[errorType].length; i++) { + this.#global.process.on( + errorType, + this.#originalHandlers[errorType][i] + ); + } + delete this.#originalHandlers[errorType]; + delete this.#jasmineHandlers[errorType]; + } + } + + _browserUninstall() { + this.#global.removeEventListener('error', this.#onBrowserError); + this.#global.removeEventListener( + 'unhandledrejection', + this.#onBrowserRejection + ); + } + + _dispatchBrowserError(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._dispatchBrowserError(event.reason, event); + } else { + this._dispatchBrowserError( + 'Unhandled promise rejection: ' + event.reason, + event + ); + } + } + + _installNodeHandler(errorType, handler) { + this.#originalHandlers[errorType] = this.#global.process.listeners( + errorType + ); + this.#jasmineHandlers[errorType] = handler; + + this.#global.process.removeAllListeners(errorType); + this.#global.process.on(errorType, handler); + } + + _handleNodeEvent(error, errorType, jasmineMessage) { + if (j$.isError_(error)) { + error.jasmineMessage = jasmineMessage + ': ' + error; + } else { + let substituteMsg; + + if (error) { + substituteMsg = jasmineMessage + ': ' + error; + } else { + substituteMsg = jasmineMessage + ' with no error or message'; + } + + if (errorType === 'unhandledRejection') { + substituteMsg += + '\n' + + '(Tip: to get a useful stack trace, use ' + + 'Promise.reject(new Error(...)) instead of Promise.reject(' + + (error ? '...' : '') + + ').)'; + } + + error = new Error(substituteMsg); + } + + const handler = this.#handlers[this.#handlers.length - 1]; + + if (this.#overrideHandler) { + // See discussion of spyOnGlobalErrorsAsync in base.js + this.#overrideHandler(error); + return; + } + + if (handler) { + handler(error); + } else { + throw error; + } + } } return GlobalErrors; diff --git a/src/core/GlobalErrors.js b/src/core/GlobalErrors.js index d9d298ac..8373059a 100644 --- a/src/core/GlobalErrors.js +++ b/src/core/GlobalErrors.js @@ -1,133 +1,69 @@ getJasmineRequireObj().GlobalErrors = function(j$) { - function GlobalErrors(global) { - global = global || j$.getGlobal(); + class GlobalErrors { + #global; + #handlers; + #originalHandlers; + #jasmineHandlers; + #overrideHandler; + #onRemoveOverrideHandler; + #onBrowserError; + #onBrowserRejection; + #onNodeError; + #onNodeRejection; - const handlers = []; - let overrideHandler = null, - onRemoveOverrideHandler = null; + constructor(global) { + this.#global = global || j$.getGlobal(); - function onBrowserError(event) { - dispatchBrowserError(event.error, event); + this.#handlers = []; + this.#overrideHandler = null; + this.#onRemoveOverrideHandler = null; + + this.#onBrowserError = event => + this.#dispatchBrowserError(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 = {}; } - function dispatchBrowserError(error, event) { - if (overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - overrideHandler(error); - return; - } - - const handler = handlers[handlers.length - 1]; - - if (handler) { - handler(error, event); - } else { - throw error; - } - } - - this.originalHandlers = {}; - this.jasmineHandlers = {}; - this.installOne_ = function installOne_(errorType, jasmineMessage) { - function taggedOnError(error) { - if (j$.isError_(error)) { - error.jasmineMessage = jasmineMessage + ': ' + error; - } else { - let substituteMsg; - - if (error) { - substituteMsg = jasmineMessage + ': ' + error; - } else { - substituteMsg = jasmineMessage + ' with no error or message'; - } - - if (errorType === 'unhandledRejection') { - substituteMsg += - '\n' + - '(Tip: to get a useful stack trace, use ' + - 'Promise.reject(new Error(...)) instead of Promise.reject(' + - (error ? '...' : '') + - ').)'; - } - - error = new Error(substituteMsg); - } - - const handler = handlers[handlers.length - 1]; - - if (overrideHandler) { - // See discussion of spyOnGlobalErrorsAsync in base.js - overrideHandler(error); - return; - } - - if (handler) { - handler(error); - } else { - throw error; - } - } - - this.originalHandlers[errorType] = global.process.listeners(errorType); - this.jasmineHandlers[errorType] = taggedOnError; - - global.process.removeAllListeners(errorType); - global.process.on(errorType, taggedOnError); - - this.uninstall = function uninstall() { - const errorTypes = Object.keys(this.originalHandlers); - for (const errorType of errorTypes) { - global.process.removeListener( - errorType, - this.jasmineHandlers[errorType] - ); - - for (let i = 0; i < this.originalHandlers[errorType].length; i++) { - global.process.on(errorType, this.originalHandlers[errorType][i]); - } - delete this.originalHandlers[errorType]; - delete this.jasmineHandlers[errorType]; - } - }; - }; - - this.install = function install() { + install() { if ( - global.process && - global.process.listeners && - j$.isFunction_(global.process.on) + this.#global.process && + this.#global.process.listeners && + j$.isFunction_(this.#global.process.on) ) { - this.installOne_('uncaughtException', 'Uncaught exception'); - this.installOne_('unhandledRejection', 'Unhandled promise rejection'); + this.#installNodeHandler('uncaughtException', this.#onNodeError); + this.#installNodeHandler('unhandledRejection', this.#onNodeRejection); } else { - global.addEventListener('error', onBrowserError); + this.#global.addEventListener('error', this.#onBrowserError); - const browserRejectionHandler = function browserRejectionHandler( - event - ) { - if (j$.isError_(event.reason)) { - event.reason.jasmineMessage = - 'Unhandled promise rejection: ' + event.reason; - dispatchBrowserError(event.reason, event); - } else { - dispatchBrowserError( - 'Unhandled promise rejection: ' + event.reason, - event - ); - } - }; - - global.addEventListener('unhandledrejection', browserRejectionHandler); - - this.uninstall = function uninstall() { - global.removeEventListener('error', onBrowserError); - global.removeEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - }; + this.#global.addEventListener( + 'unhandledrejection', + this.#onBrowserRejection + ); } - }; + } + + uninstall() { + if ( + this.#global.process && + this.#global.process.listeners && + j$.isFunction_(this.#global.process.on) + ) { + this.#nodeUninstall(); + } else { + this.#browserUninstall(); + } + } // The listener at the top of the stack will be called with two arguments: // the error and the event. Either of them may be falsy. @@ -136,35 +72,140 @@ getJasmineRequireObj().GlobalErrors = function(j$) { // browsers but will be falsy in Node. // Listeners that are pushed after spec files have been loaded should be // able to just use the error parameter. - this.pushListener = function pushListener(listener) { - handlers.push(listener); - }; + pushListener(listener) { + this.#handlers.push(listener); + } - this.popListener = function popListener(listener) { + popListener(listener) { if (!listener) { throw new Error('popListener expects a listener'); } - handlers.pop(); - }; + this.#handlers.pop(); + } - this.setOverrideListener = function(listener, onRemove) { - if (overrideHandler) { + setOverrideListener(listener, onRemove) { + if (this.#overrideHandler) { throw new Error("Can't set more than one override listener at a time"); } - overrideHandler = listener; - onRemoveOverrideHandler = onRemove; - }; + this.#overrideHandler = listener; + this.#onRemoveOverrideHandler = onRemove; + } - this.removeOverrideListener = function() { - if (onRemoveOverrideHandler) { - onRemoveOverrideHandler(); + removeOverrideListener() { + if (this.#onRemoveOverrideHandler) { + this.#onRemoveOverrideHandler(); } - overrideHandler = null; - onRemoveOverrideHandler = null; - }; + this.#overrideHandler = null; + this.#onRemoveOverrideHandler = null; + } + + #nodeUninstall() { + const errorTypes = Object.keys(this.#originalHandlers); + for (const errorType of errorTypes) { + this.#global.process.removeListener( + errorType, + this.#jasmineHandlers[errorType] + ); + + for (let i = 0; i < this.#originalHandlers[errorType].length; i++) { + this.#global.process.on( + errorType, + this.#originalHandlers[errorType][i] + ); + } + delete this.#originalHandlers[errorType]; + delete this.#jasmineHandlers[errorType]; + } + } + + #browserUninstall() { + this.#global.removeEventListener('error', this.#onBrowserError); + this.#global.removeEventListener( + 'unhandledrejection', + this.#onBrowserRejection + ); + } + + #dispatchBrowserError(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.#dispatchBrowserError(event.reason, event); + } else { + this.#dispatchBrowserError( + 'Unhandled promise rejection: ' + event.reason, + event + ); + } + } + + #installNodeHandler(errorType, handler) { + this.#originalHandlers[errorType] = this.#global.process.listeners( + errorType + ); + this.#jasmineHandlers[errorType] = handler; + + this.#global.process.removeAllListeners(errorType); + this.#global.process.on(errorType, handler); + } + + #handleNodeEvent(error, errorType, jasmineMessage) { + if (j$.isError_(error)) { + error.jasmineMessage = jasmineMessage + ': ' + error; + } else { + let substituteMsg; + + if (error) { + substituteMsg = jasmineMessage + ': ' + error; + } else { + substituteMsg = jasmineMessage + ' with no error or message'; + } + + if (errorType === 'unhandledRejection') { + substituteMsg += + '\n' + + '(Tip: to get a useful stack trace, use ' + + 'Promise.reject(new Error(...)) instead of Promise.reject(' + + (error ? '...' : '') + + ').)'; + } + + error = new Error(substituteMsg); + } + + const handler = this.#handlers[this.#handlers.length - 1]; + + if (this.#overrideHandler) { + // See discussion of spyOnGlobalErrorsAsync in base.js + this.#overrideHandler(error); + return; + } + + if (handler) { + handler(error); + } else { + throw error; + } + } } return GlobalErrors;