Merge branch 'puglyfe-aggregate-errors'
* Adds support for AggregateError * Merges #2093 from @puglyfe * Fixes #2063
This commit is contained in:
@@ -4003,7 +4003,8 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
|||||||
'lineNumber',
|
'lineNumber',
|
||||||
'column',
|
'column',
|
||||||
'description',
|
'description',
|
||||||
'jasmineMessage'
|
'jasmineMessage',
|
||||||
|
'errors'
|
||||||
];
|
];
|
||||||
|
|
||||||
function ExceptionFormatter(options) {
|
function ExceptionFormatter(options) {
|
||||||
@@ -4069,6 +4070,18 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
|||||||
lines = lines.concat(substack);
|
lines = lines.concat(substack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(error.errors)) {
|
||||||
|
error.errors.forEach((aggregatedError, index) => {
|
||||||
|
if (aggregatedError instanceof Error) {
|
||||||
|
const substack = this.stack_(aggregatedError, {
|
||||||
|
messageHandling: 'require'
|
||||||
|
});
|
||||||
|
substack[0] = 'Error ' + (index + 1) + ': ' + substack[0];
|
||||||
|
lines = lines.concat(substack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -346,5 +346,172 @@ describe('ExceptionFormatter', function() {
|
|||||||
}).not.toThrowError();
|
}).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 = new Error('first error');
|
||||||
|
const error2 = new Error('second error');
|
||||||
|
const error3 = new Error('third error');
|
||||||
|
const aggregateError = new Error('Multiple errors occurred');
|
||||||
|
aggregateError.errors = [error1, error2, error3];
|
||||||
|
|
||||||
|
const lines = subject.stack(aggregateError).split('\n');
|
||||||
|
|
||||||
|
const error1MsgIx = lines.findIndex(line =>
|
||||||
|
line.includes('Error 1: Error: first error')
|
||||||
|
);
|
||||||
|
expect(error1MsgIx)
|
||||||
|
.withContext('first error message')
|
||||||
|
.toBeGreaterThan(-1);
|
||||||
|
|
||||||
|
const error2MsgIx = lines.findIndex(line =>
|
||||||
|
line.includes('Error 2: Error: second error')
|
||||||
|
);
|
||||||
|
expect(error2MsgIx)
|
||||||
|
.withContext('second error message')
|
||||||
|
.toBeGreaterThan(error1MsgIx);
|
||||||
|
|
||||||
|
const error3MsgIx = lines.findIndex(line =>
|
||||||
|
line.includes('Error 3: Error: third error')
|
||||||
|
);
|
||||||
|
expect(error3MsgIx)
|
||||||
|
.withContext('third error message')
|
||||||
|
.toBeGreaterThan(error2MsgIx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles AggregateError with single error', function() {
|
||||||
|
const subject = new privateUnderTest.ExceptionFormatter();
|
||||||
|
const error1 = new Error('single error');
|
||||||
|
const aggregateError = new Error('One error occurred');
|
||||||
|
aggregateError.errors = [error1];
|
||||||
|
|
||||||
|
const lines = subject.stack(aggregateError).split('\n');
|
||||||
|
|
||||||
|
const error1MsgIx = lines.findIndex(line =>
|
||||||
|
line.includes('Error 1: Error: single error')
|
||||||
|
);
|
||||||
|
expect(error1MsgIx).toBeGreaterThan(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
84
spec/core/integration/ExceptionFormattingSpec.js
Normal file
84
spec/core/integration/ExceptionFormattingSpec.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
describe('Exception formatting (integration)', function() {
|
||||||
|
let env;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
specHelpers.registerIntegrationMatchers();
|
||||||
|
env = new privateUnderTest.Env();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
env.cleanup_();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AggregateError formatting', function() {
|
||||||
|
it('formats AggregateError with individual errors', async function() {
|
||||||
|
env.it('should format AggregateError with individual errors', function() {
|
||||||
|
const errors = [
|
||||||
|
new Error('Database connection failed'),
|
||||||
|
new Error('Invalid configuration'),
|
||||||
|
new Error('Service unavailable')
|
||||||
|
];
|
||||||
|
throw new AggregateError(errors, 'Multiple initialization errors');
|
||||||
|
});
|
||||||
|
|
||||||
|
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
|
||||||
|
env.addReporter(reporter);
|
||||||
|
await env.execute();
|
||||||
|
|
||||||
|
expect(reporter.specDone).toHaveBeenCalledTimes(1);
|
||||||
|
const result = reporter.specDone.calls.argsFor(0)[0];
|
||||||
|
expect(result.status).toEqual('failed');
|
||||||
|
expect(result.failedExpectations.length).toEqual(1);
|
||||||
|
|
||||||
|
const failure = result.failedExpectations[0];
|
||||||
|
expect(failure.message).toContain('AggregateError');
|
||||||
|
expect(failure.message).toContain('Multiple initialization errors');
|
||||||
|
|
||||||
|
expect(failure.stack).toContain(
|
||||||
|
'Error 1: Error: Database connection failed'
|
||||||
|
);
|
||||||
|
expect(failure.stack).toContain('Error 2: Error: Invalid configuration');
|
||||||
|
expect(failure.stack).toContain('Error 3: Error: Service unavailable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats nested AggregateError', async function() {
|
||||||
|
env.it('should format nested AggregateError', function() {
|
||||||
|
const innerErrors = [
|
||||||
|
new Error('Inner error 1'),
|
||||||
|
new Error('Inner error 2')
|
||||||
|
];
|
||||||
|
const innerAggregate = new AggregateError(
|
||||||
|
innerErrors,
|
||||||
|
'Inner operation failed'
|
||||||
|
);
|
||||||
|
|
||||||
|
const outerErrors = [
|
||||||
|
innerAggregate,
|
||||||
|
new Error('Outer error'),
|
||||||
|
new Error('Other outer error')
|
||||||
|
];
|
||||||
|
throw new AggregateError(outerErrors, 'Multiple operations failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
|
||||||
|
env.addReporter(reporter);
|
||||||
|
await env.execute();
|
||||||
|
|
||||||
|
expect(reporter.specDone).toHaveBeenCalledTimes(1);
|
||||||
|
const result = reporter.specDone.calls.argsFor(0)[0];
|
||||||
|
expect(result.status).toEqual('failed');
|
||||||
|
|
||||||
|
const failure = result.failedExpectations[0];
|
||||||
|
|
||||||
|
// Firefox & Safari don't preserve types for nested errors
|
||||||
|
expect(failure.stack).toMatch(
|
||||||
|
/Error 1: (AggregateError|Error): Inner operation failed/
|
||||||
|
);
|
||||||
|
expect(failure.stack).toContain('Error 2: Error: Outer error');
|
||||||
|
expect(failure.stack).toContain('Error 3: Error: Other outer error');
|
||||||
|
|
||||||
|
expect(failure.stack).toContain('Error 1: Error: Inner error 1');
|
||||||
|
expect(failure.stack).toContain('Error 2: Error: Inner error 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,8 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
|||||||
'lineNumber',
|
'lineNumber',
|
||||||
'column',
|
'column',
|
||||||
'description',
|
'description',
|
||||||
'jasmineMessage'
|
'jasmineMessage',
|
||||||
|
'errors'
|
||||||
];
|
];
|
||||||
|
|
||||||
function ExceptionFormatter(options) {
|
function ExceptionFormatter(options) {
|
||||||
@@ -77,6 +78,18 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
|||||||
lines = lines.concat(substack);
|
lines = lines.concat(substack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(error.errors)) {
|
||||||
|
error.errors.forEach((aggregatedError, index) => {
|
||||||
|
if (aggregatedError instanceof Error) {
|
||||||
|
const substack = this.stack_(aggregatedError, {
|
||||||
|
messageHandling: 'require'
|
||||||
|
});
|
||||||
|
substack[0] = 'Error ' + (index + 1) + ': ' + substack[0];
|
||||||
|
lines = lines.concat(substack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user