From bc2aa7be257b99564a8db7a14a8859997b84d525 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Mon, 7 Jul 2025 07:53:56 -0700 Subject: [PATCH] Start breaking up integration/EnvSpec.js --- spec/core/integration/EnvSpec.js | 783 ----------------- .../integration/GlobalErrorHandlingSpec.js | 820 ++++++++++++++++++ 2 files changed, 820 insertions(+), 783 deletions(-) create mode 100644 spec/core/integration/GlobalErrorHandlingSpec.js diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index a887f96b..9b7f4f49 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -429,350 +429,6 @@ describe('Env integration', function() { ]); }); - describe('Handling async errors', function() { - it('routes async errors to a running spec', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - env.cleanup_(); - env = new jasmineUnderTest.Env(); - const reporter = jasmine.createSpyObj('fakeReporter', [ - 'specDone', - 'suiteDone' - ]); - - env.addReporter(reporter); - - env.describe('A suite', function() { - env.it('fails', function(specDone) { - setTimeout(function() { - dispatchErrorEvent(global, { error: 'fail' }); - specDone(); - }); - }); - }); - - await env.execute(); - - expect(reporter.specDone).toHaveFailedExpectationsForRunnable( - 'A suite fails', - ['fail thrown'] - ); - }); - - describe('When the running spec has reported specDone', function() { - it('routes async errors to an ancestor suite', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn) { - clearTimeout(fn); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - - const realClearStack = jasmineUnderTest.getClearStack(global); - const clearStackCallbacks = {}; - let clearStackCallCount = 0; - spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { - clearStackCallCount++; - - if (clearStackCallbacks[clearStackCallCount]) { - clearStackCallbacks[clearStackCallCount](); - } - - realClearStack(fn); - }); - - env.cleanup_(); - env = new jasmineUnderTest.Env(); - - let suiteErrors = []; - env.addReporter({ - suiteDone: function(result) { - const messages = result.failedExpectations.map(e => e.message); - suiteErrors = suiteErrors.concat(messages); - }, - specDone: function() { - clearStackCallbacks[clearStackCallCount + 1] = function() { - dispatchErrorEvent(global, { - error: 'fail at the end of the reporter queue' - }); - }; - clearStackCallbacks[clearStackCallCount + 2] = function() { - dispatchErrorEvent(global, { - error: 'fail at the end of the spec queue' - }); - }; - } - }); - - env.describe('A suite', function() { - env.it('is finishing when the failure occurs', function() {}); - }); - - await env.execute(); - - expect(suiteErrors).toEqual([ - 'fail at the end of the reporter queue thrown', - 'fail at the end of the spec queue thrown' - ]); - }); - }); - - it('routes async errors to a running suite', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - env.cleanup_(); - env = new jasmineUnderTest.Env(); - const reporter = jasmine.createSpyObj('fakeReporter', [ - 'specDone', - 'suiteDone' - ]); - - env.addReporter(reporter); - - env.fdescribe('A suite', function() { - env.it('fails', function(specDone) { - setTimeout(function() { - specDone(); - queueMicrotask(function() { - queueMicrotask(function() { - dispatchErrorEvent(global, { error: 'fail' }); - }); - }); - }); - }); - }); - - env.describe('Ignored', function() { - env.it('is not run', function() {}); - }); - - await env.execute(); - - expect(reporter.specDone).not.toHaveFailedExpectationsForRunnable( - 'A suite fails', - ['fail thrown'] - ); - expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable( - 'A suite', - ['fail thrown'] - ); - }); - - describe('When the running suite has reported suiteDone', function() { - it('routes async errors to an ancestor suite', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - - const realClearStack = jasmineUnderTest.getClearStack(global); - const clearStackCallbacks = {}; - let clearStackCallCount = 0; - spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { - clearStackCallCount++; - - if (clearStackCallbacks[clearStackCallCount]) { - clearStackCallbacks[clearStackCallCount](); - } - - realClearStack(fn); - }); - - env.cleanup_(); - env = new jasmineUnderTest.Env(); - - let suiteErrors = []; - env.addReporter({ - suiteDone: function(result) { - const messages = result.failedExpectations.map(e => e.message); - suiteErrors = suiteErrors.concat(messages); - - if (result.description === 'A nested suite') { - clearStackCallbacks[clearStackCallCount + 1] = function() { - dispatchErrorEvent(global, { - error: 'fail at the end of the reporter queue' - }); - }; - clearStackCallbacks[clearStackCallCount + 2] = function() { - dispatchErrorEvent(global, { - error: 'fail at the end of the suite queue' - }); - }; - } - } - }); - - env.describe('A suite', function() { - env.describe('A nested suite', function() { - env.it('a spec', function() {}); - }); - }); - - await env.execute(); - - expect(suiteErrors).toEqual([ - 'fail at the end of the reporter queue thrown', - 'fail at the end of the suite queue thrown' - ]); - }); - }); - - describe('When the env has started reporting jasmineDone', function() { - it('logs the error to the console', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - env.cleanup_(); - env = new jasmineUnderTest.Env(); - - spyOn(console, 'error'); - - env.addReporter({ - jasmineDone: function() { - dispatchErrorEvent(global, { error: 'a very late error' }); - } - }); - - env.it('a spec', function() {}); - - await env.execute(); - - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - 'Jasmine received a result after the suite finished:' - ); - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - jasmine.objectContaining({ - message: 'a very late error thrown', - globalErrorType: 'afterAll' - }) - ); - }); - }); - - it('routes all errors that occur during stack clearing somewhere', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn) { - clearTimeout(fn); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - } - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - - const realClearStack = jasmineUnderTest.getClearStack(global); - let clearStackCallCount = 0; - let jasmineDone = false; - const expectedErrors = []; - const expectedErrorsAfterJasmineDone = []; - spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { - clearStackCallCount++; - const msg = `Error in clearStack #${clearStackCallCount}`; - - if (jasmineDone) { - expectedErrorsAfterJasmineDone.push(`${msg} thrown`); - } else { - expectedErrors.push(`${msg} thrown`); - } - - dispatchErrorEvent(global, { error: msg }); - realClearStack(fn); - }); - spyOn(console, 'error'); - - env.cleanup_(); - env = new jasmineUnderTest.Env(); - - const receivedErrors = []; - function logErrors(event) { - for (const failure of event.failedExpectations) { - receivedErrors.push(failure.message); - } - } - env.addReporter({ - specDone: logErrors, - suiteDone: logErrors, - jasmineDone: function(event) { - jasmineDone = true; - logErrors(event); - } - }); - - env.describe('A suite', function() { - env.it('is finishing when the failure occurs', function() {}); - }); - - await env.execute(); - - expect(receivedErrors.length).toEqual(expectedErrors.length); - - for (const e of expectedErrors) { - expect(receivedErrors).toContain(e); - } - - for (const message of expectedErrorsAfterJasmineDone) { - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - jasmine.objectContaining({ message }) - ); - } - }); - }); - it('reports multiple calls to done in the top suite as errors', async function() { const reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']); const message = @@ -2840,104 +2496,6 @@ describe('Env integration', function() { ); }); - it('reports errors that occur during loading', async function() { - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - }, - onerror: function() {} - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - - env.cleanup_(); - env = new jasmineUnderTest.Env(); - const reporter = jasmine.createSpyObj('reporter', [ - 'jasmineDone', - 'suiteDone', - 'specDone' - ]); - - env.addReporter(reporter); - 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(); - - const e = reporter.jasmineDone.calls.argsFor(0)[0]; - expect(e.failedExpectations).toEqual([ - { - passed: false, - globalErrorType: 'load', - message: 'Uncaught SyntaxError: Unexpected end of input', - stack: undefined, - filename: 'borkenSpec.js', - lineno: 42 - }, - { - passed: false, - globalErrorType: 'load', - message: 'ENOCHEESE', - stack: error.stack, - filename: undefined, - lineno: undefined - } - ]); - }); - - describe('If suppressLoadErrors: true was passed', function() { - it('does not install a global error handler during loading', async function() { - const originalOnerror = jasmine.createSpy('original onerror'); - const global = { - ...browserEventMethods(), - setTimeout: function(fn, delay) { - return setTimeout(fn, delay); - }, - clearTimeout: function(fn, delay) { - clearTimeout(fn, delay); - }, - queueMicrotask: function(fn) { - queueMicrotask(fn); - }, - onerror: originalOnerror - }; - spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); - const globalErrors = new jasmineUnderTest.GlobalErrors(global); - const onerror = jasmine.createSpy('onerror'); - globalErrors.pushListener(onerror); - spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors); - - env.cleanup_(); - env = new jasmineUnderTest.Env({ suppressLoadErrors: true }); - const reporter = jasmine.createSpyObj('reporter', [ - 'jasmineDone', - 'suiteDone', - 'specDone' - ]); - - env.addReporter(reporter); - global.onerror('Uncaught Error: ENOCHEESE'); - - await env.execute(); - - const e = reporter.jasmineDone.calls.argsFor(0)[0]; - expect(e.failedExpectations).toEqual([]); - expect(originalOnerror).toHaveBeenCalledWith('Uncaught Error: ENOCHEESE'); - }); - }); - describe('Overall status in the jasmineDone event', function() { describe('When everything passes', function() { it('is "passed"', async function() { @@ -3884,347 +3442,6 @@ describe('Env integration', function() { ]); }); - describe('#spyOnGlobalErrorsAsync', function() { - const leftInstalledMessage = - 'Global error spy was not uninstalled. ' + - '(Did you forget to await the return value of spyOnGlobalErrorsAsync?)'; - - function resultForRunable(reporterSpy, fullName) { - const match = reporterSpy.calls.all().find(function(call) { - return call.args[0].fullName === fullName; - }); - - if (!match) { - throw new Error(`No result for runable "${fullName}"`); - } - - return match.args[0]; - } - - it('allows global errors to be suppressed and spied on', async function() { - env.it('a passing spec', async function() { - await env.spyOnGlobalErrorsAsync(async spy => { - setTimeout(() => { - throw new Error('nope'); - }); - await new Promise(resolve => setTimeout(resolve)); - env.expect(spy).toHaveBeenCalledWith(new Error('nope')); - }); - }); - - env.it('a failing spec', async function() { - await env.spyOnGlobalErrorsAsync(async spy => { - setTimeout(() => { - throw new Error('yep'); - }); - await new Promise(resolve => setTimeout(resolve)); - env.expect(spy).toHaveBeenCalledWith(new Error('nope')); - }); - }); - - const reporter = jasmine.createSpyObj('reporter', ['specDone']); - env.addReporter(reporter); - 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, - 'a passing spec' - ); - expect(passingResult.status).toEqual('passed'); - expect(passingResult.failedExpectations).toEqual([]); - - const failingResult = resultForRunable( - reporter.specDone, - 'a failing spec' - ); - expect(failingResult.status).toEqual('failed'); - expect(failingResult.failedExpectations[0].message).toMatch( - /Expected \$\[0] = Error: yep to equal Error: nope\./ - ); - }); - - it('cleans up if the global error spy is left installed in a beforeAll', async function() { - env.configure({ random: false }); - - env.describe('Suite 1', function() { - env.beforeAll(async function() { - env.spyOnGlobalErrorsAsync(function() { - // Never resolves - return new Promise(() => {}); - }); - }); - - env.it('a spec', function() {}); - }); - - env.describe('Suite 2', function() { - env.it('a spec', async function() { - setTimeout(function() { - throw new Error('should fail the spec'); - }); - await new Promise(resolve => setTimeout(resolve)); - }); - }); - - const reporter = jasmine.createSpyObj('reporter', [ - 'specDone', - 'suiteDone' - ]); - env.addReporter(reporter); - 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'); - expect(suiteResult.failedExpectations.length).toEqual(1); - expect(suiteResult.failedExpectations[0].message).toEqual( - leftInstalledMessage - ); - - const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec'); - expect(specResult.status).toEqual('failed'); - expect(specResult.failedExpectations.length).toEqual(1); - expect(specResult.failedExpectations[0].message).toMatch( - /Error: should fail the spec/ - ); - }); - - it('cleans up if the global error spy is left installed in an afterAll', async function() { - env.configure({ random: false }); - - env.describe('Suite 1', function() { - env.afterAll(async function() { - env.spyOnGlobalErrorsAsync(function() { - // Never resolves - return new Promise(() => {}); - }); - }); - - env.it('a spec', function() {}); - }); - - env.describe('Suite 2', function() { - env.it('a spec', async function() { - setTimeout(function() { - throw new Error('should fail the spec'); - }); - await new Promise(resolve => setTimeout(resolve)); - }); - }); - - const reporter = jasmine.createSpyObj('reporter', [ - 'specDone', - 'suiteDone' - ]); - env.addReporter(reporter); - 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', - [leftInstalledMessage] - ); - - const suiteResult = resultForRunable(reporter.suiteDone, 'Suite 1'); - expect(suiteResult.status).toEqual('failed'); - expect(suiteResult.failedExpectations.length).toEqual(1); - expect(suiteResult.failedExpectations[0].message).toEqual( - leftInstalledMessage - ); - - const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec'); - expect(specResult.status).toEqual('failed'); - expect(specResult.failedExpectations.length).toEqual(1); - expect(specResult.failedExpectations[0].message).toMatch( - /Error: should fail the spec/ - ); - }); - - it('cleans up if the global error spy is left installed in a beforeEach', async function() { - env.configure({ random: false }); - - env.describe('Suite 1', function() { - env.beforeEach(async function() { - env.spyOnGlobalErrorsAsync(function() { - // Never resolves - return new Promise(() => {}); - }); - }); - - env.it('a spec', function() {}); - }); - - env.describe('Suite 2', function() { - env.it('a spec', async function() { - setTimeout(function() { - throw new Error('should fail the spec'); - }); - await new Promise(resolve => setTimeout(resolve)); - }); - }); - - const reporter = jasmine.createSpyObj('reporter', [ - 'specDone', - 'suiteDone' - ]); - env.addReporter(reporter); - - 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'); - expect(spec1Result.failedExpectations.length).toEqual(1); - expect(spec1Result.failedExpectations[0].message).toEqual( - leftInstalledMessage - ); - - const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec'); - expect(spec2Result.status).toEqual('failed'); - expect(spec2Result.failedExpectations.length).toEqual(1); - expect(spec2Result.failedExpectations[0].message).toMatch( - /Error: should fail the spec/ - ); - }); - - it('cleans up if the global error spy is left installed in an it', async function() { - env.configure({ random: false }); - - env.it('spec 1', async function() { - env.spyOnGlobalErrorsAsync(function() { - // Never resolves - return new Promise(() => {}); - }); - }); - - env.it('spec 2', async function() { - setTimeout(function() { - throw new Error('should fail the spec'); - }); - await new Promise(resolve => setTimeout(resolve)); - }); - - const reporter = jasmine.createSpyObj('reporter', ['specDone']); - env.addReporter(reporter); - 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'); - expect(spec1Result.failedExpectations.length).toEqual(1); - expect(spec1Result.failedExpectations[0].message).toEqual( - leftInstalledMessage - ); - - const spec2Result = resultForRunable(reporter.specDone, 'spec 2'); - expect(spec2Result.status).toEqual('failed'); - expect(spec2Result.failedExpectations.length).toEqual(1); - expect(spec2Result.failedExpectations[0].message).toMatch( - /Error: should fail the spec/ - ); - }); - - it('cleans up if the global error spy is left installed in an afterEach', async function() { - env.configure({ random: false }); - - env.describe('Suite 1', function() { - env.afterEach(async function() { - env.spyOnGlobalErrorsAsync(function() { - // Never resolves - return new Promise(() => {}); - }); - }); - - env.it('a spec', function() {}); - }); - - env.describe('Suite 2', function() { - env.it('a spec', async function() { - setTimeout(function() { - throw new Error('should fail the spec'); - }); - await new Promise(resolve => setTimeout(resolve)); - }); - }); - - const reporter = jasmine.createSpyObj('reporter', [ - 'specDone', - 'suiteDone' - ]); - env.addReporter(reporter); - 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'); - expect(spec1Result.failedExpectations.length).toEqual(1); - expect(spec1Result.failedExpectations[0].message).toEqual( - leftInstalledMessage - ); - - const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec'); - expect(spec2Result.status).toEqual('failed'); - expect(spec2Result.failedExpectations.length).toEqual(1); - expect(spec2Result.failedExpectations[0].message).toMatch( - /Error: should fail the spec/ - ); - }); - }); - it('reports a suite level error when a describe fn throws', async function() { const reporter = jasmine.createSpyObj('reporter', ['suiteDone']); env.addReporter(reporter); diff --git a/spec/core/integration/GlobalErrorHandlingSpec.js b/spec/core/integration/GlobalErrorHandlingSpec.js new file mode 100644 index 00000000..bf08eedc --- /dev/null +++ b/spec/core/integration/GlobalErrorHandlingSpec.js @@ -0,0 +1,820 @@ +describe('Global error handling (integration)', function() { + const isBrowser = typeof window !== 'undefined'; + let env; + + beforeEach(function() { + specHelpers.registerIntegrationMatchers(); + env = new jasmineUnderTest.Env(); + }); + + afterEach(function() { + env.cleanup_(); + }); + + it('reports errors that occur during loading', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + }, + onerror: function() {} + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + + env.cleanup_(); + env = new jasmineUnderTest.Env(); + const reporter = jasmine.createSpyObj('reporter', [ + 'jasmineDone', + 'suiteDone', + 'specDone' + ]); + + env.addReporter(reporter); + 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(); + + const e = reporter.jasmineDone.calls.argsFor(0)[0]; + expect(e.failedExpectations).toEqual([ + { + passed: false, + globalErrorType: 'load', + message: 'Uncaught SyntaxError: Unexpected end of input', + stack: undefined, + filename: 'borkenSpec.js', + lineno: 42 + }, + { + passed: false, + globalErrorType: 'load', + message: 'ENOCHEESE', + stack: error.stack, + filename: undefined, + lineno: undefined + } + ]); + }); + + describe('If suppressLoadErrors: true was passed', function() { + it('does not install a global error handler during loading', async function() { + const originalOnerror = jasmine.createSpy('original onerror'); + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + }, + onerror: originalOnerror + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + const globalErrors = new jasmineUnderTest.GlobalErrors(global); + const onerror = jasmine.createSpy('onerror'); + globalErrors.pushListener(onerror); + spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors); + + env.cleanup_(); + env = new jasmineUnderTest.Env({ suppressLoadErrors: true }); + const reporter = jasmine.createSpyObj('reporter', [ + 'jasmineDone', + 'suiteDone', + 'specDone' + ]); + + env.addReporter(reporter); + global.onerror('Uncaught Error: ENOCHEESE'); + + await env.execute(); + + const e = reporter.jasmineDone.calls.argsFor(0)[0]; + expect(e.failedExpectations).toEqual([]); + expect(originalOnerror).toHaveBeenCalledWith('Uncaught Error: ENOCHEESE'); + }); + }); + + describe('Handling unhandled exceptions', function() { + it('routes unhandled exceptions to the running spec', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + env.cleanup_(); + env = new jasmineUnderTest.Env(); + const reporter = jasmine.createSpyObj('fakeReporter', [ + 'specDone', + 'suiteDone' + ]); + + env.addReporter(reporter); + + env.describe('A suite', function() { + env.it('fails', function(specDone) { + setTimeout(function() { + dispatchErrorEvent(global, { error: 'fail' }); + specDone(); + }); + }); + }); + + await env.execute(); + + expect(reporter.specDone).toHaveFailedExpectationsForRunnable( + 'A suite fails', + ['fail thrown'] + ); + }); + + describe('When the most recently running spec has reported specDone', function() { + it('routes unhandled exceptions to an ancestor suite', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn) { + clearTimeout(fn); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + + const realClearStack = jasmineUnderTest.getClearStack(global); + const clearStackCallbacks = {}; + let clearStackCallCount = 0; + spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { + clearStackCallCount++; + + if (clearStackCallbacks[clearStackCallCount]) { + clearStackCallbacks[clearStackCallCount](); + } + + realClearStack(fn); + }); + + env.cleanup_(); + env = new jasmineUnderTest.Env(); + + let suiteErrors = []; + env.addReporter({ + suiteDone: function(result) { + const messages = result.failedExpectations.map(e => e.message); + suiteErrors = suiteErrors.concat(messages); + }, + specDone: function() { + clearStackCallbacks[clearStackCallCount + 1] = function() { + dispatchErrorEvent(global, { + error: 'fail at the end of the reporter queue' + }); + }; + clearStackCallbacks[clearStackCallCount + 2] = function() { + dispatchErrorEvent(global, { + error: 'fail at the end of the spec queue' + }); + }; + } + }); + + env.describe('A suite', function() { + env.it('is finishing when the failure occurs', function() {}); + }); + + await env.execute(); + + expect(suiteErrors).toEqual([ + 'fail at the end of the reporter queue thrown', + 'fail at the end of the spec queue thrown' + ]); + }); + }); + + it('routes unhandled exceptions to the running suite', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + env.cleanup_(); + env = new jasmineUnderTest.Env(); + const reporter = jasmine.createSpyObj('fakeReporter', [ + 'specDone', + 'suiteDone' + ]); + + env.addReporter(reporter); + + env.fdescribe('A suite', function() { + env.it('fails', function(specDone) { + setTimeout(function() { + specDone(); + queueMicrotask(function() { + queueMicrotask(function() { + dispatchErrorEvent(global, { error: 'fail' }); + }); + }); + }); + }); + }); + + env.describe('Ignored', function() { + env.it('is not run', function() {}); + }); + + await env.execute(); + + expect(reporter.specDone).not.toHaveFailedExpectationsForRunnable( + 'A suite fails', + ['fail thrown'] + ); + expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable( + 'A suite', + ['fail thrown'] + ); + }); + + describe('When the most recently suite has reported suiteDone', function() { + it('routes unhandled exceptions errors to an ancestor suite', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + + const realClearStack = jasmineUnderTest.getClearStack(global); + const clearStackCallbacks = {}; + let clearStackCallCount = 0; + spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { + clearStackCallCount++; + + if (clearStackCallbacks[clearStackCallCount]) { + clearStackCallbacks[clearStackCallCount](); + } + + realClearStack(fn); + }); + + env.cleanup_(); + env = new jasmineUnderTest.Env(); + + let suiteErrors = []; + env.addReporter({ + suiteDone: function(result) { + const messages = result.failedExpectations.map(e => e.message); + suiteErrors = suiteErrors.concat(messages); + + if (result.description === 'A nested suite') { + clearStackCallbacks[clearStackCallCount + 1] = function() { + dispatchErrorEvent(global, { + error: 'fail at the end of the reporter queue' + }); + }; + clearStackCallbacks[clearStackCallCount + 2] = function() { + dispatchErrorEvent(global, { + error: 'fail at the end of the suite queue' + }); + }; + } + } + }); + + env.describe('A suite', function() { + env.describe('A nested suite', function() { + env.it('a spec', function() {}); + }); + }); + + await env.execute(); + + expect(suiteErrors).toEqual([ + 'fail at the end of the reporter queue thrown', + 'fail at the end of the suite queue thrown' + ]); + }); + }); + + describe('When the env has started reporting jasmineDone', function() { + it('logs the error to the console', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn, delay) { + clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + env.cleanup_(); + env = new jasmineUnderTest.Env(); + + spyOn(console, 'error'); + + env.addReporter({ + jasmineDone: function() { + dispatchErrorEvent(global, { error: 'a very late error' }); + } + }); + + env.it('a spec', function() {}); + + await env.execute(); + + /* eslint-disable-next-line no-console */ + expect(console.error).toHaveBeenCalledWith( + 'Jasmine received a result after the suite finished:' + ); + /* eslint-disable-next-line no-console */ + expect(console.error).toHaveBeenCalledWith( + jasmine.objectContaining({ + message: 'a very late error thrown', + globalErrorType: 'afterAll' + }) + ); + }); + }); + + it('routes all errors that occur during stack clearing somewhere', async function() { + const global = { + ...browserEventMethods(), + setTimeout: function(fn, delay) { + return setTimeout(fn, delay); + }, + clearTimeout: function(fn) { + clearTimeout(fn); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + } + }; + spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); + + const realClearStack = jasmineUnderTest.getClearStack(global); + let clearStackCallCount = 0; + let jasmineDone = false; + const expectedErrors = []; + const expectedErrorsAfterJasmineDone = []; + spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) { + clearStackCallCount++; + const msg = `Error in clearStack #${clearStackCallCount}`; + + if (jasmineDone) { + expectedErrorsAfterJasmineDone.push(`${msg} thrown`); + } else { + expectedErrors.push(`${msg} thrown`); + } + + dispatchErrorEvent(global, { error: msg }); + realClearStack(fn); + }); + spyOn(console, 'error'); + + env.cleanup_(); + env = new jasmineUnderTest.Env(); + + const receivedErrors = []; + function logErrors(event) { + for (const failure of event.failedExpectations) { + receivedErrors.push(failure.message); + } + } + env.addReporter({ + specDone: logErrors, + suiteDone: logErrors, + jasmineDone: function(event) { + jasmineDone = true; + logErrors(event); + } + }); + + env.describe('A suite', function() { + env.it('is finishing when the failure occurs', function() {}); + }); + + await env.execute(); + + expect(receivedErrors.length).toEqual(expectedErrors.length); + + for (const e of expectedErrors) { + expect(receivedErrors).toContain(e); + } + + for (const message of expectedErrorsAfterJasmineDone) { + /* eslint-disable-next-line no-console */ + expect(console.error).toHaveBeenCalledWith( + jasmine.objectContaining({ message }) + ); + } + }); + }); + + describe('#spyOnGlobalErrorsAsync', function() { + const leftInstalledMessage = + 'Global error spy was not uninstalled. ' + + '(Did you forget to await the return value of spyOnGlobalErrorsAsync?)'; + + function resultForRunable(reporterSpy, fullName) { + const match = reporterSpy.calls.all().find(function(call) { + return call.args[0].fullName === fullName; + }); + + if (!match) { + throw new Error(`No result for runable "${fullName}"`); + } + + return match.args[0]; + } + + it('allows global errors to be suppressed and spied on', async function() { + env.it('a passing spec', async function() { + await env.spyOnGlobalErrorsAsync(async spy => { + setTimeout(() => { + throw new Error('nope'); + }); + await new Promise(resolve => setTimeout(resolve)); + env.expect(spy).toHaveBeenCalledWith(new Error('nope')); + }); + }); + + env.it('a failing spec', async function() { + await env.spyOnGlobalErrorsAsync(async spy => { + setTimeout(() => { + throw new Error('yep'); + }); + await new Promise(resolve => setTimeout(resolve)); + env.expect(spy).toHaveBeenCalledWith(new Error('nope')); + }); + }); + + const reporter = jasmine.createSpyObj('reporter', ['specDone']); + env.addReporter(reporter); + 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, + 'a passing spec' + ); + expect(passingResult.status).toEqual('passed'); + expect(passingResult.failedExpectations).toEqual([]); + + const failingResult = resultForRunable( + reporter.specDone, + 'a failing spec' + ); + expect(failingResult.status).toEqual('failed'); + expect(failingResult.failedExpectations[0].message).toMatch( + /Expected \$\[0] = Error: yep to equal Error: nope\./ + ); + }); + + it('cleans up if the global error spy is left installed in a beforeAll', async function() { + env.configure({ random: false }); + + env.describe('Suite 1', function() { + env.beforeAll(async function() { + env.spyOnGlobalErrorsAsync(function() { + // Never resolves + return new Promise(() => {}); + }); + }); + + env.it('a spec', function() {}); + }); + + env.describe('Suite 2', function() { + env.it('a spec', async function() { + setTimeout(function() { + throw new Error('should fail the spec'); + }); + await new Promise(resolve => setTimeout(resolve)); + }); + }); + + const reporter = jasmine.createSpyObj('reporter', [ + 'specDone', + 'suiteDone' + ]); + env.addReporter(reporter); + 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'); + expect(suiteResult.failedExpectations.length).toEqual(1); + expect(suiteResult.failedExpectations[0].message).toEqual( + leftInstalledMessage + ); + + const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec'); + expect(specResult.status).toEqual('failed'); + expect(specResult.failedExpectations.length).toEqual(1); + expect(specResult.failedExpectations[0].message).toMatch( + /Error: should fail the spec/ + ); + }); + + it('cleans up if the global error spy is left installed in an afterAll', async function() { + env.configure({ random: false }); + + env.describe('Suite 1', function() { + env.afterAll(async function() { + env.spyOnGlobalErrorsAsync(function() { + // Never resolves + return new Promise(() => {}); + }); + }); + + env.it('a spec', function() {}); + }); + + env.describe('Suite 2', function() { + env.it('a spec', async function() { + setTimeout(function() { + throw new Error('should fail the spec'); + }); + await new Promise(resolve => setTimeout(resolve)); + }); + }); + + const reporter = jasmine.createSpyObj('reporter', [ + 'specDone', + 'suiteDone' + ]); + env.addReporter(reporter); + 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', + [leftInstalledMessage] + ); + + const suiteResult = resultForRunable(reporter.suiteDone, 'Suite 1'); + expect(suiteResult.status).toEqual('failed'); + expect(suiteResult.failedExpectations.length).toEqual(1); + expect(suiteResult.failedExpectations[0].message).toEqual( + leftInstalledMessage + ); + + const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec'); + expect(specResult.status).toEqual('failed'); + expect(specResult.failedExpectations.length).toEqual(1); + expect(specResult.failedExpectations[0].message).toMatch( + /Error: should fail the spec/ + ); + }); + + it('cleans up if the global error spy is left installed in a beforeEach', async function() { + env.configure({ random: false }); + + env.describe('Suite 1', function() { + env.beforeEach(async function() { + env.spyOnGlobalErrorsAsync(function() { + // Never resolves + return new Promise(() => {}); + }); + }); + + env.it('a spec', function() {}); + }); + + env.describe('Suite 2', function() { + env.it('a spec', async function() { + setTimeout(function() { + throw new Error('should fail the spec'); + }); + await new Promise(resolve => setTimeout(resolve)); + }); + }); + + const reporter = jasmine.createSpyObj('reporter', [ + 'specDone', + 'suiteDone' + ]); + env.addReporter(reporter); + + 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'); + expect(spec1Result.failedExpectations.length).toEqual(1); + expect(spec1Result.failedExpectations[0].message).toEqual( + leftInstalledMessage + ); + + const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec'); + expect(spec2Result.status).toEqual('failed'); + expect(spec2Result.failedExpectations.length).toEqual(1); + expect(spec2Result.failedExpectations[0].message).toMatch( + /Error: should fail the spec/ + ); + }); + + it('cleans up if the global error spy is left installed in an it', async function() { + env.configure({ random: false }); + + env.it('spec 1', async function() { + env.spyOnGlobalErrorsAsync(function() { + // Never resolves + return new Promise(() => {}); + }); + }); + + env.it('spec 2', async function() { + setTimeout(function() { + throw new Error('should fail the spec'); + }); + await new Promise(resolve => setTimeout(resolve)); + }); + + const reporter = jasmine.createSpyObj('reporter', ['specDone']); + env.addReporter(reporter); + 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'); + expect(spec1Result.failedExpectations.length).toEqual(1); + expect(spec1Result.failedExpectations[0].message).toEqual( + leftInstalledMessage + ); + + const spec2Result = resultForRunable(reporter.specDone, 'spec 2'); + expect(spec2Result.status).toEqual('failed'); + expect(spec2Result.failedExpectations.length).toEqual(1); + expect(spec2Result.failedExpectations[0].message).toMatch( + /Error: should fail the spec/ + ); + }); + + it('cleans up if the global error spy is left installed in an afterEach', async function() { + env.configure({ random: false }); + + env.describe('Suite 1', function() { + env.afterEach(async function() { + env.spyOnGlobalErrorsAsync(function() { + // Never resolves + return new Promise(() => {}); + }); + }); + + env.it('a spec', function() {}); + }); + + env.describe('Suite 2', function() { + env.it('a spec', async function() { + setTimeout(function() { + throw new Error('should fail the spec'); + }); + await new Promise(resolve => setTimeout(resolve)); + }); + }); + + const reporter = jasmine.createSpyObj('reporter', [ + 'specDone', + 'suiteDone' + ]); + env.addReporter(reporter); + 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'); + expect(spec1Result.failedExpectations.length).toEqual(1); + expect(spec1Result.failedExpectations[0].message).toEqual( + leftInstalledMessage + ); + + const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec'); + expect(spec2Result.status).toEqual('failed'); + expect(spec2Result.failedExpectations.length).toEqual(1); + expect(spec2Result.failedExpectations[0].message).toMatch( + /Error: should fail the spec/ + ); + }); + }); + + function browserEventMethods() { + return { + 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); + } + } +});