-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add finalize option for post-processing formatted output #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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
| return escapeAnsi(value) | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">") | ||
| .replace(/&/g, "&"); |
Check failure
Code scanning / CodeQL
Double escaping or unescaping High test
here
Show autofix suggestion
Hide autofix suggestion
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 < are not themselves transformed into &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, "&"), followed by .replace(/</g, "<") and .replace(/>/g, ">"). Because this changes the actual output, the expected string in the corresponding checkError call must also be updated: instead of expecting &lt;div&gt;Test&lt;/div&gt;, it should now expect <div>Test</div> (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.
-
Copy modified line R563 -
Copy modified line R565 -
Copy modified line R578
| @@ -560,9 +560,9 @@ | ||
| // Custom finalize function that converts to HTML-safe format | ||
| const customEscape = (value: string): string => { | ||
| return escapeAnsi(value) | ||
| .replace(/&/g, "&") | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">") | ||
| .replace(/&/g, "&"); | ||
| .replace(/>/g, ">"); | ||
| }; | ||
|
|
||
| assertConfig.format = { | ||
| @@ -576,7 +575,7 @@ | ||
|
|
||
| checkError(function () { | ||
| assert.equal(obj, { html: "Plain" }); | ||
| }, "expected {html:\"\\x1b[31m&lt;div&gt;Test&lt;/div&gt;\\x1b[0m\"} to equal {html:\"Plain\"}"); | ||
| }, "expected {html:\"\\x1b[31m<div>Test</div>\\x1b[0m\"} to equal {html:\"Plain\"}"); | ||
| }); | ||
|
|
||
| it("should handle undefined finalizeFn (fallback to default)", function () { |
There was a problem hiding this 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
IFormatterinterface and plugin architecture - Added
finalizeandfinalizeFnoptions toIFormatterOptionsfor post-processing complete formatted output - Refactored
_formatValueto use a context-based formatter pattern with built-in type handlers - Implemented global scope context management via
useScopefor 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
| 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
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| 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); |
| 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 |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| * 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 . |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| * 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. |
| * @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 |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| * @since 0.1.0 | |
| * @since 0.1.3 |
| 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"\}/); | ||
| }); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| let theValues: IAssertConfig = _mergeConfig({}, DEFAULT_CONFIG); | ||
|
|
||
| function _removeFormatter(formatter: IFormatter) { | ||
| let formatters = theValues.format.formatters |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| let formatters = theValues.format.formatters | |
| let formatters = theValues.format.formatters; |
| if (isArray(source[key])) { | ||
| target[key] = source[key]; |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| * @returns A string representation of the value. | ||
| */ | ||
| function _doFormat(formatCtx: IFormatCtx, value: any): string { | ||
| let result: string = EMPTY_STRING; |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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.
| let result: string = EMPTY_STRING; | |
| let result: string; |
| if (!inAnsi) { | ||
| result += cyan(msg) + brightRed; | ||
| } else { | ||
| result += msg |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
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).
Add new
finalizeandfinalizeFnoptions to control post-processing of formatted assertion error messages.Features
Finalization Options
finalize(boolean): Enable post-processing of formatted outputfinalizeFn(function): Custom function to transform the complete formatted stringDefault Behavior
When
finalize: truewithout a customfinalizeFn, automatically uses escapeAnsi from @nevware21/chromacon to escape ANSI codes for display as literal characters.Use Cases
Core Changes
finalizeandfinalizeFntoIFormatterOptionsinterface_formatValue()- applied once to complete outputassertConfigdefaults with new options