Files
jasmine/spec/core/ExceptionFormatterSpec.js
Steve Gravrock b88ce2d49f Improve AggregateError specs
* Assert that the right inner stack traces are in the right places
* Drop the integration tests. All of the AggregateError-specific code
  is in ExceptionFormatter, so the integration tests just duplicate
  existing integration tests and the ExceptionFormatter specs.
2026-02-07 17:41:39 -08:00

522 lines
20 KiB
JavaScript

describe('ExceptionFormatter', function() {
describe('#message', function() {
it('formats Firefox exception messages', function() {
const sampleFirefoxException = {
fileName: 'foo.js',
lineNumber: '1978',
message: 'you got your foo in my bar',
name: 'A Classic Mistake'
},
exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(sampleFirefoxException);
expect(message).toEqual(
'A Classic Mistake: you got your foo in my bar in foo.js (line 1978)'
);
});
it('formats Webkit exception messages', function() {
const sampleWebkitException = {
sourceURL: 'foo.js',
line: '1978',
message: 'you got your foo in my bar',
name: 'A Classic Mistake'
},
exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(sampleWebkitException);
expect(message).toEqual(
'A Classic Mistake: you got your foo in my bar in foo.js (line 1978)'
);
});
it('formats V8 exception messages', function() {
const sampleV8 = {
message: 'you got your foo in my bar',
name: 'A Classic Mistake'
},
exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(sampleV8);
expect(message).toEqual('A Classic Mistake: you got your foo in my bar');
});
it('formats unnamed exceptions with message', function() {
const unnamedError = { message: 'This is an unnamed error message.' };
const exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(unnamedError);
expect(message).toEqual('This is an unnamed error message.');
});
it('formats empty exceptions with toString format', function() {
const EmptyError = function() {};
EmptyError.prototype.toString = function() {
return '[EmptyError]';
};
const emptyError = new EmptyError();
const exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(emptyError);
expect(message).toEqual('[EmptyError] thrown');
});
it("formats thrown exceptions that aren't errors", function() {
const thrown = 'crazy error',
exceptionFormatter = new privateUnderTest.ExceptionFormatter(),
message = exceptionFormatter.message(thrown);
expect(message).toEqual('crazy error thrown');
});
});
describe('#stack', function() {
it('formats stack traces', function() {
const error = new Error('an error');
expect(new privateUnderTest.ExceptionFormatter().stack(error)).toMatch(
/ExceptionFormatterSpec\.js.*\d+/
);
});
it('filters Jasmine stack frames from V8-style traces but leaves unmatched lines intact', function() {
const error = {
message: 'nope',
stack:
'C:\\__spec__\\core\\UtilSpec.ts:120\n' +
" new Error('nope');\n" +
' ^\n' +
'\n' +
'Error: nope\n' +
' at fn1 (C:\\__spec__\\core\\UtilSpec.js:115:19)\n' +
' -> C:\\__spec__\\core\\UtilSpec.ts:120:15\n' +
' at fn2 (C:\\__jasmine__\\lib\\jasmine-core\\jasmine.js:7533:40)\n' +
' at fn3 (C:\\__jasmine__\\lib\\jasmine-core\\jasmine.js:7575:25)\n' +
' at fn4 (node:internal/timers:462:21)\n'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'C:\\__jasmine__\\lib\\jasmine-core\\jasmine.js'
});
const result = subject.stack(error);
expect(result).toEqual(
'C:\\__spec__\\core\\UtilSpec.ts:120\n' +
" new Error('nope');\n" +
' ^\n' +
'Error: nope\n' +
' at fn1 (C:\\__spec__\\core\\UtilSpec.js:115:19)\n' +
' -> C:\\__spec__\\core\\UtilSpec.ts:120:15\n' +
' at <Jasmine>\n' +
' at fn4 (node:internal/timers:462:21)'
);
});
it('filters Jasmine stack frames from V8 style traces', function() {
const error = {
message: 'nope',
stack:
'Error: nope\n' +
' at fn1 (http://localhost:8888/__spec__/core/UtilSpec.js:115:19)\n' +
' at fn2 (http://localhost:8888/__jasmine__/jasmine.js:4320:20)\n' +
' at fn3 (http://localhost:8888/__jasmine__/jasmine.js:4320:20)\n' +
' at fn4 (http://localhost:8888/__spec__/core/UtilSpec.js:110:19)\n'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'http://localhost:8888/__jasmine__/jasmine.js'
});
const result = subject.stack(error);
expect(result).toEqual(
'Error: nope\n' +
' at fn1 (http://localhost:8888/__spec__/core/UtilSpec.js:115:19)\n' +
' at <Jasmine>\n' +
' at fn4 (http://localhost:8888/__spec__/core/UtilSpec.js:110:19)'
);
});
it('filters Jasmine stack frames from Webkit style traces', function() {
const error = {
stack:
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'fn1@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'fn2@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'http://localhost:8888/__jasmine__/jasmine.js'
});
const result = subject.stack(error);
expect(result).toEqual(
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'<Jasmine>\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
);
});
it('filters Jasmine stack frames with Firefox async annotations', function() {
const error = {
stack:
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'promise callback*fn1@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'setTimeout handler*fn2@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'http://localhost:8888/__jasmine__/jasmine.js'
});
const result = subject.stack(error);
expect(result).toEqual(
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'<Jasmine>\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
);
});
it('filters Jasmine stack frames in this environment', function() {
const error = new Error('an error');
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: jasmine.private.util.jasmineFile()
});
const result = subject.stack(error);
jasmine.debugLog('Original stack trace: ' + error.stack);
jasmine.debugLog('Filtered stack trace: ' + result);
const lines = result.split('\n');
if (lines[0].match(/an error/)) {
lines.shift();
}
expect(lines[0]).toMatch(/ExceptionFormatterSpec.js/);
expect(lines[1]).toMatch(/<Jasmine>/);
// Node has some number of additional frames below Jasmine.
for (let i = 2; i < lines.length; i++) {
expect(lines[i]).not.toMatch(/jasmine.js/);
}
});
it('handles multiline error messages in this environment', function() {
const msg = 'an error\nwith two lines';
const error = new Error(msg);
if (error.stack.indexOf(msg) === -1) {
pending("Stack traces don't have messages in this environment");
}
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: jasmine.private.util.jasmineFile()
});
const result = subject.stack(error);
const lines = result.split('\n');
expect(lines[0]).toMatch(/an error/);
expect(lines[1]).toMatch(/with two lines/);
expect(lines[2]).toMatch(/ExceptionFormatterSpec.js/);
expect(lines[3]).toMatch(/<Jasmine>/);
});
it('returns null if no Error provided', function() {
expect(new privateUnderTest.ExceptionFormatter().stack()).toBeNull();
});
it("includes the error's own properties in stack", function() {
const error = new Error('an error');
error.someProperty = 'hello there';
const result = new privateUnderTest.ExceptionFormatter().stack(error);
expect(result).toMatch(/error properties:.*someProperty.*hello there/);
});
it('does not include inherited error properties', function() {
function CustomError(msg) {
Error.call(this, msg);
}
CustomError.prototype = new Error();
CustomError.prototype.anInheritedProp = 'something';
const error = new CustomError('nope');
const result = new privateUnderTest.ExceptionFormatter().stack(error);
expect(result).not.toContain('anInheritedProp');
});
describe('When omitMessage is true', function() {
it('filters the message from V8-style stack traces', function() {
const error = {
message: 'nope',
stack:
'Error: nope\n' +
' at fn1 (http://localhost:8888/__spec__/core/UtilSpec.js:115:19)\n' +
' at fn2 (http://localhost:8888/__jasmine__/jasmine.js:4320:20)\n' +
' at fn3 (http://localhost:8888/__jasmine__/jasmine.js:4320:20)\n' +
' at fn4 (http://localhost:8888/__spec__/core/UtilSpec.js:110:19)\n'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'http://localhost:8888/__jasmine__/jasmine.js'
});
const result = subject.stack(error, { omitMessage: true });
expect(result).toEqual(
' at fn1 (http://localhost:8888/__spec__/core/UtilSpec.js:115:19)\n' +
' at <Jasmine>\n' +
' at fn4 (http://localhost:8888/__spec__/core/UtilSpec.js:110:19)'
);
});
it('handles Webkit style traces that do not include a message', function() {
const error = {
stack:
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'fn1@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'fn2@http://localhost:8888/__jasmine__/jasmine.js:4320:27\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
};
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: 'http://localhost:8888/__jasmine__/jasmine.js'
});
const result = subject.stack(error, { omitMessage: true });
expect(result).toEqual(
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28\n' +
'<Jasmine>\n' +
'http://localhost:8888/__spec__/core/UtilSpec.js:115:28'
);
});
it('ensures that stack traces do not include the message in this environment', function() {
const error = new Error('an error');
const subject = new privateUnderTest.ExceptionFormatter({
jasmineFile: jasmine.private.util.jasmineFile()
});
const result = subject.stack(error, { omitMessage: true });
expect(result).not.toContain('an error');
});
});
describe('when the error has a cause property', function() {
it('recursively includes the cause in the stack trace in this environment', function() {
const subject = new privateUnderTest.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');
});
it('does not throw if cause is a non Error', function() {
const formatter = new privateUnderTest.ExceptionFormatter();
expect(function() {
formatter.stack(
new Error('error', {
cause: function() {}
})
);
}).not.toThrowError();
expect(function() {
formatter.stack(
new Error('error', {
cause: 'another error'
})
);
}).not.toThrowError();
});
});
describe('when the error has an errors array (AggregateError)', function() {
it('includes all aggregated errors in the stack trace', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const error1 = (function fn1() {
return new Error('first error');
})();
const error2 = (function fn2() {
return new Error('second error');
})();
const aggregateError = (function fn3() {
return new Error('Multiple errors occurred');
})();
aggregateError.errors = [error1, error2];
const lines = subject.stack(aggregateError).split('\n');
// TODO: be consistent across environments about whether the message is
// included in the stack trace
if (lines[0] === 'Error: Multiple errors occurred') {
lines.shift();
}
for (let i = 0; i < lines.length; i++) {
jasmine.debugLog(`Line ${i}: ${lines[i]}`);
}
expect(lines[0])
.withContext('first stack frame of the overall error')
.toMatch(/fn3.*core\/ExceptionFormatterSpec\.js/);
const error1MsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: first error')
);
expect(error1MsgIx)
.withContext('first nested error message')
.toBeGreaterThan(-1);
expect(lines[error1MsgIx + 1])
.withContext('first stack frame of first nested error')
.toMatch(/fn1.*core\/ExceptionFormatterSpec\.js/);
const error2MsgIx = lines.findIndex(line =>
line.includes('Error 2: Error: second error')
);
expect(error2MsgIx)
.withContext('second nested error message')
.toBeGreaterThan(error1MsgIx);
expect(lines[error2MsgIx + 1])
.withContext('first stack frame of second nested error')
.toMatch(/fn2.*core\/ExceptionFormatterSpec\.js/);
});
it('handles empty errors array', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const aggregateError = new Error('No errors');
aggregateError.errors = [];
expect(function() {
subject.stack(aggregateError);
}).not.toThrowError();
});
it('handles nested AggregateError', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const innerError1 = new Error('inner error 1');
const innerError2 = new Error('inner error 2');
const innerAggregate = new Error('Inner aggregate');
innerAggregate.errors = [innerError1, innerError2];
const outerError = new Error('outer error');
const outerAggregate = new Error('Outer aggregate');
outerAggregate.errors = [innerAggregate, outerError];
const lines = subject.stack(outerAggregate).split('\n');
const innerAggMsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: Inner aggregate')
);
expect(innerAggMsgIx).toBeGreaterThan(-1);
const innerError1MsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: inner error 1')
);
expect(innerError1MsgIx).toBeGreaterThan(innerAggMsgIx);
const innerError2MsgIx = lines.findIndex(line =>
line.includes('Error 2: Error: inner error 2')
);
expect(innerError2MsgIx).toBeGreaterThan(innerError1MsgIx);
const outerErrorMsgIx = lines.findIndex(line =>
line.includes('Error 2: Error: outer error')
);
expect(outerErrorMsgIx).toBeGreaterThan(innerError2MsgIx);
});
it('handles AggregateError containing error with cause', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const rootCause = new Error('root cause');
const errorWithCause = new Error('error with cause', {
cause: rootCause
});
const aggregateError = new Error('Aggregate with cause chain');
aggregateError.errors = [errorWithCause];
const lines = subject.stack(aggregateError).split('\n');
const error1MsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: error with cause')
);
expect(error1MsgIx).toBeGreaterThan(-1);
const causeMsgIx = lines.findIndex(line =>
line.includes('Caused by: Error: root cause')
);
expect(causeMsgIx).toBeGreaterThan(error1MsgIx);
});
it('skips non-Error items in errors array', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const error1 = new Error('real error');
const aggregateError = new Error('Mixed array');
aggregateError.errors = [
error1,
'string error',
{ message: 'object error' },
null,
undefined,
42
];
const lines = subject.stack(aggregateError).split('\n');
const error1MsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: real error')
);
expect(error1MsgIx).toBeGreaterThan(-1);
const hasStringError = lines.some(line =>
line.includes('string error')
);
expect(hasStringError).toBe(false);
const hasObjectError = lines.some(line =>
line.includes('object error')
);
expect(hasObjectError).toBe(false);
});
it('works with native AggregateError constructor', function() {
const subject = new privateUnderTest.ExceptionFormatter();
const error1 = new Error('first error');
const error2 = new Error('second error');
const aggregateError = new AggregateError(
[error1, error2],
'Multiple errors'
);
const lines = subject.stack(aggregateError).split('\n');
const error1MsgIx = lines.findIndex(line =>
line.includes('Error 1: Error: first error')
);
expect(error1MsgIx).toBeGreaterThan(-1);
const error2MsgIx = lines.findIndex(line =>
line.includes('Error 2: Error: second error')
);
expect(error2MsgIx).toBeGreaterThan(error1MsgIx);
});
});
});
});