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:
@@ -31,7 +31,7 @@ function validateCollectionsData(
|
|||||||
) {
|
) {
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: collections,
|
value: collections,
|
||||||
valueName: 'collections',
|
valueName: 'Collections',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ function validateCollection(
|
|||||||
): void {
|
): void {
|
||||||
validator.assertObject({
|
validator.assertObject({
|
||||||
value: content,
|
value: content,
|
||||||
valueName: 'collection',
|
valueName: 'Collection',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'os', 'scripting', 'actions', 'functions',
|
'os', 'scripting', 'actions', 'functions',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: content.actions,
|
value: content.actions,
|
||||||
valueName: '"actions" in collection',
|
valueName: '\'actions\' in collection',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,116 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
import { indentText } from '@/application/Common/Text/IndentText';
|
||||||
|
|
||||||
export interface ErrorWithContextWrapper {
|
export interface ErrorWithContextWrapper {
|
||||||
(
|
(
|
||||||
error: Error,
|
innerError: Error,
|
||||||
additionalContext: string,
|
additionalContext: string,
|
||||||
): Error;
|
): Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
error: Error,
|
innerError,
|
||||||
additionalContext: string,
|
additionalContext,
|
||||||
) => {
|
) => {
|
||||||
return (error instanceof ContextualError ? error : new ContextualError(error))
|
if (!additionalContext) {
|
||||||
.withAdditionalContext(additionalContext);
|
throw new Error('Missing additional context');
|
||||||
|
}
|
||||||
|
return new ContextualError({
|
||||||
|
innerError,
|
||||||
|
additionalContext,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
/**
|
||||||
|
* Class for building a detailed error trace.
|
||||||
|
*
|
||||||
|
* Alternatives considered:
|
||||||
|
* - `AggregateError`:
|
||||||
|
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
|
||||||
|
* - `cause` property:
|
||||||
|
* Not displayed by all browsers (last tested v126).
|
||||||
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
||||||
|
*
|
||||||
|
* This is immutable where the constructor sets the values because using getter functions such as
|
||||||
|
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
|
||||||
|
* Firefox (last tested v127).
|
||||||
|
*/
|
||||||
class ContextualError extends CustomError {
|
class ContextualError extends CustomError {
|
||||||
private readonly additionalContext = new Array<string>();
|
constructor(public readonly context: ErrorContext) {
|
||||||
|
super(
|
||||||
constructor(
|
generateDetailedErrorMessageWithContext(context),
|
||||||
public readonly innerError: Error,
|
{
|
||||||
) {
|
cause: context.innerError,
|
||||||
super();
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
public withAdditionalContext(additionalContext: string): this {
|
|
||||||
this.additionalContext.push(additionalContext);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get message(): string { // toString() is not used when Chromium logs it on console
|
|
||||||
return [
|
|
||||||
'\n',
|
|
||||||
this.innerError.message,
|
|
||||||
'\n',
|
|
||||||
'Additional context:',
|
|
||||||
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
readonly innerError: Error;
|
||||||
|
readonly additionalContext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDetailedErrorMessageWithContext(
|
||||||
|
context: ErrorContext,
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
// Display the current error message first, then the root cause.
|
||||||
|
// This prevents repetitive main messages for errors with a `cause:` chain,
|
||||||
|
// aligning with browser error display conventions.
|
||||||
|
context.additionalContext,
|
||||||
|
'\n',
|
||||||
|
'Error Trace (starting from root cause):',
|
||||||
|
indentText(
|
||||||
|
formatErrorTrace(
|
||||||
|
// Displaying contexts from the top frame (deepest, most recent) aligns with
|
||||||
|
// common debugger/compiler standard.
|
||||||
|
extractErrorTraceAscendingFromDeepest(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'\n',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorTraceAscendingFromDeepest(
|
||||||
|
context: ErrorContext,
|
||||||
|
): string[] {
|
||||||
|
const originalError = findRootError(context.innerError);
|
||||||
|
const contextsDescendingFromMostRecent: string[] = [
|
||||||
|
context.additionalContext,
|
||||||
|
...gatherContextsFromErrorChain(context.innerError),
|
||||||
|
originalError.toString(),
|
||||||
|
];
|
||||||
|
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
|
||||||
|
return contextsAscendingFromDeepest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRootError(error: Error): Error {
|
||||||
|
if (error instanceof ContextualError) {
|
||||||
|
return findRootError(error.context.innerError);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gatherContextsFromErrorChain(
|
||||||
|
error: Error,
|
||||||
|
accumulatedContexts: string[] = [],
|
||||||
|
): string[] {
|
||||||
|
if (error instanceof ContextualError) {
|
||||||
|
accumulatedContexts.push(error.context.additionalContext);
|
||||||
|
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
|
||||||
|
}
|
||||||
|
return accumulatedContexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorTrace(
|
||||||
|
errorMessages: readonly string[],
|
||||||
|
): string {
|
||||||
|
if (errorMessages.length === 1) {
|
||||||
|
return errorMessages[0];
|
||||||
|
}
|
||||||
|
return errorMessages
|
||||||
|
.map((context, index) => `${index + 1}.${indentText(context)}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function assertArray(
|
|||||||
valueName: string,
|
valueName: string,
|
||||||
): asserts value is Array<unknown> {
|
): asserts value is Array<unknown> {
|
||||||
if (!isArray(value)) {
|
if (!isArray(value)) {
|
||||||
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
|
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ function assertString(
|
|||||||
valueName: string,
|
valueName: string,
|
||||||
): asserts value is string {
|
): asserts value is string {
|
||||||
if (!isString(value)) {
|
if (!isString(value)) {
|
||||||
throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`);
|
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function ensureValidCategory(
|
|||||||
});
|
});
|
||||||
validator.assertType((v) => v.assertObject({
|
validator.assertType((v) => v.assertObject({
|
||||||
value: category,
|
value: category,
|
||||||
valueName: category.category ?? 'category',
|
valueName: `Category '${category.category}'` ?? 'Category',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'docs', 'children', 'category',
|
'docs', 'children', 'category',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const createFunctionCallArgument: FunctionCallArgumentFactory = (
|
|||||||
utilities.validateParameterName(parameterName);
|
utilities.validateParameterName(parameterName);
|
||||||
utilities.typeValidator.assertNonEmptyString({
|
utilities.typeValidator.assertNonEmptyString({
|
||||||
value: argumentValue,
|
value: argumentValue,
|
||||||
valueName: `Missing argument value for the parameter "${parameterName}".`,
|
valueName: `Function parameter '${parameterName}'`,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
parameterName,
|
parameterName,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): Fu
|
|||||||
if (isArray(calls)) {
|
if (isArray(calls)) {
|
||||||
validator.assertNonEmptyCollection({
|
validator.assertNonEmptyCollection({
|
||||||
value: calls,
|
value: calls,
|
||||||
valueName: 'function call sequence',
|
valueName: 'Function call sequence',
|
||||||
});
|
});
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ function parseFunctionCall(
|
|||||||
): FunctionCall {
|
): FunctionCall {
|
||||||
utilities.typeValidator.assertObject({
|
utilities.typeValidator.assertObject({
|
||||||
value: call,
|
value: call,
|
||||||
valueName: 'function call',
|
valueName: 'Function call',
|
||||||
allowedProperties: ['function', 'parameters'],
|
allowedProperties: ['function', 'parameters'],
|
||||||
});
|
});
|
||||||
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const validateParameterName = (
|
|||||||
) => {
|
) => {
|
||||||
typeValidator.assertNonEmptyString({
|
typeValidator.assertNonEmptyString({
|
||||||
value: parameterName,
|
value: parameterName,
|
||||||
valueName: 'parameter name',
|
valueName: 'Parameter name',
|
||||||
rule: {
|
rule: {
|
||||||
expectedMatch: /^[0-9a-zA-Z]+$/,
|
expectedMatch: /^[0-9a-zA-Z]+$/,
|
||||||
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function validateScript(
|
|||||||
): asserts script is NonNullable<ScriptData> {
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
||||||
value: script,
|
value: script,
|
||||||
valueName: script.name ?? 'script',
|
valueName: `Script '${script.name}'` ?? 'Script',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function validateData(
|
|||||||
): void {
|
): void {
|
||||||
validator.assertObject({
|
validator.assertObject({
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'scripting definition',
|
valueName: 'Scripting definition',
|
||||||
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ describe('ApplicationParser', () => {
|
|||||||
const data = [new CollectionDataStub()];
|
const data = [new CollectionDataStub()];
|
||||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'collections',
|
valueName: 'Collections',
|
||||||
};
|
};
|
||||||
const validator = new TypeValidatorStub();
|
const validator = new TypeValidatorStub();
|
||||||
const sut = new ApplicationParserBuilder()
|
const sut = new ApplicationParserBuilder()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe('CategoryCollectionParser', () => {
|
|||||||
const data = new CollectionDataStub();
|
const data = new CollectionDataStub();
|
||||||
const expectedAssertion: ObjectAssertion<CollectionData> = {
|
const expectedAssertion: ObjectAssertion<CollectionData> = {
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'collection',
|
valueName: 'Collection',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'os', 'scripting', 'actions', 'functions',
|
'os', 'scripting', 'actions', 'functions',
|
||||||
],
|
],
|
||||||
@@ -48,7 +48,7 @@ describe('CategoryCollectionParser', () => {
|
|||||||
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
value: actions,
|
value: actions,
|
||||||
valueName: '"actions" in collection',
|
valueName: '\'actions\' in collection',
|
||||||
};
|
};
|
||||||
const validator = new TypeValidatorStub();
|
const validator = new TypeValidatorStub();
|
||||||
const context = new TestContext()
|
const context = new TestContext()
|
||||||
|
|||||||
@@ -1,101 +1,168 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { CustomError } from '@/application/Common/CustomError';
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
import { wrapErrorWithAdditionalContext } from '@/application/Parser/Common/ContextualError';
|
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', () => {
|
describe('wrapErrorWithAdditionalContext', () => {
|
||||||
it('preserves the original error when wrapped', () => {
|
it(`extend ${CustomError.name}`, () => {
|
||||||
// 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}`, () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expected = CustomError;
|
const expected = CustomError;
|
||||||
// act
|
// act
|
||||||
const error = new TestContext()
|
const error = new TestContext()
|
||||||
.wrap();
|
.build();
|
||||||
// assert
|
// assert
|
||||||
expect(error).to.be.an.instanceof(expected);
|
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', () => {
|
describe('error message construction', () => {
|
||||||
it('includes the message from the original error', () => {
|
it('includes the original error message', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedOriginalErrorMessage = 'Message from the inner error';
|
const expectedOriginalErrorMessage = 'Message from the inner error';
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const error = new TestContext()
|
const error = new TestContext()
|
||||||
.withError(new Error(expectedOriginalErrorMessage))
|
.withInnerError(new Error(expectedOriginalErrorMessage))
|
||||||
.wrap();
|
.build();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(error.message).contains(expectedOriginalErrorMessage);
|
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
|
// arrange
|
||||||
const expectedAdditionalContext = 'Expected additional context message';
|
const expectedAdditionalContext = 'Expected additional context message';
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const error = new TestContext()
|
const error = new TestContext()
|
||||||
.withAdditionalContext(expectedAdditionalContext)
|
.withAdditionalContext(expectedAdditionalContext)
|
||||||
.wrap();
|
.build();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(error.message).contains(expectedAdditionalContext);
|
expect(error.message).contains(expectedAdditionalContext);
|
||||||
});
|
});
|
||||||
it('appends multiple contexts to the error message in sequential order', () => {
|
describe('message order', () => {
|
||||||
// arrange
|
it('displays the latest context before the original error message', () => {
|
||||||
const expectedFirstContext = 'First context';
|
// arrange
|
||||||
const expectedSecondContext = 'Second context';
|
const originalErrorMessage = 'Original message from the inner error to be shown first';
|
||||||
|
const additionalContext = 'Context to be displayed after';
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const firstError = new TestContext()
|
const error = new TestContext()
|
||||||
.withAdditionalContext(expectedFirstContext)
|
.withInnerError(new Error(originalErrorMessage))
|
||||||
.wrap();
|
.withAdditionalContext(additionalContext)
|
||||||
const secondError = new TestContext()
|
.build();
|
||||||
.withError(firstError)
|
|
||||||
.withAdditionalContext(expectedSecondContext)
|
|
||||||
.wrap();
|
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const messageLines = splitTextIntoLines(secondError.message);
|
expectMessageDisplayOrder(error.message, {
|
||||||
expect(messageLines).to.contain(`1: ${expectedFirstContext}`);
|
firstMessage: additionalContext,
|
||||||
expect(messageLines).to.contain(`2: ${expectedSecondContext}`);
|
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 {
|
class TestContext {
|
||||||
private error: Error = new Error();
|
private innerError: Error = new Error(`[${TestContext.name}] original error`);
|
||||||
|
|
||||||
private additionalContext = `[${TestContext.name}] additional context`;
|
private additionalContext = `[${TestContext.name}] additional context`;
|
||||||
|
|
||||||
public withError(error: Error) {
|
public withInnerError(innerError: Error) {
|
||||||
this.error = error;
|
this.innerError = innerError;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,19 +171,21 @@ class TestContext {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public wrap(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
public build(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
||||||
return wrapErrorWithAdditionalContext(
|
return wrapErrorWithAdditionalContext(
|
||||||
this.error,
|
this.innerError,
|
||||||
this.additionalContext,
|
this.additionalContext,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractInnerErrorFromContextualError(error: Error): Error {
|
function getInnerErrorFromContextualError(error: Error & {
|
||||||
const innerErrorProperty = 'innerError';
|
readonly context?: {
|
||||||
if (!(innerErrorProperty in error)) {
|
readonly innerError?: Error;
|
||||||
throw new Error(`${innerErrorProperty} property is missing`);
|
},
|
||||||
|
}): Error {
|
||||||
|
if (error?.context?.innerError instanceof Error) {
|
||||||
|
return error.context.innerError;
|
||||||
}
|
}
|
||||||
const actualError = error[innerErrorProperty];
|
throw new Error('Error must have a context with a valid innerError property.');
|
||||||
return actualError as Error;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ describe('createTypeValidator', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
it(description, () => {
|
it(description, () => {
|
||||||
const valueName = 'invalidValue';
|
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();
|
const { assertNonEmptyString } = createTypeValidator();
|
||||||
// act
|
// act
|
||||||
const act = () => assertNonEmptyString({ value: invalidValue, valueName });
|
const act = () => assertNonEmptyString({ value: invalidValue, valueName });
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('FunctionCallsParser', () => {
|
|||||||
const data = new FunctionCallDataStub();
|
const data = new FunctionCallDataStub();
|
||||||
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'function call',
|
valueName: 'Function call',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'function', 'parameters',
|
'function', 'parameters',
|
||||||
],
|
],
|
||||||
@@ -117,7 +117,7 @@ describe('FunctionCallsParser', () => {
|
|||||||
const data: FunctionCallsData = [new FunctionCallDataStub()];
|
const data: FunctionCallsData = [new FunctionCallDataStub()];
|
||||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
value: data,
|
value: data,
|
||||||
valueName: 'function call sequence',
|
valueName: 'Function call sequence',
|
||||||
};
|
};
|
||||||
const validator = new TypeValidatorStub();
|
const validator = new TypeValidatorStub();
|
||||||
const context = new TestContext()
|
const context = new TestContext()
|
||||||
@@ -134,7 +134,7 @@ describe('FunctionCallsParser', () => {
|
|||||||
const data: FunctionCallsData = [expectedValidatedCallData];
|
const data: FunctionCallsData = [expectedValidatedCallData];
|
||||||
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
const expectedAssertion: ObjectAssertion<FunctionCallData> = {
|
||||||
value: expectedValidatedCallData,
|
value: expectedValidatedCallData,
|
||||||
valueName: 'function call',
|
valueName: 'Function call',
|
||||||
allowedProperties: [
|
allowedProperties: [
|
||||||
'function', 'parameters',
|
'function', 'parameters',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user