diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 166ffdb6..b210638f 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1291,20 +1291,14 @@ getJasmineRequireObj().Env = function(j$) { if (!options.suppressLoadErrors) { installGlobalErrors(); - globalErrors.pushListener(function loadtimeErrorHandler( - message, - filename, - lineno, - colNo, - err - ) { + globalErrors.pushListener(function loadtimeErrorHandler(error, event) { topSuite.result.failedExpectations.push({ passed: false, globalErrorType: 'load', - message: message, - stack: err && err.stack, - filename: filename, - lineno: lineno + message: error ? error.message : event.message, + stack: error && error.stack, + filename: event && event.filename, + lineno: event && event.lineno }); }); } @@ -3978,18 +3972,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) { let overrideHandler = null, onRemoveOverrideHandler = null; - function onerror(message, source, lineno, colno, error) { + function onBrowserError(event) { + dispatchBrowserError(event.error, event); + } + + function dispatchBrowserError(error, event) { if (overrideHandler) { - overrideHandler(error || message); + overrideHandler(error); return; } const handler = handlers[handlers.length - 1]; if (handler) { - handler.apply(null, Array.prototype.slice.call(arguments, 0)); + handler(error, event); } else { - throw arguments[0]; + throw error; } } @@ -4066,8 +4064,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.installOne_('uncaughtException', 'Uncaught exception'); this.installOne_('unhandledRejection', 'Unhandled promise rejection'); } else { - const originalHandler = global.onerror; - global.onerror = onerror; + global.addEventListener('error', onBrowserError); const browserRejectionHandler = function browserRejectionHandler( event @@ -4075,16 +4072,19 @@ getJasmineRequireObj().GlobalErrors = function(j$) { if (j$.isError_(event.reason)) { event.reason.jasmineMessage = 'Unhandled promise rejection: ' + event.reason; - global.onerror(event.reason); + dispatchBrowserError(event.reason, event); } else { - global.onerror('Unhandled promise rejection: ' + event.reason); + dispatchBrowserError( + 'Unhandled promise rejection: ' + event.reason, + event + ); } }; global.addEventListener('unhandledrejection', browserRejectionHandler); this.uninstall = function uninstall() { - global.onerror = originalHandler; + global.removeEventListener('error', onBrowserError); global.removeEventListener( 'unhandledrejection', browserRejectionHandler @@ -4093,6 +4093,13 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } }; + // 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. + // The error will normally be provided, but will be falsy in the case of + // some browser load-time errors. The event will normally be provided in + // 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); }; @@ -7490,11 +7497,8 @@ getJasmineRequireObj().QueueRunner = function(j$) { } QueueRunner.prototype.execute = function() { - this.handleFinalError = (message, source, lineno, colno, error) => { - // Older browsers would send the error as the first parameter. HTML5 - // specifies the the five parameters above. The error instance should - // be preffered, otherwise the call stack would get lost. - this.onException(error || message); + this.handleFinalError = error => { + this.onException(error); }; this.globalErrors.pushListener(this.handleFinalError); this.run(0); @@ -10448,5 +10452,5 @@ getJasmineRequireObj().UserContext = function(j$) { }; getJasmineRequireObj().version = function() { - return '4.3.0'; + return '5.0.0-dev.0'; }; diff --git a/package.json b/package.json index fd4eb305..facedf00 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jasmine-core", "license": "MIT", - "version": "4.3.0", + "version": "5.0.0-dev.0", "repository": { "type": "git", "url": "https://github.com/jasmine/jasmine.git" diff --git a/spec/core/GlobalErrorsSpec.js b/spec/core/GlobalErrorsSpec.js index dfb714ad..68189e90 100644 --- a/spec/core/GlobalErrorsSpec.js +++ b/spec/core/GlobalErrorsSpec.js @@ -1,56 +1,42 @@ describe('GlobalErrors', function() { it('calls the added handler on error', function() { - const fakeGlobal = minimalBrowserGlobal(); + const fakeGlobal = browserGlobal(); const handler = jasmine.createSpy('errorHandler'); const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler); - fakeGlobal.onerror('foo'); - - expect(handler).toHaveBeenCalledWith('foo'); - }); - - it('enables external interception of error by overriding global.onerror', function() { - const fakeGlobal = minimalBrowserGlobal(); - const handler = jasmine.createSpy('errorHandler'); - const hijackHandler = jasmine.createSpy('hijackErrorHandler'); - const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - - errors.install(); - errors.pushListener(handler); - - fakeGlobal.onerror = hijackHandler; - - fakeGlobal.onerror('foo'); - - expect(hijackHandler).toHaveBeenCalledWith('foo'); - expect(handler).not.toHaveBeenCalled(); - }); - - it('calls the global error handler with all parameters', function() { - const fakeGlobal = minimalBrowserGlobal(); - const handler = jasmine.createSpy('errorHandler'); - const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - const fooError = new Error('foo'); - - errors.install(); - errors.pushListener(handler); - - fakeGlobal.onerror(fooError.message, 'foo.js', 1, 1, fooError); + const error = new Error('nope'); + dispatchErrorEvent(fakeGlobal, { error }); expect(handler).toHaveBeenCalledWith( - fooError.message, - 'foo.js', - 1, - 1, - fooError + jasmine.is(error), + jasmine.objectContaining({ error: jasmine.is(error) }) + ); + }); + + it('is not affected by overriding global.onerror', function() { + const fakeGlobal = browserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + + errors.install(); + errors.pushListener(handler); + + fakeGlobal.onerror = () => {}; + + const error = new Error('nope'); + dispatchErrorEvent(fakeGlobal, { error }); + + expect(handler).toHaveBeenCalledWith( + jasmine.is(error), + jasmine.objectContaining({ error: jasmine.is(error) }) ); }); it('only calls the most recent handler', function() { - const fakeGlobal = minimalBrowserGlobal(); + const fakeGlobal = browserGlobal(); const handler1 = jasmine.createSpy('errorHandler1'); const handler2 = jasmine.createSpy('errorHandler2'); const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); @@ -59,14 +45,18 @@ describe('GlobalErrors', function() { errors.pushListener(handler1); errors.pushListener(handler2); - fakeGlobal.onerror('foo'); + const error = new Error('nope'); + dispatchErrorEvent(fakeGlobal, { error }); expect(handler1).not.toHaveBeenCalled(); - expect(handler2).toHaveBeenCalledWith('foo'); + expect(handler2).toHaveBeenCalledWith( + jasmine.is(error), + jasmine.objectContaining({ error: jasmine.is(error) }) + ); }); it('calls previous handlers when one is removed', function() { - const fakeGlobal = minimalBrowserGlobal(); + const fakeGlobal = browserGlobal(); const handler1 = jasmine.createSpy('errorHandler1'); const handler2 = jasmine.createSpy('errorHandler2'); const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); @@ -77,9 +67,13 @@ describe('GlobalErrors', function() { errors.popListener(handler2); - fakeGlobal.onerror('foo'); + const error = new Error('nope'); + dispatchErrorEvent(fakeGlobal, { error }); - expect(handler1).toHaveBeenCalledWith('foo'); + expect(handler1).toHaveBeenCalledWith( + jasmine.is(error), + jasmine.objectContaining({ error: jasmine.is(error) }) + ); expect(handler2).not.toHaveBeenCalled(); }); @@ -90,34 +84,27 @@ describe('GlobalErrors', function() { }).toThrowError('popListener expects a listener'); }); - it('uninstalls itself, putting back a previous callback', function() { - const originalCallback = jasmine.createSpy('error'); - const fakeGlobal = { - ...minimalBrowserGlobal(), - onerror: originalCallback - }; + it('uninstalls itself', function() { + const fakeGlobal = browserGlobal(); const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - - expect(fakeGlobal.onerror).toBe(originalCallback); + function unrelatedListener() {} errors.install(); - - expect(fakeGlobal.onerror).not.toBe(originalCallback); - + fakeGlobal.addEventListener('error', unrelatedListener); errors.uninstall(); - expect(fakeGlobal.onerror).toBe(originalCallback); + expect(fakeGlobal.listeners_.error).toEqual([unrelatedListener]); }); it('rethrows the original error when there is no handler', function() { - const fakeGlobal = minimalBrowserGlobal(); + const fakeGlobal = browserGlobal(); const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); const originalError = new Error('nope'); errors.install(); try { - fakeGlobal.onerror(originalError); + dispatchErrorEvent(fakeGlobal, { error: originalError }); } catch (e) { expect(e).toBe(originalError); } @@ -289,128 +276,61 @@ describe('GlobalErrors', function() { describe('Reporting unhandled promise rejections in the browser', function() { it('subscribes and unsubscribes from the unhandledrejection event', function() { - const fakeGlobal = jasmine.createSpyObj('globalErrors', [ - 'addEventListener', - 'removeEventListener', - 'onerror' - ]), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = browserGlobal(); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); - expect(fakeGlobal.addEventListener).toHaveBeenCalledWith( - 'unhandledrejection', + expect(fakeGlobal.listeners_.unhandledrejection).toEqual([ jasmine.any(Function) - ); + ]); - const addedListener = fakeGlobal.addEventListener.calls.argsFor(0)[1]; errors.uninstall(); - - expect(fakeGlobal.removeEventListener).toHaveBeenCalledWith( - 'unhandledrejection', - addedListener - ); + expect(fakeGlobal.listeners_.unhandledrejection).toEqual([]); }); it('reports rejections whose reason is a string', function() { - const fakeGlobal = jasmine.createSpyObj('globalErrors', [ - 'addEventListener', - 'removeEventListener', - 'onerror' - ]), - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = browserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler); - const addedListener = fakeGlobal.addEventListener.calls.argsFor(0)[1]; - addedListener({ reason: 'nope' }); + const event = { reason: 'nope' }; + dispatchUnhandledRejectionEvent(fakeGlobal, event); - expect(handler).toHaveBeenCalledWith('Unhandled promise rejection: nope'); + expect(handler).toHaveBeenCalledWith( + 'Unhandled promise rejection: nope', + event + ); }); it('reports rejections whose reason is an Error', function() { - const fakeGlobal = jasmine.createSpyObj('globalErrors', [ - 'addEventListener', - 'removeEventListener', - 'onerror' - ]), - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = browserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler); - const addedListener = fakeGlobal.addEventListener.calls.argsFor(0)[1]; const reason = new Error('bar'); - - addedListener({ reason: reason }); + const event = { reason }; + dispatchUnhandledRejectionEvent(fakeGlobal, event); expect(handler).toHaveBeenCalledWith( jasmine.objectContaining({ jasmineMessage: 'Unhandled promise rejection: Error: bar', message: reason.message, stack: reason.stack - }) + }), + event ); }); - - describe('Enabling external interception of reported rejections by overriding global.onerror', function() { - it('overriding global.onerror intercepts rejections whose reason is a string', function() { - const fakeGlobal = jasmine.createSpyObj('globalErrors', [ - 'addEventListener' - ]), - handler = jasmine.createSpy('errorHandler'), - hijackHandler = jasmine.createSpy('hijackErrorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - - errors.install(); - errors.pushListener(handler); - - fakeGlobal.onerror = hijackHandler; - - const addedListener = fakeGlobal.addEventListener.calls.argsFor(0)[1]; - addedListener({ reason: 'nope' }); - - expect(hijackHandler).toHaveBeenCalledWith( - 'Unhandled promise rejection: nope' - ); - expect(handler).not.toHaveBeenCalled(); - }); - - it('overriding global.onerror intercepts rejections whose reason is an Error', function() { - const fakeGlobal = jasmine.createSpyObj('globalErrors', [ - 'addEventListener' - ]), - handler = jasmine.createSpy('errorHandler'), - hijackHandler = jasmine.createSpy('hijackErrorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - - errors.install(); - errors.pushListener(handler); - - fakeGlobal.onerror = hijackHandler; - - const addedListener = fakeGlobal.addEventListener.calls.argsFor(0)[1]; - const reason = new Error('bar'); - - addedListener({ reason: reason }); - - expect(hijackHandler).toHaveBeenCalledWith( - jasmine.objectContaining({ - jasmineMessage: 'Unhandled promise rejection: Error: bar', - message: reason.message, - stack: reason.stack - }) - ); - expect(handler).not.toHaveBeenCalled(); - }); - }); }); describe('#setOverrideListener', function() { it('overrides the existing handlers in browsers until removed', function() { - const fakeGlobal = minimalBrowserGlobal(); + const fakeGlobal = browserGlobal(); const handler0 = jasmine.createSpy('handler0'); const handler1 = jasmine.createSpy('handler1'); const overrideHandler = jasmine.createSpy('overrideHandler'); @@ -420,19 +340,18 @@ describe('GlobalErrors', function() { errors.pushListener(handler0); errors.setOverrideListener(overrideHandler, () => {}); errors.pushListener(handler1); - fakeGlobal.onerror('foo'); - fakeGlobal.onerror(null, null, null, null, new Error('bar')); + dispatchErrorEvent(fakeGlobal, { error: 'foo' }); expect(overrideHandler).toHaveBeenCalledWith('foo'); - expect(overrideHandler).toHaveBeenCalledWith(new Error('bar')); expect(handler0).not.toHaveBeenCalled(); expect(handler1).not.toHaveBeenCalled(); errors.removeOverrideListener(); - fakeGlobal.onerror('baz'); + const event = { error: 'baz' }; + dispatchErrorEvent(fakeGlobal, event); expect(overrideHandler).not.toHaveBeenCalledWith('baz'); - expect(handler1).toHaveBeenCalledWith('baz'); + expect(handler1).toHaveBeenCalledWith('baz', event); }); it('overrides the existing handlers in Node until removed', function() { @@ -532,7 +451,7 @@ describe('GlobalErrors', function() { }); it('throws if there is already an override handler', function() { - const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); + const errors = new jasmineUnderTest.GlobalErrors(browserGlobal()); errors.setOverrideListener(() => {}, () => {}); expect(function() { @@ -544,7 +463,7 @@ describe('GlobalErrors', function() { describe('#removeOverrideListener', function() { it("calls the handler's onRemove callback", function() { const onRemove = jasmine.createSpy('onRemove'); - const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); + const errors = new jasmineUnderTest.GlobalErrors(browserGlobal()); errors.setOverrideListener(() => {}, onRemove); errors.removeOverrideListener(); @@ -553,17 +472,43 @@ describe('GlobalErrors', function() { }); it('does not throw if there is no handler', function() { - const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); + const errors = new jasmineUnderTest.GlobalErrors(browserGlobal()); expect(() => errors.removeOverrideListener()).not.toThrow(); }); }); - function minimalBrowserGlobal() { + function browserGlobal() { return { - addEventListener() {}, - removeEventListener() {}, - onerror: null + listeners_: { error: [], unhandledrejection: [] }, + addEventListener(eventName, listener) { + this.listeners_[eventName].push(listener); + }, + removeEventListener(eventName, listener) { + this.listeners_[eventName] = this.listeners_[eventName].filter( + l => l !== listener + ); + } }; } + + function dispatchErrorEvent(global, event) { + expect(global.listeners_.error.length) + .withContext('number of error listeners') + .toBeGreaterThan(0); + + for (const l of global.listeners_.error) { + l(event); + } + } + + function dispatchUnhandledRejectionEvent(global, event) { + expect(global.listeners_.unhandledrejection.length) + .withContext('number of unhandledrejection listeners') + .toBeGreaterThan(0); + + for (const l of global.listeners_.unhandledrejection) { + l(event); + } + } }); diff --git a/spec/core/QueueRunnerSpec.js b/spec/core/QueueRunnerSpec.js index bf00c12b..de33eeb5 100644 --- a/spec/core/QueueRunnerSpec.js +++ b/spec/core/QueueRunnerSpec.js @@ -651,7 +651,7 @@ describe('QueueRunner', function() { }); }); - it('passes the error instance to exception handlers in HTML browsers', function() { + it('passes final errors to exception handlers', function() { const error = new Error('fake error'), onExceptionCallback = jasmine.createSpy('on exception callback'), queueRunner = new jasmineUnderTest.QueueRunner({ @@ -659,24 +659,11 @@ describe('QueueRunner', function() { }); queueRunner.execute(); - queueRunner.handleFinalError(error.message, 'fake.js', 1, 1, error); + queueRunner.handleFinalError(error); expect(onExceptionCallback).toHaveBeenCalledWith(error); }); - it('passes the first argument to exception handlers for compatibility', function() { - const error = new Error('fake error'), - onExceptionCallback = jasmine.createSpy('on exception callback'), - queueRunner = new jasmineUnderTest.QueueRunner({ - onException: onExceptionCallback - }); - - queueRunner.execute(); - queueRunner.handleFinalError(error.message); - - expect(onExceptionCallback).toHaveBeenCalledWith(error.message); - }); - it('calls exception handlers when an exception is thrown in a fn', function() { const queueableFn = { type: 'queueable', diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index b61322d2..bfaa8a51 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -1,5 +1,6 @@ describe('Env integration', function() { let env; + const isBrowser = typeof window !== 'undefined'; beforeEach(function() { jasmine.getEnv().registerIntegrationMatchers(); @@ -455,7 +456,7 @@ describe('Env integration', function() { env.describe('A suite', function() { env.it('fails', function(specDone) { setTimeout(function() { - global.onerror('fail'); + dispatchErrorEvent(global, { error: 'fail' }); specDone(); }); }); @@ -509,10 +510,14 @@ describe('Env integration', function() { }, specDone: function() { clearStackCallbacks[clearStackCallCount + 1] = function() { - global.onerror('fail at the end of the reporter queue'); + dispatchErrorEvent(global, { + error: 'fail at the end of the reporter queue' + }); }; clearStackCallbacks[clearStackCallCount + 2] = function() { - global.onerror('fail at the end of the spec queue'); + dispatchErrorEvent(global, { + error: 'fail at the end of the spec queue' + }); }; } }); @@ -559,7 +564,7 @@ describe('Env integration', function() { specDone(); queueMicrotask(function() { queueMicrotask(function() { - global.onerror('fail'); + dispatchErrorEvent(global, { error: 'fail' }); }); }); }); @@ -622,10 +627,14 @@ describe('Env integration', function() { if (result.description === 'A nested suite') { clearStackCallbacks[clearStackCallCount + 1] = function() { - global.onerror('fail at the end of the reporter queue'); + dispatchErrorEvent(global, { + error: 'fail at the end of the reporter queue' + }); }; clearStackCallbacks[clearStackCallCount + 2] = function() { - global.onerror('fail at the end of the suite queue'); + dispatchErrorEvent(global, { + error: 'fail at the end of the suite queue' + }); }; } } @@ -668,7 +677,7 @@ describe('Env integration', function() { env.addReporter({ jasmineDone: function() { - global.onerror('a very late error'); + dispatchErrorEvent(global, { error: 'a very late error' }); } }); @@ -720,7 +729,7 @@ describe('Env integration', function() { expectedErrors.push(`${msg} thrown`); } - global.onerror(msg); + dispatchErrorEvent(global, { error: msg }); realClearStack(fn); }); spyOn(console, 'error'); @@ -2524,15 +2533,24 @@ describe('Env integration', function() { ); }); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(2); + expect(globalErrorSpy).toHaveBeenCalledWith(new Error('suite')); + expect(globalErrorSpy).toHaveBeenCalledWith(new Error('spec')); + } + }); expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable( 'async suite', - [/^(((Uncaught )?(exception: )?Error: suite( thrown)?)|(suite thrown))$/] + [/Error: suite/] ); expect(reporter.specDone).toHaveFailedExpectationsForRunnable( 'suite async spec', - [/^(((Uncaught )?(exception: )?Error: spec( thrown)?)|(spec thrown))$/] + [/Error: spec/] ); }); @@ -2666,14 +2684,14 @@ describe('Env integration', function() { ]); env.addReporter(reporter); - global.onerror( - 'Uncaught SyntaxError: Unexpected end of input', - 'borkenSpec.js', - 42, - undefined, - { stack: 'a stack' } - ); - global.onerror('Uncaught Error: ENOCHEESE'); + dispatchErrorEvent(global, { + message: 'Uncaught SyntaxError: Unexpected end of input', + error: undefined, + filename: 'borkenSpec.js', + lineno: 42 + }); + const error = new Error('ENOCHEESE'); + dispatchErrorEvent(global, { error }); await env.execute(); @@ -2683,15 +2701,15 @@ describe('Env integration', function() { passed: false, globalErrorType: 'load', message: 'Uncaught SyntaxError: Unexpected end of input', - stack: 'a stack', + stack: undefined, filename: 'borkenSpec.js', lineno: 42 }, { passed: false, globalErrorType: 'load', - message: 'Uncaught Error: ENOCHEESE', - stack: undefined, + message: 'ENOCHEESE', + stack: error.stack, filename: undefined, lineno: undefined } @@ -2923,7 +2941,7 @@ describe('Env integration', function() { env.addReporter(reporter); env.it('passes', function() {}); - global.onerror('Uncaught Error: ENOCHEESE'); + dispatchErrorEvent(global, { error: 'ENOCHEESE' }); await env.execute(); expect(reporter.jasmineDone).toHaveBeenCalled(); @@ -3702,7 +3720,16 @@ describe('Env integration', function() { const reporter = jasmine.createSpyObj('reporter', ['specDone']); env.addReporter(reporter); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(2); + expect(globalErrorSpy).toHaveBeenCalledWith(new Error('nope')); + expect(globalErrorSpy).toHaveBeenCalledWith(new Error('yep')); + } + }); const passingResult = resultForRunable( reporter.specDone, @@ -3749,7 +3776,17 @@ describe('Env integration', function() { 'suiteDone' ]); env.addReporter(reporter); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(1); + expect(globalErrorSpy).toHaveBeenCalledWith( + new Error('should fail the spec') + ); + } + }); const suiteResult = resultForRunable(reporter.suiteDone, 'Suite 1'); expect(suiteResult.status).toEqual('failed'); @@ -3794,7 +3831,17 @@ describe('Env integration', function() { 'suiteDone' ]); env.addReporter(reporter); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(1); + expect(globalErrorSpy).toHaveBeenCalledWith( + new Error('should fail the spec') + ); + } + }); expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable( 'Suite 1', @@ -3844,7 +3891,18 @@ describe('Env integration', function() { 'suiteDone' ]); env.addReporter(reporter); - await env.execute(); + + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(1); + expect(globalErrorSpy).toHaveBeenCalledWith( + new Error('should fail the spec') + ); + } + }); const spec1Result = resultForRunable(reporter.specDone, 'Suite 1 a spec'); expect(spec1Result.status).toEqual('failed'); @@ -3880,7 +3938,17 @@ describe('Env integration', function() { const reporter = jasmine.createSpyObj('reporter', ['specDone']); env.addReporter(reporter); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(1); + expect(globalErrorSpy).toHaveBeenCalledWith( + new Error('should fail the spec') + ); + } + }); const spec1Result = resultForRunable(reporter.specDone, 'spec 1'); expect(spec1Result.status).toEqual('failed'); @@ -3925,7 +3993,17 @@ describe('Env integration', function() { 'suiteDone' ]); env.addReporter(reporter); - await env.execute(); + await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + await env.execute(); + + if (isBrowser) { + // Verify that there were no unexpected errors + expect(globalErrorSpy).toHaveBeenCalledTimes(1); + expect(globalErrorSpy).toHaveBeenCalledWith( + new Error('should fail the spec') + ); + } + }); const spec1Result = resultForRunable(reporter.specDone, 'Suite 1 a spec'); expect(spec1Result.status).toEqual('failed'); @@ -3945,8 +4023,25 @@ describe('Env integration', function() { function browserEventMethods() { return { - addEventListener() {}, - removeEventListener() {} + listeners_: { error: [], unhandledrejection: [] }, + addEventListener(eventName, listener) { + this.listeners_[eventName].push(listener); + }, + removeEventListener(eventName, listener) { + this.listeners_[eventName] = this.listeners_[eventName].filter( + l => l !== listener + ); + } }; } + + function dispatchErrorEvent(global, event) { + expect(global.listeners_.error.length) + .withContext('number of error listeners') + .toBeGreaterThan(0); + + for (const l of global.listeners_.error) { + l(event); + } + } }); diff --git a/src/core/Env.js b/src/core/Env.js index a9905328..3233daf7 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -149,20 +149,14 @@ getJasmineRequireObj().Env = function(j$) { if (!options.suppressLoadErrors) { installGlobalErrors(); - globalErrors.pushListener(function loadtimeErrorHandler( - message, - filename, - lineno, - colNo, - err - ) { + globalErrors.pushListener(function loadtimeErrorHandler(error, event) { topSuite.result.failedExpectations.push({ passed: false, globalErrorType: 'load', - message: message, - stack: err && err.stack, - filename: filename, - lineno: lineno + message: error ? error.message : event.message, + stack: error && error.stack, + filename: event && event.filename, + lineno: event && event.lineno }); }); } diff --git a/src/core/GlobalErrors.js b/src/core/GlobalErrors.js index 11b9697f..f5d6524e 100644 --- a/src/core/GlobalErrors.js +++ b/src/core/GlobalErrors.js @@ -6,18 +6,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) { let overrideHandler = null, onRemoveOverrideHandler = null; - function onerror(message, source, lineno, colno, error) { + function onBrowserError(event) { + dispatchBrowserError(event.error, event); + } + + function dispatchBrowserError(error, event) { if (overrideHandler) { - overrideHandler(error || message); + overrideHandler(error); return; } const handler = handlers[handlers.length - 1]; if (handler) { - handler.apply(null, Array.prototype.slice.call(arguments, 0)); + handler(error, event); } else { - throw arguments[0]; + throw error; } } @@ -94,8 +98,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) { this.installOne_('uncaughtException', 'Uncaught exception'); this.installOne_('unhandledRejection', 'Unhandled promise rejection'); } else { - const originalHandler = global.onerror; - global.onerror = onerror; + global.addEventListener('error', onBrowserError); const browserRejectionHandler = function browserRejectionHandler( event @@ -103,16 +106,19 @@ getJasmineRequireObj().GlobalErrors = function(j$) { if (j$.isError_(event.reason)) { event.reason.jasmineMessage = 'Unhandled promise rejection: ' + event.reason; - global.onerror(event.reason); + dispatchBrowserError(event.reason, event); } else { - global.onerror('Unhandled promise rejection: ' + event.reason); + dispatchBrowserError( + 'Unhandled promise rejection: ' + event.reason, + event + ); } }; global.addEventListener('unhandledrejection', browserRejectionHandler); this.uninstall = function uninstall() { - global.onerror = originalHandler; + global.removeEventListener('error', onBrowserError); global.removeEventListener( 'unhandledrejection', browserRejectionHandler @@ -121,6 +127,13 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } }; + // 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. + // The error will normally be provided, but will be falsy in the case of + // some browser load-time errors. The event will normally be provided in + // 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); }; diff --git a/src/core/QueueRunner.js b/src/core/QueueRunner.js index 0d7001c7..af7c63cd 100644 --- a/src/core/QueueRunner.js +++ b/src/core/QueueRunner.js @@ -66,11 +66,8 @@ getJasmineRequireObj().QueueRunner = function(j$) { } QueueRunner.prototype.execute = function() { - this.handleFinalError = (message, source, lineno, colno, error) => { - // Older browsers would send the error as the first parameter. HTML5 - // specifies the the five parameters above. The error instance should - // be preffered, otherwise the call stack would get lost. - this.onException(error || message); + this.handleFinalError = error => { + this.onException(error); }; this.globalErrors.pushListener(this.handleFinalError); this.run(0);