The old style of merging all of a function's variable declarations into a single statement made some sense back in the days of var, but there's no reason to keep doing it now that we use const and let.
530 lines
20 KiB
JavaScript
530 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'
|
|
};
|
|
const exceptionFormatter = new privateUnderTest.ExceptionFormatter();
|
|
const 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'
|
|
};
|
|
const exceptionFormatter = new privateUnderTest.ExceptionFormatter();
|
|
const 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'
|
|
};
|
|
const exceptionFormatter = new privateUnderTest.ExceptionFormatter();
|
|
const 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();
|
|
const 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();
|
|
const message = exceptionFormatter.message(emptyError);
|
|
|
|
expect(message).toEqual('[EmptyError] thrown');
|
|
});
|
|
|
|
it("formats thrown exceptions that aren't errors", function() {
|
|
const thrown = 'crazy error';
|
|
const exceptionFormatter = new privateUnderTest.ExceptionFormatter();
|
|
const 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();
|
|
}
|
|
|
|
// Exclude lines that vary from environment to environment
|
|
const filteredLines = lines.filter(
|
|
x =>
|
|
!x.match(/[\/\\]jasmine\.js:/) &&
|
|
// Some Node 20 and 22 minors when running in parallel
|
|
!x.includes('process.processTicksAndRejections')
|
|
);
|
|
|
|
for (let i = 0; i < filteredLines.length; i++) {
|
|
jasmine.debugLog(`Line ${i} after filtering: ${filteredLines[i]}`);
|
|
}
|
|
|
|
// Inexact matching because stack frame formatting varies from runtime
|
|
// to runtime
|
|
const expectedPatterns = [
|
|
// Overall error
|
|
/fn3.*ExceptionFormatterSpec\.js/,
|
|
/ExceptionFormatterSpec\.js/,
|
|
/^$/,
|
|
|
|
// First nested error
|
|
/^ Error 1: Error: first error$/,
|
|
/^ .*fn1.*ExceptionFormatterSpec\.js/,
|
|
/^ .*ExceptionFormatterSpec\.js/,
|
|
/^$/,
|
|
|
|
// Second nested error
|
|
/^ .*Error 2: Error: second error$/,
|
|
/^ .*fn2.*ExceptionFormatterSpec\.js/,
|
|
/^ .*ExceptionFormatterSpec\.js/
|
|
];
|
|
|
|
expect(filteredLines).toEqual(
|
|
expectedPatterns.map(p => jasmine.stringMatching(p))
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
});
|