diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index ad3eadc4..fda51281 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -3494,18 +3494,38 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) { return null; } - const stackTrace = new j$.StackTrace(error); - const lines = filterJasmine(stackTrace); - let result = ''; + const lines = this.stack_(error, { + messageHandling: omitMessage ? 'omit' : undefined + }); + return lines.join('\n'); + }; - if (stackTrace.message && !omitMessage) { + // messageHandling can be falsy (unspecified), 'omit', or 'require' + this.stack_ = function(error, { messageHandling }) { + let lines = formatProperties(error).split('\n'); + + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + const stackTrace = new j$.StackTrace(error); + lines = lines.concat(filterJasmine(stackTrace)); + + if (messageHandling === 'require') { + lines.unshift(stackTrace.message || 'Error: ' + error.message); + } else if (messageHandling !== 'omit' && stackTrace.message) { lines.unshift(stackTrace.message); } - result += formatProperties(error); - result += lines.join('\n'); + if (error.cause) { + const substack = this.stack_(error.cause, { + messageHandling: 'require' + }); + substack[0] = 'Caused by: ' + substack[0]; + lines = lines.concat(substack); + } - return result; + return lines; }; function filterJasmine(stackTrace) { diff --git a/spec/core/ExceptionFormatterSpec.js b/spec/core/ExceptionFormatterSpec.js index 3f410e10..39c8f5f9 100644 --- a/spec/core/ExceptionFormatterSpec.js +++ b/spec/core/ExceptionFormatterSpec.js @@ -256,5 +256,51 @@ describe('ExceptionFormatter', function() { expect(result).not.toContain('an error'); }); }); + + describe('In environments that support the cause property of Errors', function() { + beforeEach(function() { + const inner = new Error('inner'); + const outer = new Error('outer', { cause: inner }); + + if (!outer.cause) { + // Currently: Node 12, Node 14, Safari 14 + pending('Environment does not support error cause'); + } + }); + + it('recursively includes the cause in the stack trace in this environment', function() { + const subject = new jasmineUnderTest.ExceptionFormatter(); + const rootCause = new Error('root cause'); + const proximateCause = new Error('proximate cause', { + cause: rootCause + }); + const symptom = new Error('symptom', { cause: proximateCause }); + + const lines = subject.stack(symptom).split('\n'); + // Not all environments include the message in the stack trace. + const hasRootMessage = lines[0].indexOf('symptom') !== -1; + const firstSymptomStackIx = hasRootMessage ? 1 : 0; + + expect(lines[firstSymptomStackIx]) + .withContext('first symptom stack frame') + .toContain('ExceptionFormatterSpec.js'); + const proximateCauseMsgIx = lines.indexOf( + 'Caused by: Error: proximate cause' + ); + expect(proximateCauseMsgIx) + .withContext('index of proximate cause message') + .toBeGreaterThan(firstSymptomStackIx); + expect(lines[proximateCauseMsgIx + 1]) + .withContext('first proximate cause stack frame') + .toContain('ExceptionFormatterSpec.js'); + const rootCauseMsgIx = lines.indexOf('Caused by: Error: root cause'); + expect(rootCauseMsgIx) + .withContext('index of root cause message') + .toBeGreaterThan(proximateCauseMsgIx + 1); + expect(lines[rootCauseMsgIx + 1]) + .withContext('first root cause stack frame') + .toContain('ExceptionFormatterSpec.js'); + }); + }); }); }); diff --git a/src/core/ExceptionFormatter.js b/src/core/ExceptionFormatter.js index e5ba82ed..0d69458b 100644 --- a/src/core/ExceptionFormatter.js +++ b/src/core/ExceptionFormatter.js @@ -44,18 +44,38 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) { return null; } - const stackTrace = new j$.StackTrace(error); - const lines = filterJasmine(stackTrace); - let result = ''; + const lines = this.stack_(error, { + messageHandling: omitMessage ? 'omit' : undefined + }); + return lines.join('\n'); + }; - if (stackTrace.message && !omitMessage) { + // messageHandling can be falsy (unspecified), 'omit', or 'require' + this.stack_ = function(error, { messageHandling }) { + let lines = formatProperties(error).split('\n'); + + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + const stackTrace = new j$.StackTrace(error); + lines = lines.concat(filterJasmine(stackTrace)); + + if (messageHandling === 'require') { + lines.unshift(stackTrace.message || 'Error: ' + error.message); + } else if (messageHandling !== 'omit' && stackTrace.message) { lines.unshift(stackTrace.message); } - result += formatProperties(error); - result += lines.join('\n'); + if (error.cause) { + const substack = this.stack_(error.cause, { + messageHandling: 'require' + }); + substack[0] = 'Caused by: ' + substack[0]; + lines = lines.concat(substack); + } - return result; + return lines; }; function filterJasmine(stackTrace) {