Skip to content

Conversation

@nev21
Copy link
Contributor

@nev21 nev21 commented Dec 28, 2025

Add new finalize and finalizeFn options to control post-processing of formatted assertion error messages.

Features

Finalization Options

  • finalize (boolean): Enable post-processing of formatted output
  • finalizeFn (function): Custom function to transform the complete formatted string

Default Behavior

When finalize: true without a custom finalizeFn, automatically uses escapeAnsi from @nevware21/chromacon to escape ANSI codes for display as literal characters.

Use Cases

  • Escape ANSI codes for terminal display
  • Colorize escaped ANSI sequences (e.g., in gray)
  • Convert output to HTML-safe format
  • Wrap or decorate formatted messages
  • Apply custom transformations to final output

Core Changes

  • Added finalize and finalizeFn to IFormatterOptions interface
  • Implemented finalization in _formatValue() - applied once to complete output
  • Updated assertConfig defaults with new options
  • Reduced karma worker log level from DEBUG to INFO for faster test startup

Add new `finalize` and `finalizeFn` options to control post-processing of
formatted assertion error messages.

# Features

## Finalization Options
- `finalize` (boolean): Enable post-processing of formatted output
- `finalizeFn` (function): Custom function to transform the complete formatted string

## Default Behavior
When `finalize: true` without a custom `finalizeFn`, automatically uses
[escapeAnsi](https://nevware21.github.io/chromacon/typedoc/core/functions/escapeAnsi.html)
from @nevware21/chromacon to escape ANSI codes for display as literal characters.

## Use Cases
- Escape ANSI codes for terminal display
- Colorize escaped ANSI sequences (e.g., in gray)
- Convert output to HTML-safe format
- Wrap or decorate formatted messages
- Apply custom transformations to final output

## Core Changes
- Added `finalize` and `finalizeFn` to `IFormatterOptions` interface
- Implemented finalization in `_formatValue()` - applied once to complete output
- Updated `assertConfig` defaults with new options
- Reduced karma worker log level from DEBUG to INFO for faster test startup
Copilot AI review requested due to automatic review settings December 28, 2025 00:47
@nev21 nev21 added this to the 0.1.3 milestone Dec 28, 2025
Comment on lines +562 to +565
return escapeAnsi(value)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/&/g, "&amp;");

Check failure

Code scanning / CodeQL

Double escaping or unescaping High test

This replacement may double-escape '&' characters from
here
.

Copilot Autofix

AI 2 days ago

In general, to avoid double-escaping when HTML-encoding text, escape the ampersand (&) first, then other characters like < and >, so that entity strings like &lt; are not themselves transformed into &amp;lt;. In this file, the customEscape finalize function is meant to “convert ANSI to HTML entities” and claims to be “HTML-safe”. It currently calls escapeAnsi and then replaces <, >, and finally &, which causes & characters introduced for </> to be re-escaped.

The best fix is to change the replacement order in customEscape so that & is replaced before < and >. Concretely, on the customEscape definition at lines 560–566, change the chained .replace calls so the first one is .replace(/&/g, "&amp;"), followed by .replace(/</g, "&lt;") and .replace(/>/g, "&gt;"). Because this changes the actual output, the expected string in the corresponding checkError call must also be updated: instead of expecting &amp;lt;div&amp;gt;Test&amp;lt;/div&amp;gt;, it should now expect &lt;div&gt;Test&lt;/div&gt; (with only a single level of escaping). No new imports or helper methods are needed; we only adjust the order of the existing replace calls and update the hard-coded expectation string in the test.

