Improve compiler error display for latest Chromium
This commit addresses the issue of Chromium v126 and later not displaying error messages correctly when the error object's `message` property uses a getter. It refactors the code to utilize an immutable Error object with recursive context, improves error message formatting and leverages the `cause` property. Changes: - Refactor error wrapping internals to use an immutable error object, eliminating `message` getters. - Utilize the `cause` property in contextual errors for enhanced error display in the console. - Enhance message formatting with better indentation and listing. - Improve clarity by renaming values thrown during validations.
This commit is contained in:
@@ -134,7 +134,7 @@ describe('ApplicationParser', () => {
|
||||
const data = [new CollectionDataStub()];
|
||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||
value: data,
|
||||
valueName: 'collections',
|
||||
valueName: 'Collections',
|
||||
};
|
||||
const validator = new TypeValidatorStub();
|
||||
const sut = new ApplicationParserBuilder()
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('CategoryCollectionParser', () => {
|
||||
const data = new CollectionDataStub();
|
||||
const expectedAssertion: ObjectAssertion<CollectionData> = {
|
||||
value: data,
|
||||
valueName: 'collection',
|
||||
valueName: 'Collection',
|
||||
allowedProperties: [
|
||||
'os', 'scripting', 'actions', 'functions',
|
||||
],
|
||||
@@ -48,7 +48,7 @@ describe('CategoryCollectionParser', () => {
|
||||
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||
value: actions,
|
||||
valueName: '"actions" in collection',
|
||||
valueName: '\'actions\' in collection',
|
||||
};
|
||||
const validator = new TypeValidatorStub();
|
||||
const context = new TestContext()
|
||||
|
||||
@@ -1,101 +1,168 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { wrapErrorWithAdditionalContext } from '@/application/Parser/Common/ContextualError';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('wrapErrorWithAdditionalContext', () => {
|
||||
it('preserves the original error when wrapped', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
const context = new TestContext()
|
||||
.withError(expectedError);
|
||||
// act
|
||||
const error = context.wrap();
|
||||
// assert
|
||||
const actualError = extractInnerErrorFromContextualError(error);
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
it('maintains the original error when re-wrapped', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
|
||||
// act
|
||||
const firstError = new TestContext()
|
||||
.withError(expectedError)
|
||||
.withAdditionalContext('first error')
|
||||
.wrap();
|
||||
const secondError = new TestContext()
|
||||
.withError(firstError)
|
||||
.withAdditionalContext('second error')
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
const actualError = extractInnerErrorFromContextualError(secondError);
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
it(`the object extends ${CustomError.name}`, () => {
|
||||
it(`extend ${CustomError.name}`, () => {
|
||||
// arrange
|
||||
const expected = CustomError;
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.wrap();
|
||||
.build();
|
||||
// assert
|
||||
expect(error).to.be.an.instanceof(expected);
|
||||
});
|
||||
describe('inner error preservation', () => {
|
||||
it('preserves the original error', () => {
|
||||
// arrange
|
||||
const expectedError = new Error();
|
||||
const context = new TestContext()
|
||||
.withInnerError(expectedError);
|
||||
// act
|
||||
const error = context.build();
|
||||
// assert
|
||||
const actualError = getInnerErrorFromContextualError(error);
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
it('sets the original error as the cause', () => {
|
||||
// arrange
|
||||
const expectedError = new Error('error causing the issue');
|
||||
const context = new TestContext()
|
||||
.withInnerError(expectedError);
|
||||
// act
|
||||
const error = context.build();
|
||||
// assert
|
||||
const actualError = error.cause;
|
||||
expect(actualError).to.equal(expectedError);
|
||||
});
|
||||
});
|
||||
describe('error message construction', () => {
|
||||
it('includes the message from the original error', () => {
|
||||
it('includes the original error message', () => {
|
||||
// arrange
|
||||
const expectedOriginalErrorMessage = 'Message from the inner error';
|
||||
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withError(new Error(expectedOriginalErrorMessage))
|
||||
.wrap();
|
||||
.withInnerError(new Error(expectedOriginalErrorMessage))
|
||||
.build();
|
||||
|
||||
// assert
|
||||
expect(error.message).contains(expectedOriginalErrorMessage);
|
||||
});
|
||||
it('appends provided additional context to the error message', () => {
|
||||
it('includes original error toString() if message is absent', () => {
|
||||
// arrange
|
||||
const originalError = new Error();
|
||||
const expectedPartInMessage = originalError.toString();
|
||||
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withInnerError(originalError)
|
||||
.build();
|
||||
|
||||
// assert
|
||||
expect(error.message).contains(expectedPartInMessage);
|
||||
});
|
||||
it('appends additional context to the error message', () => {
|
||||
// arrange
|
||||
const expectedAdditionalContext = 'Expected additional context message';
|
||||
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withAdditionalContext(expectedAdditionalContext)
|
||||
.wrap();
|
||||
.build();
|
||||
|
||||
// assert
|
||||
expect(error.message).contains(expectedAdditionalContext);
|
||||
});
|
||||
it('appends multiple contexts to the error message in sequential order', () => {
|
||||
// arrange
|
||||
const expectedFirstContext = 'First context';
|
||||
const expectedSecondContext = 'Second context';
|
||||
describe('message order', () => {
|
||||
it('displays the latest context before the original error message', () => {
|
||||
// arrange
|
||||
const originalErrorMessage = 'Original message from the inner error to be shown first';
|
||||
const additionalContext = 'Context to be displayed after';
|
||||
|
||||
// act
|
||||
const firstError = new TestContext()
|
||||
.withAdditionalContext(expectedFirstContext)
|
||||
.wrap();
|
||||
const secondError = new TestContext()
|
||||
.withError(firstError)
|
||||
.withAdditionalContext(expectedSecondContext)
|
||||
.wrap();
|
||||
// act
|
||||
const error = new TestContext()
|
||||
.withInnerError(new Error(originalErrorMessage))
|
||||
.withAdditionalContext(additionalContext)
|
||||
.build();
|
||||
|
||||
// assert
|
||||
const messageLines = splitTextIntoLines(secondError.message);
|
||||
expect(messageLines).to.contain(`1: ${expectedFirstContext}`);
|
||||
expect(messageLines).to.contain(`2: ${expectedSecondContext}`);
|
||||
// assert
|
||||
expectMessageDisplayOrder(error.message, {
|
||||
firstMessage: additionalContext,
|
||||
secondMessage: originalErrorMessage,
|
||||
});
|
||||
});
|
||||
it('appends multiple contexts from most specific to most general', () => {
|
||||
// arrange
|
||||
const deepErrorContext = 'first-context';
|
||||
const parentErrorContext = 'second-context';
|
||||
|
||||
// act
|
||||
const deepError = new TestContext()
|
||||
.withAdditionalContext(deepErrorContext)
|
||||
.build();
|
||||
const parentError = new TestContext()
|
||||
.withInnerError(deepError)
|
||||
.withAdditionalContext(parentErrorContext)
|
||||
.build();
|
||||
const grandParentError = new TestContext()
|
||||
.withInnerError(parentError)
|
||||
.withAdditionalContext('latest-error')
|
||||
.build();
|
||||
|
||||
// assert
|
||||
expectMessageDisplayOrder(grandParentError.message, {
|
||||
firstMessage: deepErrorContext,
|
||||
secondMessage: parentErrorContext,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('throws error when context is missing', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'Missing additional context';
|
||||
const context = new TestContext()
|
||||
.withAdditionalContext(absentValue);
|
||||
// act
|
||||
const act = () => context.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
|
||||
function expectMessageDisplayOrder(
|
||||
actualMessage: string,
|
||||
expectation: {
|
||||
readonly firstMessage: string;
|
||||
readonly secondMessage: string;
|
||||
},
|
||||
): void {
|
||||
const firstMessageIndex = actualMessage.indexOf(expectation.firstMessage);
|
||||
const secondMessageIndex = actualMessage.indexOf(expectation.secondMessage);
|
||||
expect(firstMessageIndex).to.be.lessThan(secondMessageIndex, formatAssertionMessage([
|
||||
'Error output order does not match the expected order.',
|
||||
'Expected the first message to be displayed before the second message.',
|
||||
'Expected first message:',
|
||||
indentText(expectation.firstMessage),
|
||||
'Expected second message:',
|
||||
indentText(expectation.secondMessage),
|
||||
'Received message:',
|
||||
indentText(actualMessage),
|
||||
]));
|
||||
}
|
||||
|
||||
class TestContext {
|
||||
private error: Error = new Error();
|
||||
private innerError: Error = new Error(`[${TestContext.name}] original error`);
|
||||
|
||||
private additionalContext = `[${TestContext.name}] additional context`;
|
||||
|
||||
public withError(error: Error) {
|
||||
this.error = error;
|
||||
public withInnerError(innerError: Error) {
|
||||
this.innerError = innerError;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -104,19 +171,21 @@ class TestContext {
|
||||
return this;
|
||||
}
|
||||
|
||||
public wrap(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
||||
public build(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
||||
return wrapErrorWithAdditionalContext(
|
||||
this.error,
|
||||
this.innerError,
|
||||
this.additionalContext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function extractInnerErrorFromContextualError(error: Error): Error {
|
||||
const innerErrorProperty = 'innerError';
|
||||
if (!(innerErrorProperty in error)) {
|
||||
throw new Error(`${innerErrorProperty} property is missing`);
|
||||
function getInnerErrorFromContextualError(error: Error & {
|
||||
readonly context?: {
|
||||
readonly innerError?: Error;
|
||||
},
|
||||
}): Error {
|
||||
if (error?.context?.innerError instanceof Error) {
|
||||
return error.context.innerError;
|
||||
}
|
||||
const actualError = error[innerErrorProperty];
|
||||
return actualError as Error;
|
||||
throw new Error('Error must have a context with a valid innerError property.');
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('createTypeValidator', () => {
|
||||
}) => {
|
||||
it(description, () => {
|
||||
const valueName = 'invalidValue';
|
||||
const expectedMessage = `'${valueName}' should be of type 'string', but is of type '${typeof invalidValue}'.`;
|
||||
const expectedMessage = `${valueName} should be of type 'string', but is of type '${typeof invalidValue}'.`;
|
||||
const { assertNonEmptyString } = createTypeValidator();
|
||||
// act
|
||||
const act = () => assertNonEmptyString({ value: invalidValue, valueName });
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('FunctionCallsParser', () => {
|
||||
const data = new FunctionCallDataStub();
|
||||
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
||||
value: data,
|
||||
valueName: 'function call',
|
||||
valueName: 'Function call',
|
||||
allowedProperties: [
|
||||
'function', 'parameters',
|
||||
],
|
||||
@@ -117,7 +117,7 @@ describe('FunctionCallsParser', () => {
|
||||
const data: FunctionCallsData = [new FunctionCallDataStub()];
|
||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||
value: data,
|
||||
valueName: 'function call sequence',
|
||||
valueName: 'Function call sequence',
|
||||
};
|
||||
const validator = new TypeValidatorStub();
|
||||
const context = new TestContext()
|
||||
@@ -134,7 +134,7 @@ describe('FunctionCallsParser', () => {
|
||||
const data: FunctionCallsData = [expectedValidatedCallData];
|
||||
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
||||
value: expectedValidatedCallData,
|
||||
valueName: 'function call',
|
||||
valueName: 'Function call',
|
||||
allowedProperties: [
|
||||
'function', 'parameters',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user