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',
|
||||
'column',
|
||||
'description',
|
||||
'jasmineMessage'
|
||||
'jasmineMessage',
|
||||
'errors'
|
||||
];
|
||||
|
||||
function ExceptionFormatter(options) {
|
||||
@@ -4069,6 +4070,18 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -346,5 +346,172 @@ describe('ExceptionFormatter', function() {
|
||||
}).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',
|
||||
'column',
|
||||
'description',
|
||||
'jasmineMessage'
|
||||
'jasmineMessage',
|
||||
'errors'
|
||||
];
|
||||
|
||||
function ExceptionFormatter(options) {
|
||||
@@ -77,6 +78,18 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user