Suggested changeset 1
core/test/src/assert/assert.format.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/core/test/src/assert/assert.format.test.ts b/core/test/src/assert/assert.format.test.ts
--- a/core/test/src/assert/assert.format.test.ts
+++ b/core/test/src/assert/assert.format.test.ts
@@ -560,9 +560,9 @@
             // Custom finalize function that converts to HTML-safe format
             const customEscape = (value: string): string => {
                 return escapeAnsi(value)
+                    .replace(/&/g, "&amp;")
                     .replace(/</g, "&lt;")
-                    .replace(/>/g, "&gt;")
-                    .replace(/&/g, "&amp;");
+                    .replace(/>/g, "&gt;");
             };
 
             assertConfig.format = {
@@ -576,7 +575,7 @@
 
             checkError(function () {
                 assert.equal(obj, { html: "Plain" });
-            }, "expected {html:\"\\x1b[31m&amp;lt;div&amp;gt;Test&amp;lt;/div&amp;gt;\\x1b[0m\"} to equal {html:\"Plain\"}");
+            }, "expected {html:\"\\x1b[31m&lt;div&gt;Test&lt;/div&gt;\\x1b[0m\"} to equal {html:\"Plain\"}");
         });
 
         it("should handle undefined finalizeFn (fallback to default)", function () {
EOF
@@ -560,9 +560,9 @@
// Custom finalize function that converts to HTML-safe format
const customEscape = (value: string): string => {
return escapeAnsi(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/&/g, "&amp;");
.replace(/>/g, "&gt;");
};

assertConfig.format = {
@@ -576,7 +575,7 @@

checkError(function () {
assert.equal(obj, { html: "Plain" });
}, "expected {html:\"\\x1b[31m&amp;lt;div&amp;gt;Test&amp;lt;/div&amp;gt;\\x1b[0m\"} to equal {html:\"Plain\"}");
}, "expected {html:\"\\x1b[31m&lt;div&gt;Test&lt;/div&gt;\\x1b[0m\"} to equal {html:\"Plain\"}");
});

