@@ -594,6 +594,49 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
594594 j$ . debugLog = function ( msg ) {
595595 j$ . getEnv ( ) . debugLog ( msg ) ;
596596 } ;
597+
598+ /**
599+ * Replaces Jasmine's global error handling with a spy. This prevents Jasmine
600+ * from treating uncaught exceptions and unhandled promise rejections
601+ * as spec failures and allows them to be inspected using the spy's
602+ * {@link Spy#calls|calls property} and related matchers such as
603+ * {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}.
604+ *
605+ * After installing the spy, spyOnGlobalErrorsAsync immediately calls its
606+ * argument, which must be an async or promise-returning function. The spy
607+ * will be passed as the first argument to that callback. Normal error
608+ * handling will be restored when the promise returned from the callback is
609+ * settled.
610+ *
611+ * Note: The JavaScript runtime may deliver uncaught error events and unhandled
612+ * rejection events asynchronously, especially in browsers. If the event
613+ * occurs after the promise returned from the callback is settled, it won't
614+ * be routed to the spy even if the underlying error occurred previously.
615+ * It's up to you to ensure that the returned promise isn't resolved until
616+ * all of the error/rejection events that you want to handle have occurred.
617+ *
618+ * You must await the return value of spyOnGlobalErrorsAsync.
619+ * @name jasmine.spyOnGlobalErrorsAsync
620+ * @function
621+ * @async
622+ * @param {AsyncFunction } fn - A function to run, during which the global error spy will be effective
623+ * @example
624+ * it('demonstrates global error spies', async function() {
625+ * await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
626+ * setTimeout(function() {
627+ * throw new Error('the expected error');
628+ * });
629+ * await new Promise(function(resolve) {
630+ * setTimeout(resolve);
631+ * });
632+ * const expected = new Error('the expected error');
633+ * expect(globalErrorSpy).toHaveBeenCalledWith(expected);
634+ * });
635+ * });
636+ */
637+ j$ . spyOnGlobalErrorsAsync = async function ( fn ) {
638+ await jasmine . getEnv ( ) . spyOnGlobalErrorsAsync ( fn ) ;
639+ } ;
597640} ;
598641
599642getJasmineRequireObj ( ) . util = function ( j$ ) {
@@ -764,13 +807,19 @@ getJasmineRequireObj().Spec = function(j$) {
764807
765808 Spec . prototype . addExpectationResult = function ( passed , data , isError ) {
766809 const expectationResult = j$ . buildExpectationResult ( data ) ;
810+
767811 if ( passed ) {
768812 this . result . passedExpectations . push ( expectationResult ) ;
769813 } else {
770814 if ( this . reportedDone ) {
771815 this . onLateError ( expectationResult ) ;
772816 } else {
773817 this . result . failedExpectations . push ( expectationResult ) ;
818+
819+ // TODO: refactor so that we don't need to override cached status
820+ if ( this . result . status ) {
821+ this . result . status = 'failed' ;
822+ }
774823 }
775824
776825 if ( this . throwOnExpectationFailure && ! isError ) {
@@ -1117,9 +1166,23 @@ getJasmineRequireObj().Env = function(j$) {
11171166 new j$ . MockDate ( global )
11181167 ) ;
11191168
1120- const runableResources = new j$ . RunableResources ( function ( ) {
1121- const r = runner . currentRunable ( ) ;
1122- return r ? r . id : null ;
1169+ const globalErrors = new j$ . GlobalErrors ( ) ;
1170+ const installGlobalErrors = ( function ( ) {
1171+ let installed = false ;
1172+ return function ( ) {
1173+ if ( ! installed ) {
1174+ globalErrors . install ( ) ;
1175+ installed = true ;
1176+ }
1177+ } ;
1178+ } ) ( ) ;
1179+
1180+ const runableResources = new j$ . RunableResources ( {
1181+ getCurrentRunableId : function ( ) {
1182+ const r = runner . currentRunable ( ) ;
1183+ return r ? r . id : null ;
1184+ } ,
1185+ globalErrors
11231186 } ) ;
11241187
11251188 let reporter ;
@@ -1226,20 +1289,9 @@ getJasmineRequireObj().Env = function(j$) {
12261289 verboseDeprecations : false
12271290 } ;
12281291
1229- let globalErrors = null ;
1230-
1231- function installGlobalErrors ( ) {
1232- if ( globalErrors ) {
1233- return ;
1234- }
1235-
1236- globalErrors = new j$ . GlobalErrors ( ) ;
1237- globalErrors . install ( ) ;
1238- }
1239-
12401292 if ( ! options . suppressLoadErrors ) {
12411293 installGlobalErrors ( ) ;
1242- globalErrors . pushListener ( function (
1294+ globalErrors . pushListener ( function loadtimeErrorHandler (
12431295 message ,
12441296 filename ,
12451297 lineno ,
@@ -1712,6 +1764,47 @@ getJasmineRequireObj().Env = function(j$) {
17121764 ) ;
17131765 } ;
17141766
1767+ this . spyOnGlobalErrorsAsync = async function ( fn ) {
1768+ const spy = this . createSpy ( 'global error handler' ) ;
1769+ const associatedRunable = runner . currentRunable ( ) ;
1770+ let cleanedUp = false ;
1771+
1772+ globalErrors . setOverrideListener ( spy , ( ) => {
1773+ if ( ! cleanedUp ) {
1774+ const message =
1775+ 'Global error spy was not uninstalled. (Did you ' +
1776+ 'forget to await the return value of spyOnGlobalErrorsAsync?)' ;
1777+ associatedRunable . addExpectationResult ( false , {
1778+ matcherName : '' ,
1779+ passed : false ,
1780+ expected : '' ,
1781+ actual : '' ,
1782+ message,
1783+ error : null
1784+ } ) ;
1785+ }
1786+
1787+ cleanedUp = true ;
1788+ } ) ;
1789+
1790+ try {
1791+ const maybePromise = fn ( spy ) ;
1792+
1793+ if ( ! j$ . isPromiseLike ( maybePromise ) ) {
1794+ throw new Error (
1795+ 'The callback to spyOnGlobalErrorsAsync must be an async or promise-returning function'
1796+ ) ;
1797+ }
1798+
1799+ await maybePromise ;
1800+ } finally {
1801+ if ( ! cleanedUp ) {
1802+ cleanedUp = true ;
1803+ globalErrors . removeOverrideListener ( ) ;
1804+ }
1805+ }
1806+ } ;
1807+
17151808 function ensureIsNotNested ( method ) {
17161809 const runable = runner . currentRunable ( ) ;
17171810 if ( runable !== null && runable !== undefined ) {
@@ -3853,18 +3946,26 @@ getJasmineRequireObj().formatErrorMsg = function() {
38533946
38543947getJasmineRequireObj ( ) . GlobalErrors = function ( j$ ) {
38553948 function GlobalErrors ( global ) {
3856- const handlers = [ ] ;
38573949 global = global || j$ . getGlobal ( ) ;
38583950
3859- const onerror = function onerror ( ) {
3951+ const handlers = [ ] ;
3952+ let overrideHandler = null ,
3953+ onRemoveOverrideHandler = null ;
3954+
3955+ function onerror ( message , source , lineno , colno , error ) {
3956+ if ( overrideHandler ) {
3957+ overrideHandler ( error || message ) ;
3958+ return ;
3959+ }
3960+
38603961 const handler = handlers [ handlers . length - 1 ] ;
38613962
38623963 if ( handler ) {
38633964 handler . apply ( null , Array . prototype . slice . call ( arguments , 0 ) ) ;
38643965 } else {
38653966 throw arguments [ 0 ] ;
38663967 }
3867- } ;
3968+ }
38683969
38693970 this . originalHandlers = { } ;
38703971 this . jasmineHandlers = { } ;
@@ -3895,6 +3996,11 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
38953996
38963997 const handler = handlers [ handlers . length - 1 ] ;
38973998
3999+ if ( overrideHandler ) {
4000+ overrideHandler ( error ) ;
4001+ return ;
4002+ }
4003+
38984004 if ( handler ) {
38994005 handler ( error ) ;
39004006 } else {
@@ -3979,6 +4085,24 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
39794085
39804086 handlers . pop ( ) ;
39814087 } ;
4088+
4089+ this . setOverrideListener = function ( listener , onRemove ) {
4090+ if ( overrideHandler ) {
4091+ throw new Error ( "Can't set more than one override listener at a time" ) ;
4092+ }
4093+
4094+ overrideHandler = listener ;
4095+ onRemoveOverrideHandler = onRemove ;
4096+ } ;
4097+
4098+ this . removeOverrideListener = function ( ) {
4099+ if ( onRemoveOverrideHandler ) {
4100+ onRemoveOverrideHandler ( ) ;
4101+ }
4102+
4103+ overrideHandler = null ;
4104+ onRemoveOverrideHandler = null ;
4105+ } ;
39824106 }
39834107
39844108 return GlobalErrors ;
@@ -8083,9 +8207,10 @@ getJasmineRequireObj().interface = function(jasmine, env) {
80838207
80848208getJasmineRequireObj ( ) . RunableResources = function ( j$ ) {
80858209 class RunableResources {
8086- constructor ( getCurrentRunableId ) {
8210+ constructor ( options ) {
80878211 this . byRunableId_ = { } ;
8088- this . getCurrentRunableId_ = getCurrentRunableId ;
8212+ this . getCurrentRunableId_ = options . getCurrentRunableId ;
8213+ this . globalErrors_ = options . globalErrors ;
80898214
80908215 this . spyFactory = new j$ . SpyFactory (
80918216 ( ) => {
@@ -8136,6 +8261,7 @@ getJasmineRequireObj().RunableResources = function(j$) {
81368261 }
81378262
81388263 clearForRunable ( runableId ) {
8264+ this . globalErrors_ . removeOverrideListener ( ) ;
81398265 this . spyRegistry . clearSpies ( ) ;
81408266 delete this . byRunableId_ [ runableId ] ;
81418267 }
@@ -9597,6 +9723,11 @@ getJasmineRequireObj().Suite = function(j$) {
95979723 this . onLateError ( expectationResult ) ;
95989724 } else {
95999725 this . result . failedExpectations . push ( expectationResult ) ;
9726+
9727+ // TODO: refactor so that we don't need to override cached status
9728+ if ( this . result . status ) {
9729+ this . result . status = 'failed' ;
9730+ }
96009731 }
96019732
96029733 if ( this . throwOnExpectationFailure ) {
0 commit comments