Merge branch 'puglyfe-aggregate-errors'

* Adds support for AggregateError
* Merges #2093 from @puglyfe
* Fixes #2063
This commit is contained in:
Steve Gravrock
2026-02-07 16:01:06 -08:00
4 changed files with 279 additions and 2 deletions

View File

@@ -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;
};

View File

@@ -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);
});
});
});
});

View 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');
});
});
});

View File

@@ -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;
};