it("should handle undefined finalizeFn (fallback to default)", function () {
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds powerful formatting customization capabilities to the tripwire assertion library through new finalize and finalizeFn options. The changes enable post-processing of formatted assertion error messages, with default support for escaping ANSI codes using chromacon's escapeAnsi function when finalize: true is set without a custom function.

Key changes:

  • Introduced a comprehensive formatter system with IFormatter interface and plugin architecture
  • Added finalize and finalizeFn options to IFormatterOptions for post-processing complete formatted output
  • Refactored _formatValue to use a context-based formatter pattern with built-in type handlers
  • Implemented global scope context management via useScope for better error context tracking

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
core/src/assert/internal/_formatValue.ts Refactored to use formatter pattern with context; added finalization logic and default formatters for arrays, strings, objects, etc.
core/src/assert/interface/IFormatter.ts New interfaces defining formatter system: IFormatter, IFormatterOptions, IFormatCtx, IFormattedValue, and eFormatResult enum
core/src/assert/interface/IRemovable.ts New interface for removable resources with rm() method
core/src/assert/interface/IAssertConfig.ts Added format, circularMsg, reset, addFormatter, and removeFormatter to config interface
core/src/assert/config.ts Implemented formatter management with add/remove, config reset, and deep merge for nested config objects
core/src/assert/useScope.ts New global scope context management for tracking context during error creation
core/src/assert/scopeContext.ts Integrated with useScope to provide context during error throwing
core/src/assert/assertScope.ts Wraps scope execution in useScope for proper context tracking
core/src/assert/assertionError.ts Uses global scope context for formatting error properties
core/src/assert/assertClass.ts Passes assertConfig when creating context
core/src/assert/funcs/equal.ts Updated to pass context to _formatValue
core/src/assert/adapters/exprAdapter.ts Updated to pass context to _formatValue
core/src/index.ts Exports new interfaces, enums, and functions
core/test/src/assert/assert.format.test.ts Comprehensive tests for finalize options and custom formatters
core/test/src/assert/assert.equals.test.ts Added tests for custom circular messages and dynamic message usage
core/test/src/assert/context.test.ts Minor test message improvements
core/test/src/assert/assert.function.test.ts Added beforeEach/afterEach with config reset
core/test/src/support/checkError.ts Enhanced diff visualization with ANSI handling and escape character display
shim/chai/test/src/chaiAssert.test.ts Updated to use dynamic circular message from config
core/package.json Updated @nevware21/chromacon dependency to >= 0.1.3
core/karma.worker.conf.js Updated istanbul exclude patterns and output directory
core/karma.debug.worker.conf.js Uncommented node-resolve plugin
core/karma.browser.conf.js Fixed preprocessor pattern specificity
common/config/rush/npm-shrinkwrap.json Updated chromacon version and package integrity hashes
.github/dependabot.yml Expanded configuration with grouped dependency updates for better organization
Files not reviewed (1)
  • common/config/rush/npm-shrinkwrap.json: Language not supported
Comments suppressed due to low confidence (1)

core/src/assert/assertionError.ts:174

  • Avoid automated semicolon insertion (93% of all statements in the enclosing function have an explicit semicolon).
        return formatted

Comment on lines +65 to +69
values[key] = (isNullOrUndefined(value) ? defaults[key] : value)
} else if (isObject(values[key])) {
_mergeConfig(values[key], (isNullOrUndefined(value) ? defaults[key] : value));
} else {
values[key] = (isNullOrUndefined(value) ? defaults[key] : value)
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolons at the end of the assignment statements on lines 65, 69. While JavaScript allows this, the project appears to use semicolons consistently elsewhere, and ESLint may flag this inconsistency.

Suggested change
values[key] = (isNullOrUndefined(value) ? defaults[key] : value)
} else if (isObject(values[key])) {
_mergeConfig(values[key], (isNullOrUndefined(value) ? defaults[key] : value));
} else {
values[key] = (isNullOrUndefined(value) ? defaults[key] : value)
values[key] = (isNullOrUndefined(value) ? defaults[key] : value);
} else if (isObject(values[key])) {
_mergeConfig(values[key], (isNullOrUndefined(value) ? defaults[key] : value));
} else {
values[key] = (isNullOrUndefined(value) ? defaults[key] : value);

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +164
result += msg
}
} else if (char1 === "\\") {
result += "\\\\";
} else {
result += char1;
}
} else {
if (match !== 1) {
result += normal + brightRed;
match = 1;
}
result += char1;

if (_isNonPrintableChar(char1)) {
let msg = "\\x" + char1.charCodeAt(0).toString(16).padStart(2, "0");
if (!inAnsi) {
result += cyan(msg) + brightRed;
} else {
result += msg
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at the end of the statement on lines 146 and 164. The code style in this file and project consistently uses semicolons, so these should be added for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +440 to +443
* Creates a format context that uses the given formatters.
* User-supplied formatters are tried first, then default formatters are used as fallback.
* @param formatters - Array of user-supplied formatters to use.
* @returns A format context that can be passed .
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 443 is incomplete - it ends with "A format context that can be passed ." without completing the sentence. This should be completed to properly describe what the format context is passed to.

Suggested change
* Creates a format context that uses the given formatters.
* User-supplied formatters are tried first, then default formatters are used as fallback.
* @param formatters - Array of user-supplied formatters to use.
* @returns A format context that can be passed .
* Creates a format context bound to the supplied scope context.
* The returned context handles circular references while formatting values.
* @param ctx - The scope context used to resolve formatting options.
* @returns A format context that can be passed to `_doFormat` or used via its `format` method.

Copilot uses AI. Check for mistakes.
* @param callback - The function to execute with the scope context set
* @returns The return value of the callback function
* @group Scope
* @since 0.1.0
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @SInCE tag indicates version 0.1.0, but this is a new feature being added to the codebase. Based on the PR description and other @SInCE tags in IFormatter.ts showing 0.1.3, this should likely be @SInCE 0.1.3 to accurately reflect when this feature was introduced.

Suggested change
* @since 0.1.0
* @since 0.1.3

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +119
describe("escapeAnsi", function () {
let originalFormat: any;

beforeEach(function () {
originalFormat = assertConfig.format;
});

afterEach(function () {
assertConfig.format = originalFormat;
});

it("should escape ANSI codes when escapeAnsi is true", function () {
assertConfig.format = {
finalize: true
};

// Create an object with ANSI escape codes in a string value
const objWithAnsi = { message: "\x1b[31mRed Text\x1b[0m" };

checkError(function () {
assert.equal(objWithAnsi, { message: "Different" });
}, "expected {message:\"\\x1b[31mRed Text\\x1b[0m\"} to equal {message:\"Different\"}");
});

it("should not escape ANSI codes when escapeAnsi is false", function () {
assertConfig.format = {
finalize: false
};

// Create an object with ANSI escape codes in a string value
const objWithAnsi = { message: "\x1b[31mRed Text\x1b[0m" };

checkError(function () {
assert.equal(objWithAnsi, { message: "Different" });
}, /expected \{message:".*Red Text.*"\} to equal \{message:"Different"\}/);
});

it("should escape ANSI codes in circular references when escapeAnsi is true", function () {
assertConfig.format = {
finalize: true
};

const obj1: any = { a: 1 };
obj1.self = obj1;

const obj2: any = { a: 2 };
obj2.self = obj2;

checkError(function () {
assert.deepEqual(obj1, obj2);
}, /expected \{a:1,self:\{a:1,self:.*\}\} to deeply equal \{a:2,self:\{a:2,self:.*\}\}/);
});

it("should handle escapeAnsi with deeply nested objects", function () {
assertConfig.format = {
finalize: true
};

const obj1 = {
nested: {
deep: {
value: "\x1b[32mGreen\x1b[0m"
}
}
};

const obj2 = {
nested: {
deep: {
value: "Plain"
}
}
};

checkError(function () {
assert.deepEqual(obj1, obj2);
}, "expected {nested:{deep:{value:\"\\x1b[32mGreen\\x1b[0m\"}}} to deeply equal {nested:{deep:{value:\"Plain\"}}}");
});

it("should handle escapeAnsi with arrays containing ANSI codes", function () {
assertConfig.format = {
finalize: true
};

const arr1 = ["\x1b[34mBlue\x1b[0m", "normal"];
const arr2 = ["different", "normal"];

checkError(function () {
assert.deepEqual(arr1, arr2);
}, "expected [\"\\x1b[34mBlue\\x1b[0m\",\"normal\"] to deeply equal [\"different\",\"normal\"]");
});

it("should handle escapeAnsi being set to undefined (default behavior)", function () {
assertConfig.format = {
finalize: undefined
};

const obj = { value: "\x1b[35mMagenta\x1b[0m" };

checkError(function () {
assert.equal(obj, { value: "Plain" });
}, /expected \{value:".*Magenta.*"\} to equal \{value:"Plain"\}/);
});
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description and comments refer to "escapeAnsi" as a property name, but the actual property being set is "finalize". This is misleading. The test names and comments should be updated to reflect that they're testing the "finalize" option, not a non-existent "escapeAnsi" option.

Copilot uses AI. Check for mistakes.
let theValues: IAssertConfig = _mergeConfig({}, DEFAULT_CONFIG);

function _removeFormatter(formatter: IFormatter) {
let formatters = theValues.format.formatters
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the variable assignment on line 79. The code style in this project consistently uses semicolons, so this should be added for consistency.

Suggested change
let formatters = theValues.format.formatters
let formatters = theValues.format.formatters;

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +45
if (isArray(source[key])) {
target[key] = source[key];
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _mergeConfig function doesn't handle deep cloning of arrays. When source[key] is an array (line 44-45), it directly assigns the reference, which means modifications to the array in one config will affect the other. This could lead to unexpected behavior, especially for the formatters array. Consider using array spread or slice to create a shallow copy of arrays.

Copilot uses AI. Check for mistakes.
* @returns A string representation of the value.
*/
function _doFormat(formatCtx: IFormatCtx, value: any): string {
let result: string = EMPTY_STRING;
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial value of result is unused, since it is always overwritten.

Suggested change
let result: string = EMPTY_STRING;
let result: string;

Copilot uses AI. Check for mistakes.
if (!inAnsi) {
result += cyan(msg) + brightRed;
} else {
result += msg
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid automated semicolon insertion (93% of all statements in the enclosing function have an explicit semicolon).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants