diff --git a/core/src/assert/adapters/exprAdapter.ts b/core/src/assert/adapters/exprAdapter.ts index bf0ac76..a8e3d18 100644 --- a/core/src/assert/adapters/exprAdapter.ts +++ b/core/src/assert/adapters/exprAdapter.ts @@ -6,7 +6,10 @@ * Licensed under the MIT license. */ -import { arrForEach, arrSlice, dumpObj, fnApply, isArray, isFunction, isNumber, isString, objDefine, objKeys, strEndsWith, strIndexOf, strLeft, strStartsWith } from "@nevware21/ts-utils"; +import { + arrForEach, arrSlice, dumpObj, fnApply, isArray, isFunction, isNumber, isString, + objDefine, objKeys, strEndsWith, strIndexOf, strLeft, strStartsWith +} from "@nevware21/ts-utils"; import { IAssertScope } from "../interface/IAssertScope"; import { IScopeContext } from "../interface/IScopeContext"; import { IScopeFn } from "../interface/IScopeFuncs"; @@ -136,7 +139,7 @@ function _runExpr(theAssert: any, scope: IAssertScope, steps: IStepDef[], scopeF if (!(step.name in scope.that)) { throw new AssertionError( - "" + idx + " Invalid step: " + step.name + " for [" + steps.map((s: any) => s.name).join("->") + "] available steps: [" + objKeys(scope.that).join(";") + "] - " + _formatValue(scope.context.opts, scope.that), + "" + idx + " Invalid step: " + step.name + " for [" + steps.map((s: any) => s.name).join("->") + "] available steps: [" + objKeys(scope.that).join(";") + "] - " + _formatValue(scope.context, scope.that), { expected: step.name, actual: objKeys(scope.that).join(";") @@ -191,7 +194,7 @@ function _processFn(scope: IAssertScope, scopeFn: IScopeFn, theArgs: any[], theR // The last step is a function, so we call it theResult = scope.exec(theResult, theArgs); if (scope.context.opts.isVerbose) { - scope.context.setOp("=>[[r:" + _formatValue(scope.context.opts, theResult) + "]]"); + scope.context.setOp("=>[[r:" + _formatValue(scope.context, theResult) + "]]"); } } diff --git a/core/src/assert/assertClass.ts b/core/src/assert/assertClass.ts index a321063..310bc7c 100644 --- a/core/src/assert/assertClass.ts +++ b/core/src/assert/assertClass.ts @@ -15,7 +15,9 @@ import { IPromise } from "@nevware21/ts-async"; import { IAssertClass } from "./interface/IAssertClass"; import { IAssertInst } from "./interface/IAssertInst"; import { MsgSource } from "./type/MsgSource"; -import { isBooleanFunc, isFunctionFunc, isNumberFunc, isNullFunc, isObjectFunc, isPlainObjectFunc, isStrictFalseFunc, isStrictTrueFunc, isUndefinedFunc } from "./funcs/is"; +import { + isBooleanFunc, isFunctionFunc, isNumberFunc, isNullFunc, isObjectFunc, isPlainObjectFunc, isStrictFalseFunc, isStrictTrueFunc, isUndefinedFunc +} from "./funcs/is"; import { isEmptyFunc } from "./funcs/isEmpty"; import { IAssertClassDef } from "./interface/IAssertClassDef"; import { matchFunc } from "./funcs/match"; @@ -487,29 +489,6 @@ function _createLazyInstHandler(target: any, propName: string, argDef: IAssertCl }); } -function _extractInitMsg(theArgs: any[], numArgs?: number, mIdx?: number): string { - // Extract the message if its present to be passed to the scope context - let msg: string; - if (!isUndefined(mIdx)) { - if (mIdx >= 0) { - // Positive index - if (theArgs.length > mIdx) { - msg = theArgs[mIdx]; - theArgs.splice(mIdx, 1); - } - } else { - // Negative index - let idx = theArgs.length + mIdx; - if (idx >= 0 && idx < theArgs.length && numArgs >= 0 && numArgs < theArgs.length) { - msg = theArgs[idx]; - theArgs.splice(idx, 1); - } - } - } - - return msg; -} - function _createAliasFunc(alias: string) { return function _aliasProxyFunc(): IAssertInst | IPromise { let _this = this; @@ -525,6 +504,106 @@ function _createAliasFunc(alias: string) { }; } +/** + * @internal + * @ignore + * Extracts the actual value, message and scope arguments from the provided arguments based on the definition of the assertion function. + * @param theArgs - The arguments passed to the assertion function, this is expected to be an array of arguments. + * @param mIdx - The index of the message argument in the arguments array, this can be a positive index (0-based) + * or a negative index (from the end of the array), or undefined if no message extraction is needed. + * @param numArgs - The number of arguments expected by the assertion function. + * @returns An object containing the extracted message, actual value, and remaining arguments. + */ +export function _extractArgs(theArgs: any[], mIdx: number | undefined, numArgs: number): { msg?: string, act?: any, args: any[], orgArgs: any[], m?: number } { + const orgArgsLen = theArgs.length; + let initMsg: string; + let actualValue: any; + let scopeArgs: any[]; + let startArgsIdx: number = 0; + let mode: number; + + // ------------------------------------------------------------------------- + // Extract the init message if its present to be passed to the scope context + // ------------------------------------------------------------------------- + let initMsgIdx: number = -1; + if (mIdx >= 0) { + // Positive index + if (orgArgsLen > mIdx) { + initMsg = theArgs[mIdx]; + initMsgIdx = mIdx; + if (initMsgIdx === 0) { + // If the message index is the first argument, then we can start extracting the actual value and scope args from the second argument + startArgsIdx = 1; + } + } + } else if (!isUndefined(mIdx)) { + // Negative index - replicates: if (idx >= 0 && idx < theArgs.length && numArgs >= 0 && numArgs < theArgs.length) + let idx = orgArgsLen + mIdx; + if (idx >= 0 && idx < orgArgsLen && numArgs >= 0 && numArgs < orgArgsLen) { + initMsg = theArgs[idx]; + initMsgIdx = idx; + if (initMsgIdx === 0) { + // If the message index is the first argument, then we can start extracting the actual value and scope args from the second argument + startArgsIdx = 1; + } + } + } + + if (numArgs > 0 && startArgsIdx < orgArgsLen && initMsgIdx !== startArgsIdx) { + actualValue = theArgs[startArgsIdx]; + startArgsIdx++; + } + + // ------------------------------------------------------------------------- + // Extract the scope arguments, we need to simulate the array as if the message argument + // was removed (if it was present) to correctly extract the scope arguments + // ------------------------------------------------------------------------- + if (initMsgIdx < startArgsIdx) { + // If we don't have a message argument, then we can just take the remaining arguments as scope arguments + if (startArgsIdx === 0) { + // Just return the original arguments as scope arguments if we don't have a message argument and we are not skipping any arguments + mode = 0; + scopeArgs = theArgs; + } else if (startArgsIdx < orgArgsLen) { + mode = 1; + scopeArgs = arrSlice(theArgs, startArgsIdx); + } else { + // Just return an empty array as scope arguments if we don't have any arguments after the actual value argument + mode = 2; + scopeArgs = []; + } + } else { + // If we have a message argument, then we need to skip it when extracting the scope arguments + if (initMsgIdx === orgArgsLen - 1) { + // If the message argument is the last argument, then we can just take the remaining arguments as scope arguments without the need to skip any arguments + if (startArgsIdx < orgArgsLen - 1) { + mode = 11; + scopeArgs = arrSlice(theArgs, startArgsIdx, orgArgsLen - 1); + } else { + // Just return an empty array as scope arguments if we don't have any arguments after the message argument + mode = 12; + scopeArgs = []; + } + } else { + mode = 10; + scopeArgs = []; + for (let i = startArgsIdx; i < orgArgsLen; i++) { + if (i !== initMsgIdx) { + scopeArgs.push(theArgs[i]); + } + } + } + } + + return { + msg: initMsg, + act: actualValue, + args: scopeArgs, + orgArgs: theArgs, + m: mode + }; +} + function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAssertClassDef, internalErrorStackStart: Function, responseHandler: (result: any) => any): (...args: any[]) => void | IAssertInst | IPromise { // let steps: IStepDef[]; let scopeFn: IScopeFn = def.scopeFn; @@ -546,24 +625,11 @@ function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAss } return function _assertFunc(): void | IAssertInst | IPromise { - let theArgs = arrSlice(arguments, 0); - let orgArgs = arrSlice(theArgs); - - // Extract the initial message from the passed arguments (if present) - let initMsg = _extractInitMsg(theArgs, numArgs, mIdx); - let scopeArgs: any[]; - - // Extract the actual value from the arguments - let actualValue: any; - if (theArgs.length > 0 && numArgs > 0) { - // Get the actual "value" from the first argument - actualValue = theArgs[0]; - scopeArgs = arrSlice(theArgs, 1); - } + let theArgs = _extractArgs(arrSlice(arguments), mIdx, numArgs); // Create the initial scope `expect(value, initMsg)` and run any defined steps // Using either the current alias entry point or the current function - let newScope = createAssertScope(createContext(actualValue, initMsg, _aliasStackStart || _assertFunc, orgArgs)); + let newScope = createAssertScope(createContext(theArgs.act, theArgs.msg, _aliasStackStart || _assertFunc, theArgs.orgArgs)); newScope.context._$stackFn.push(_aliasStackStart || _assertFunc); newScope.context.setOp(assertName + "()"); @@ -572,7 +638,7 @@ function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAss v: newScope.context }); - let theResult = newScope.exec(scopeFn, scopeArgs || theArgs, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous") as void | IAssertInst | IPromise; + let theResult = newScope.exec(scopeFn, theArgs.args, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous") as void | IAssertInst | IPromise; return responseHandler ? responseHandler(theResult) : theResult; }; diff --git a/core/src/assert/assertScope.ts b/core/src/assert/assertScope.ts index e2db151..3ed43a7 100644 --- a/core/src/assert/assertScope.ts +++ b/core/src/assert/assertScope.ts @@ -2,11 +2,11 @@ * @nevware21/tripwire * https://github.com/nevware21/tripwire * - * Copyright (c) 2024-2025 NevWare21 Solutions LLC + * Copyright (c) 2024-2026 NevWare21 Solutions LLC * Licensed under the MIT license. */ -import { arrSlice, objDefineProps } from "@nevware21/ts-utils"; +import { arrSlice, objDefine } from "@nevware21/ts-utils"; import { IScopeContext, IScopeContextOverrides } from "./interface/IScopeContext"; import { AssertScopeFuncDefs } from "./interface/IAssertInst"; import { MsgSource } from "./type/MsgSource"; @@ -47,16 +47,11 @@ export function createAssertScope(context: IScopeContext, handlerCreator?: Asser createOperation: createOperation }; - let _that: any = null; // Initialize with the `that` the same as the IAssertInst - - objDefineProps(theScope, { - context: { - g: () => _context - }, - "that": { - g: () => _that as any, - s: (v: any) => _that = v - } + // Define context as a value property (not getter) - fast reads, controlled writes + // w: false makes it readonly to external callers + objDefine(theScope, "context", { + v: _context, + w: false }); function newScope(value?: V): IAssertScope { @@ -105,6 +100,12 @@ export function createAssertScope(context: IScopeContext, handlerCreator?: Asser function updateCtx(value: T, overrides?: IScopeContextOverrides): IAssertScope { if (value !== _context.value || overrides) { _context = _context.new(value, overrides); + // Update the context property value directly to avoid the overhead of redefining + // the property and to preserve the readonly nature of the property for external callers. + objDefine(theScope, "context", { + v: _context, + w: false + }); } return theScope; diff --git a/core/src/assert/assertionError.ts b/core/src/assert/assertionError.ts index 0bda2ca..728486a 100644 --- a/core/src/assert/assertionError.ts +++ b/core/src/assert/assertionError.ts @@ -219,15 +219,15 @@ function _formatProps(ctx: IScopeContext | null, props: any): string { thePath = props.opPath.join("->") + "->" + lastOp; } } else { - thePath = _formatValue(ctx.opts, props.opPath) + "->" + lastOp; + thePath = _formatValue(ctx, props.opPath) + "->" + lastOp; } } else { - thePath = _formatValue(ctx.opts, props.opPath); + thePath = _formatValue(ctx, props.opPath); } let parts: string[] = [formatted, "running \"", thePath, "\""]; if (props.actual) { - parts.push(" with (", _formatValue(ctx.opts, props.actual), ")"); + parts.push(" with (", _formatValue(ctx, props.actual), ")"); } let leftOver: any = {}; objForEachKey(props, (key, value) => { diff --git a/core/src/assert/funcs/equal.ts b/core/src/assert/funcs/equal.ts index 28a598e..8a14561 100644 --- a/core/src/assert/funcs/equal.ts +++ b/core/src/assert/funcs/equal.ts @@ -298,7 +298,7 @@ function _isVisiting(value: any, options: IEqualOptions, cb: () => T): T { visitCount++; if (visitCount > 10) { // Early exit on excessive visits - let errorMsg = "Unresolvable Circular reference detected for " + _formatValue(options.context.opts, visiting[0]) + " @ depth " + depth + " reference count: " + visitCount; + let errorMsg = "Unresolvable Circular reference detected for " + _formatValue(options.context, visiting[0]) + " @ depth " + depth + " reference count: " + visitCount; options.context.fatal(errorMsg); } } diff --git a/core/src/assert/scopeContext.ts b/core/src/assert/scopeContext.ts index c0272d9..183f1d5 100644 --- a/core/src/assert/scopeContext.ts +++ b/core/src/assert/scopeContext.ts @@ -7,8 +7,8 @@ */ import { - arrForEach, arrIndexOf, arrSlice, fnApply, getDeferred, getLazy, getLength, isArray, isFunction, isObject, isPlainObject, - newSymbol, objAssign, objDefine, objDefineProps, objForEachKey, objHasOwnProperty, + arrForEach, arrIndexOf, arrSlice, fnApply, getDeferred, getLazy, getLength, isArray, isFunction, + isObject, isPlainObject, newSymbol, objAssign, objDefineProps, objForEachKey, objHasOwnProperty, objKeys, strEndsWith, strIndexOf, strLeft, strSubstring } from "@nevware21/ts-utils"; import { IScopeContext, IScopeContextOverrides } from "./interface/IScopeContext"; @@ -211,7 +211,7 @@ export function createContext(value?: T, initMsg?: MsgSource, stackStart?: Fu let result = _getTokenValue(context, details, strSubstring(message, open + 1, close)); if (result.has) { - let prefix = strLeft(message, open) + _formatValue(context.opts, result.value); + let prefix = strLeft(message, open) + _formatValue(context, result.value); message = prefix + strSubstring(message, close + 1); start = prefix.length; } else { @@ -355,37 +355,33 @@ export function createContext(value?: T, initMsg?: MsgSource, stackStart?: Fu orgArgs: null }; - objDefine(context, "_$stackFn", { - v: theStack, - e: false - }); - - // Define a non-enumerable symbol based property to get the details - // This is useful when debugging to be able to see the details without - // calling the function - objDefine(context, DetailsSymbol.v as any, { - g: () => context.getDetails(), - e: false - }); - - objDefine(context as any, cScopeContextTag, { - v: true, - e: false - }); - if (stackStart) { - context._$stackFn.push(stackStart); + theStack.push(stackStart); } return objDefineProps(context, { + _$stackFn: { + v: theStack, + e: false + }, opts: { g: () => cfgInst.v }, value: { - g: () => value + v: value, + w: false }, orgArgs: { - g: () => orgArgs + v: orgArgs, + w: false + }, + [DetailsSymbol.v]: { // Define a non-enumerable symbol based property to get the details + g: () => context.getDetails(), // This is useful when debugging to be able to see the details without calling the function + e: false + }, + [cScopeContextTag]: { + v: true, + e: false } }); } @@ -492,19 +488,6 @@ function _childContext(parent: IScopeContext, value: V, overrides?: IScopeCon orgArgs: null }; - // Define a non-enumerable symbol based property to get the details - // This is useful when debugging to be able to see the details without - // calling the function - objDefine(newContext, DetailsSymbol.v as any, { - g: () => newContext.getDetails(), - e: false - }); - - objDefine(newContext as any, cScopeContextTag, { - v: true, - e: false - }); - return objDefineProps(newContext, { _$stackFn: { v: childStack, @@ -514,10 +497,20 @@ function _childContext(parent: IScopeContext, value: V, overrides?: IScopeCon g: () => parent.opts }, value: { - g: () => value + v: value, + w: false }, orgArgs: { - g: () => parent.orgArgs + v: parent.orgArgs, + w: false + }, + [DetailsSymbol.v]: { // Define a non-enumerable symbol based property to get the details + g: () => newContext.getDetails(), // This is useful when debugging to be able to see the details without calling the function + e: false + }, + [cScopeContextTag]: { + v: true, + e: false } }); } diff --git a/core/src/config/config.ts b/core/src/config/config.ts index c7c63c5..25f9644 100644 --- a/core/src/config/config.ts +++ b/core/src/config/config.ts @@ -6,7 +6,10 @@ * Licensed under the MIT license. */ -import { arrSlice, getDeferred, ICachedValue, isArray, isNullOrUndefined, isObject, isPlainObject, objDefine, objForEachKey, objIs } from "@nevware21/ts-utils"; +import { + arrSlice, getDeferred, ICachedValue, isArray, isNullOrUndefined, isObject, isPlainObject, + objDefine, objForEachKey, objIs +} from "@nevware21/ts-utils"; import { IConfig } from "../interface/IConfig"; import { IConfigInst } from "../interface/IConfigInst"; import { IFormatter } from "../interface/IFormatter"; @@ -106,16 +109,17 @@ export function _createConfig(getDefaults: () => Readonly, parentFormat let defaultValues = getDefaults(); let theValues = _mergeConfig({}, defaultValues, true); let formatMgr: IFormatManager; + let theConfig: IConfigInst; function _getFormatMgr(): IFormatManager { if (!formatMgr) { - formatMgr = createFormatMgr(parentFormatMgr ? parentFormatMgr.v : null); + formatMgr = createFormatMgr(theConfig, parentFormatMgr ? parentFormatMgr.v : null); } return formatMgr; } - let theConfig = _setupProps({}, theValues, defaultValues, true); + let baseConfig = _setupProps({}, theValues, defaultValues, true); let theConfigApi: IConfigApi = { formatMgr: null, reset: () => { @@ -151,7 +155,18 @@ export function _createConfig(getDefaults: () => Readonly, parentFormat } }); - return objDefine(theConfig, "$ops", { - g: () => theConfigApi - }) as IConfigInst; + // Make sure we don't accidentally expose the internal format manager instance and cause issues + // with cloning and dumping the config. + objDefine(theConfigApi as any, "toJSON", { + v: () => { + return theConfig.$ops.formatMgr.format(theConfigApi); + } + }); + + theConfig = objDefine(baseConfig as IConfigInst, "$ops", { + v: theConfigApi + // e: false + }); + + return theConfig; } diff --git a/core/src/config/defaultConfig.ts b/core/src/config/defaultConfig.ts index 10e4d1f..3cf9b43 100644 --- a/core/src/config/defaultConfig.ts +++ b/core/src/config/defaultConfig.ts @@ -30,7 +30,8 @@ export const DEFAULT_CONFIG: Readonly = (/* $__PURE__ */objFreeze({ finalize: false, finalizeFn: undefined, maxProps: 8, - maxFormatDepth: 50 + maxFormatDepth: 50, + maxProtoDepth: 4 }, circularMsg: () => cyan("[]"), showDiff: true, diff --git a/core/src/interface/IFormatManager.ts b/core/src/interface/IFormatManager.ts index 9e5830b..652d362 100644 --- a/core/src/interface/IFormatManager.ts +++ b/core/src/interface/IFormatManager.ts @@ -51,4 +51,15 @@ export interface IFormatManager { * Reset the format manager to its initial state, removing all registered formatters */ reset(): void; + + /** + * Format a value using the configured formatters and format options. + * This is a convenience method that uses the internal formatting logic, + * applying all registered formatters, circular reference detection, + * and finalization options. + * @param value - The value to format + * @returns The formatted string representation of the value + * @since 0.1.8 + */ + format(value: any): string; } diff --git a/core/src/interface/IFormatterOptions.ts b/core/src/interface/IFormatterOptions.ts index d866343..21b6667 100644 --- a/core/src/interface/IFormatterOptions.ts +++ b/core/src/interface/IFormatterOptions.ts @@ -162,4 +162,26 @@ export interface IFormatterOptions { * ``` */ maxFormatDepth?: number; + + /** + * Maximum depth to walk up the prototype chain when collecting object keys for display. + * Limits how many levels of inherited properties are included when formatting objects. + * Most relevant properties are typically within the first 2-3 prototype levels. + * + * @default 4 + * @since 0.1.8 + * @example + * ```typescript + * // Walk further up the prototype chain + * assertConfig.format = { + * maxProtoDepth: 5 + * }; + * + * // Only show own properties (no inherited) + * assertConfig.format = { + * maxProtoDepth: 1 + * }; + * ``` + */ + maxProtoDepth?: number; } diff --git a/core/src/internal/_defaultFormatters.ts b/core/src/internal/_defaultFormatters.ts index 7bb4e2d..85e790a 100644 --- a/core/src/internal/_defaultFormatters.ts +++ b/core/src/internal/_defaultFormatters.ts @@ -10,8 +10,7 @@ import { eFormatResult, IFormatCtx, IFormattedValue, IFormatter } from "../inter import { arrForEach, asString, dumpObj, isArray, isDate, isError, isFunction, isMapLike, isPlainObject, isPrimitive, isRegExp, isSetLike, isStrictNullOrUndefined, isString, isSymbol, iterForOf, - objCreate, - objForEachKey, objGetOwnPropertySymbols, objGetPrototypeOf, strIndexOf + objCreate, objForEachKey, objGetOwnPropertySymbols, objGetPrototypeOf, strIndexOf } from "@nevware21/ts-utils"; import { EMPTY_STRING } from "../assert/internal/const"; @@ -127,7 +126,7 @@ const _defaultPlainObjectFormatter: IFormatter = { let idx = 0; let maxProps = ctx.cfg.format.maxProps; - arrForEach(_getObjKeys(value), (key) => { + arrForEach(_getObjKeys(value, ctx.cfg.format.maxProtoDepth), (key) => { if (idx >= maxProps) { parts.push("..."); return -1; // Break from arrForEach @@ -451,32 +450,37 @@ const _defaultFallbackFormatter: IFormatter = { * @internal * @ignore * Default formatters in order of precedence + * Ordered by probability - most common types first for better performance + * Note: More specific formatters (like ErrorType) must come before more general ones (like Function) */ export const _defaultFormatters: IFormatter[] = [ - _defaultArrayFormatter, - _defaultStringFormatter, - _defaultRegExpFormatter, - _defaultSymbolFormatter, - _defaultPlainObjectFormatter, - _defaultErrorFormatter, - _defaultErrorTypeFormatter, - _defaultFunctionFormatter, - _defaultSetFormatter, - _defaultMapFormatter, - _defaultDateFormatter, - _defaultConstructorFormatter, - _defaultToStringFormatter, - _defaultFallbackFormatter + _defaultStringFormatter, // Most common in assertions + _defaultPlainObjectFormatter, // Very common + _defaultArrayFormatter, // Common + _defaultErrorFormatter, // Common in test failures + _defaultErrorTypeFormatter, // Must come before FunctionFormatter (more specific) + _defaultFunctionFormatter, // Common + _defaultDateFormatter, // Moderately common + _defaultSetFormatter, // Less common + _defaultMapFormatter, // Less common + _defaultRegExpFormatter, // Rare + _defaultSymbolFormatter, // Rare + _defaultConstructorFormatter, // Fallback + _defaultToStringFormatter, // Fallback + _defaultFallbackFormatter // Last resort ]; -function _getObjKeys(target: T): (keyof T)[] { +function _getObjKeys(target: T, maxDepth: number): (keyof T)[] { let keys: any[] = []; let seenKeys: any = objCreate(null); // Hash map for O(1) lookups, no prototype to avoid collisions let seenSymbols: any[] = []; // Symbols can't be object keys, keep array let currentObj = target; + let depth = 0; - while (!isStrictNullOrUndefined(currentObj)) { + // Limit depth - most relevant properties are within 2 levels + while (!isStrictNullOrUndefined(currentObj) && + (!maxDepth || depth < maxDepth)) { objForEachKey(currentObj, (key: any) => { if (!seenKeys[key]) { // O(1) lookup - much faster than arrIndexOf @@ -506,6 +510,7 @@ function _getObjKeys(target: T): (keyof T)[] { break; } currentObj = newObj; + depth++; } return keys; diff --git a/core/src/internal/_formatValue.ts b/core/src/internal/_formatValue.ts index e1f5782..765c32f 100644 --- a/core/src/internal/_formatValue.ts +++ b/core/src/internal/_formatValue.ts @@ -6,153 +6,16 @@ * Licensed under the MIT license. */ -import { arrForEach, asString, dumpObj, isFunction, isPrimitive, objDefine } from "@nevware21/ts-utils"; -import { EMPTY_STRING } from "../assert/internal/const"; -import { eFormatResult, IFormatCtx, IFormattedValue } from "../interface/IFormatter"; -import { escapeAnsi, yellow } from "@nevware21/chromacon"; -import { IFormatter } from "../interface/IFormatter"; -import { IConfigInst } from "../interface/IConfigInst"; -import { _defaultFormatters } from "./_defaultFormatters"; - - -function _isVisited(value: any, visited: any[], maxDepth?: number): boolean { - if (isPrimitive(value)) { - return false; - } - - // Depth limit optimization - most structures don't go beyond 50 levels - // This prevents pathological cases while maintaining reasonable depth - let depth = visited.length; - if (maxDepth && depth > maxDepth) { - return true; // Treat as circular to prevent deep recursion - } - - // Search backwards - more likely to find recent values - // Most circular references are to recently visited objects (better cache locality) - for (let idx = depth - 1; idx >= 0; idx--) { - if (visited[idx] === value) { - return true; - } - } - - return false; -} - -/** - * Perform the default formatting of a value using the provided format context. - * @param formatCtx The format context to use for formatting the value. - * @param value The value to format. - * @returns A string representation of the value. - */ -function _doFormat(formatCtx: IFormatCtx, value: any): string { - let formattedValue: IFormattedValue; - let result: string; - - function _format(formatter: IFormatter) { - try { - let fn = formatter.value; - - if (isFunction(fn)) { - let formatted = fn(formatCtx, value); - if (formatted && (formatted.res === eFormatResult.Ok || formatted.res === eFormatResult.Continue)) { - formattedValue = formatted; - if (formatted.res === eFormatResult.Ok) { - return -1; - } - } - } - } catch (e) { - formattedValue = { - res: eFormatResult.Failed, - err: e, - val: asString(value) - }; - } - } - - try { - // Process the custom formatters first - formatCtx.cfg.$ops.formatMgr.forEach(_format); - if (!formattedValue || formattedValue.res !== eFormatResult.Ok) { - // Iterate through the default formatters to find one that can format the value - arrForEach(_defaultFormatters, _format); - } - - if (formattedValue) { - if (formattedValue.res === eFormatResult.Ok || formattedValue.res === eFormatResult.Continue) { - result = formattedValue.val; - } else if (formattedValue.res === eFormatResult.Failed) { - result = dumpObj(formattedValue.err); - } else { - result = asString(value); - } - } else { - result = asString(value); - } - } catch (e) { - result = yellow("(" + _doFormat(formatCtx, e) + ")"); - } - - return result; -} - -/** - * @internal - * @ignore - * 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. - */ -function _createFormatCtx(cfg: IConfigInst): IFormatCtx { - let visited: any[] = []; - - let formatCtx: IFormatCtx = { - cfg: cfg, - format: (value: any): string => { - let maxDepth = (cfg.format && cfg.format.maxFormatDepth) || 50; - let isVisited = _isVisited(value, visited, maxDepth); - if (isVisited) { - // Circular reference detected or max depth exceeded - return cfg.circularMsg() || EMPTY_STRING; - } - - visited.push(value); - - let formattedValue: string; - try { - formattedValue = _doFormat(formatCtx, value); - } finally { - visited.pop(); - } - - return formattedValue; - } - }; - - return objDefine(formatCtx, "cfg", { v: cfg, w: false }); -} +import { IScopeContext } from "../assert/interface/IScopeContext"; /** * @internal * @ignore * Internal helper to format a value for display in an error messages. - * @param cfg - Configuration instance containing the formatters to use. + * @param ctx - Scope context containing the configuration and formatters to use. * @param value - The value to format. * @returns - A string representation of the value. */ -export function _formatValue(cfg: IConfigInst, value: any): string { - let formatCtx = _createFormatCtx(cfg); - let formatOpts = formatCtx.cfg.format; - let result = _doFormat(formatCtx, value); - - if (formatOpts && formatOpts.finalize) { - if (isFunction(formatOpts.finalizeFn)) { - result = formatOpts.finalizeFn(result); - } else { - result = escapeAnsi(result); - } - } - - return result; +export function _formatValue(ctx: IScopeContext, value: any): string { + return ctx.opts.$ops.formatMgr.format(value); } diff --git a/core/src/internal/formatManager.ts b/core/src/internal/formatManager.ts index 5206c94..b657c52 100644 --- a/core/src/internal/formatManager.ts +++ b/core/src/internal/formatManager.ts @@ -6,11 +6,179 @@ * Licensed under the MIT license. */ -import { arrAppend, arrForEach, arrIndexOf, isArray } from "@nevware21/ts-utils"; +import { arrAppend, arrForEach, arrIndexOf, asString, dumpObj, getDeferred, ICachedValue, isArray, isFunction, isPrimitive, objDefine } from "@nevware21/ts-utils"; import { IFormatManager } from "../interface/IFormatManager"; -import { IFormatter } from "../interface/IFormatter"; +import { eFormatResult, IFormatCtx, IFormattedValue, IFormatter } from "../interface/IFormatter"; import { IRemovable } from "../interface/IRemovable"; import { _noOpFn } from "./_noOp"; +import { IConfigInst } from "../interface/IConfigInst"; +import { EMPTY_STRING } from "../assert/internal/const"; +import { escapeAnsi, yellow } from "@nevware21/chromacon"; +import { _defaultFormatters } from "./_defaultFormatters"; + +let _circularTrack: any[]; +function _withCircularTrack(fn: () => string): string { + let currentTrack = _circularTrack; + + _circularTrack = []; + try { + return fn(); + } finally { + _circularTrack = currentTrack; + } +} + +function _isVisited(value: any, visited: any[], maxDepth?: number): boolean { + let result = false; + + if (!isPrimitive(value)) { + const depth = visited.length; + if (depth > 0) { + if (depth === 1) { + result = visited[0] === value; + } else if (maxDepth && depth > maxDepth) { + // Depth limit optimization - most structures don't go beyond 50 levels + // This prevents pathological cases while maintaining reasonable depth + result = true; // Treat as circular to prevent deep recursion + } else { + // Search backwards - more likely to find recent values + // Most circular references are to recently visited objects (better cache locality) + for (let idx = depth - 1; idx >= 0; idx--) { + if (visited[idx] === value) { + result = true; + break; + } + } + } + } + } + + return result; +} + +/** + * Perform the default formatting of a value using the provided format context. + * @param formatCtx The format context to use for formatting the value. + * @param value The value to format. + * @returns A string representation of the value. + */ +function _doFormat(formatCtx: IFormatCtx, value: any): string { + let formattedValue: IFormattedValue; + let result: string; + + function _format(formatter: IFormatter) { + try { + let fn = formatter.value; + + if (isFunction(fn)) { + let formatted = fn(formatCtx, value); + if (formatted && (formatted.res === eFormatResult.Ok || formatted.res === eFormatResult.Continue)) { + formattedValue = formatted; + if (formatted.res === eFormatResult.Ok) { + return -1; + } + } + } + } catch (e) { + formattedValue = { + res: eFormatResult.Failed, + err: e, + val: asString(value) + }; + } + } + + try { + // Process the custom formatters first + formatCtx.cfg.$ops.formatMgr.forEach(_format); + if (!formattedValue || formattedValue.res !== eFormatResult.Ok) { + // Iterate through the default formatters to find one that can format the value + arrForEach(_defaultFormatters, _format); + } + + if (formattedValue) { + if (formattedValue.res === eFormatResult.Ok || formattedValue.res === eFormatResult.Continue) { + result = formattedValue.val; + } else if (formattedValue.res === eFormatResult.Failed) { + result = dumpObj(formattedValue.err); + } else { + result = asString(value); + } + } else { + result = asString(value); + } + } catch (e) { + result = yellow("(" + _doFormat(formatCtx, e) + ")"); + } + + return result; +} + +/** + * @internal + * @ignore + * 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. + */ +function _createFormatCtx(cfg: IConfigInst): IFormatCtx { + let formatCtx: IFormatCtx = { + cfg: cfg, + format: (value: any): string => { + if (!_circularTrack) { + _circularTrack = []; + } + + let maxDepth = (cfg.format && cfg.format.maxFormatDepth) || 50; + let isVisited = _isVisited(value, _circularTrack, maxDepth); + if (isVisited) { + // Circular reference detected or max depth exceeded + return cfg.circularMsg() || EMPTY_STRING; + } + + _circularTrack.push(value); + + let formattedValue: string; + try { + formattedValue = _doFormat(formatCtx, value); + } finally { + _circularTrack.pop(); + } + + return formattedValue; + } + }; + + return objDefine(formatCtx, "cfg", { v: cfg, w: false }); +} + +/** + * @internal + * @ignore + * Internal helper to format a value for display in an error messages. + * @param ctx - Scope context containing the configuration and formatters to use. + * @param value - The value to format. + * @returns - A string representation of the value. + */ +function _formatCtxValue(formatCtx: IFormatCtx, value: any): string { + let formatOpts = formatCtx.cfg.format; + + return _withCircularTrack(() => { + let result = _doFormat(formatCtx, value); + + // Apply finalization if configured + if (formatOpts && formatOpts.finalize) { + if (isFunction(formatOpts.finalizeFn)) { + result = formatOpts.finalizeFn(result); + } else { + result = escapeAnsi(result); + } + } + + return result; + }); +} /** * Creates a new format manager instance that manages value formatters. @@ -23,8 +191,11 @@ import { _noOpFn } from "./_noOp"; * @since 0.1.5 * @group Formatter */ -export function createFormatMgr(parent?: IFormatManager): IFormatManager { +export function createFormatMgr(cfg: IConfigInst, parent?: IFormatManager): IFormatManager { const _formatters: IFormatter[] = []; + let formatCtx: ICachedValue = getDeferred(() => { + return _createFormatCtx(cfg); + }); function _addFormatter(formatter: IFormatter | Array): IRemovable { arrAppend(_formatters, formatter); @@ -81,11 +252,17 @@ export function createFormatMgr(parent?: IFormatManager): IFormatManager { _formatters.length = 0; } + function _format(value: any): string { + return _formatCtxValue(formatCtx.v, value); + } + return { addFormatter: _addFormatter, removeFormatter: _removeFormatter, getFormatters: _getFormatters, forEach: _forEach, - reset: _reset + reset: _reset, + format: _format + }; } diff --git a/core/test/src/assert/assert.changes.test.ts b/core/test/src/assert/assert.changes.test.ts index 219c49e..dc9f19f 100644 --- a/core/test/src/assert/assert.changes.test.ts +++ b/core/test/src/assert/assert.changes.test.ts @@ -7,7 +7,7 @@ */ import { assert } from "../../../src/assert/assertClass"; -import { expect } from "../../../src/index"; +import { expect } from "../../../src/assert/expect"; import { checkError } from "../support/checkError"; describe("assert.changes", () => { diff --git a/core/test/src/assert/assert.isInstanceOf.test.ts b/core/test/src/assert/assert.isInstanceOf.test.ts index 1b98cd2..8255d31 100644 --- a/core/test/src/assert/assert.isInstanceOf.test.ts +++ b/core/test/src/assert/assert.isInstanceOf.test.ts @@ -6,7 +6,9 @@ * Licensed under the MIT license. */ -import { assert, AssertionFailure, expect } from "../../../src/index"; +import { assert } from "../../../src/assert/assertClass"; +import { AssertionFailure } from "../../../src/assert/assertionError"; +import { expect } from "../../../src/assert/expect"; describe("assert.isInstanceOf", () => { diff --git a/core/test/src/assert/deepEqual.edgeCases.test.ts b/core/test/src/assert/deepEqual.edgeCases.test.ts index 67cd6a2..9e1125c 100644 --- a/core/test/src/assert/deepEqual.edgeCases.test.ts +++ b/core/test/src/assert/deepEqual.edgeCases.test.ts @@ -6,7 +6,8 @@ * Licensed under the MIT license. */ -import { assert, assertConfig } from "../../../src/index"; +import { assert } from "../../../src/assert/assertClass"; +import { assertConfig } from "../../../src/config/assertConfig"; import { checkError } from "../support/checkError"; describe("deepEqual edge cases and depth limits", () => { diff --git a/core/test/src/assert/extractArgs.test.ts b/core/test/src/assert/extractArgs.test.ts new file mode 100644 index 0000000..0b35788 --- /dev/null +++ b/core/test/src/assert/extractArgs.test.ts @@ -0,0 +1,350 @@ +/* + * @nevware21/tripwire + * https://github.com/nevware21/tripwire + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { _extractArgs, assert } from "../../../src/assert/assertClass"; + +describe("_extractArgs", function () { + this.timeout(5000); + + describe("with no message index (default -1)", () => { + it("should extract message from last position by default", () => { + const result = _extractArgs([42, "test message"], -1, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 0); + assert.equal(result.m, 12); + }); + + it("should extract actual value when only one arg and no message", () => { + const result = _extractArgs([42], -1, 1); + + assert.equal(result.act, 42); + assert.isUndefined(result.msg); + assert.isArray(result.args); + assert.equal(result.args.length, 0); + assert.equal(result.m, 2); + }); + + it("should extract message from last position when numArgs < theArgs.length", () => { + const result = _extractArgs([42, 100, 200], -1, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, 200); // Last arg is extracted as message when numArgs (1) < theArgs.length (3) + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 11); + }); + }); + + describe("with positive message index", () => { + it("should extract message from specified positive index (index 0)", () => { + const result = _extractArgs(["test message", 42, 100], 0, 1); + + assert.equal(result.msg, "test message"); + assert.equal(result.act, 42); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 1); + }); + + it("should extract message from specified positive index (index 1)", () => { + const result = _extractArgs([42, "test message", 100], 1, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 10); + }); + + it("should extract message from specified positive index (index 2)", () => { + const result = _extractArgs([42, 100, "test message"], 2, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 11); + }); + + it("should not extract message if index is out of bounds", () => { + const result = _extractArgs([42, 100], 5, 1); + + assert.equal(result.act, 42); + assert.isUndefined(result.msg); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 1); // No message, simple slice mode + }); + }); + + describe("with negative message index", () => { + it("should extract message from end (index -1)", () => { + const result = _extractArgs([42, 100, "test message"], -1, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 11); + }); + + it("should extract message from end (index -2)", () => { + const result = _extractArgs([42, "test message", 100], -2, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 10); + }); + + it("should only extract negative index message when numArgs condition is met", () => { + // When numArgs >= argLen, negative index should not extract + const result = _extractArgs([42, "test message"], -1, 3); + + assert.equal(result.act, 42); + assert.isUndefined(result.msg); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], "test message"); + assert.equal(result.m, 1); // No message extracted, simple slice mode + }); + }); + + describe("with numArgs = 0 (no actual value expected)", () => { + it("should not extract actual value when numArgs is 0", () => { + const result = _extractArgs([100, 200, "test message"], -1, 0); + + assert.isUndefined(result.act); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.equal(result.args[0], 100); + assert.equal(result.args[1], 200); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should extract message from last position when numArgs is 0 and numArgs < theArgs.length", () => { + const result = _extractArgs([100, 200, 300], -1, 0); + + assert.isUndefined(result.act); + assert.equal(result.msg, 300); // Last arg is extracted as message when numArgs (0) < theArgs.length (3) + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.equal(result.args[0], 100); + assert.equal(result.args[1], 200); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + }); + + describe("with numArgs = 2 (actual value + 1 expected arg)", () => { + it("should extract actual value and scope arg, with message from last position", () => { + const result = _extractArgs([42, 100, 200], -1, 2); + + assert.equal(result.act, 42); + assert.equal(result.msg, 200); // Last arg extracted as message when numArgs (2) < theArgs.length (3) + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should extract actual value, scope arg, and message", () => { + const result = _extractArgs([42, 100, "test message"], -1, 2); + + assert.equal(result.act, 42); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 1); + assert.equal(result.args[0], 100); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + }); + + describe("edge cases", () => { + it("should handle empty args array", () => { + const result = _extractArgs([], -1, 1); + + assert.isUndefined(result.act); + assert.isUndefined(result.msg); + assert.isArray(result.args); + assert.equal(result.args.length, 0); + assert.equal(result.m, 0); // Empty array, no processing mode + }); + + it("should extract single arg as message when numArgs = 0 and numArgs < theArgs.length", () => { + const result = _extractArgs([42], -1, 0); + + assert.isUndefined(result.act); + assert.equal(result.msg, 42); // Single arg extracted as message when numArgs (0) < theArgs.length (1) + assert.isArray(result.args); + assert.equal(result.args.length, 0); + assert.equal(result.m, 2); // Message at index 0, empty scopeArgs + }); + + it("should handle message at first position with numArgs = 0", () => { + const result = _extractArgs(["test message", 100, 200], 0, 0); + + assert.isUndefined(result.act); + assert.equal(result.msg, "test message"); + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.equal(result.args[0], 100); + assert.equal(result.args[1], 200); + assert.equal(result.m, 1); // Message at index 0, simple slice mode + }); + + it("should handle undefined message index", () => { + const result = _extractArgs([42, 100, 200], undefined, 1); + + assert.equal(result.act, 42); + assert.isUndefined(result.msg); + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.equal(result.args[0], 100); + assert.equal(result.args[1], 200); + assert.equal(result.m, 1); // No message, simple slice mode + }); + + it("should preserve arg types correctly", () => { + const obj = { a: 1 }; + const arr = [1, 2, 3]; + const fn = () => {}; + + const result = _extractArgs([obj, arr, fn, "msg"], -1, 1); + + assert.equal(result.act, obj); + assert.equal(result.msg, "msg"); + assert.equal(result.args[0], arr); + assert.equal(result.args[1], fn); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should handle null and undefined values correctly", () => { + const result = _extractArgs([null, undefined, 0, "msg"], -1, 1); + + assert.isNull(result.act); + assert.equal(result.msg, "msg"); + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.isUndefined(result.args[0]); + assert.equal(result.args[1], 0); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + }); + + describe("performance optimization validation", () => { + it("should not modify original args array", () => { + const originalArgs = [42, 100, "test message"]; + const argsCopy = [...originalArgs]; + + const result = _extractArgs(originalArgs, -1, 1); + + assert.deepEqual(originalArgs, argsCopy); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should handle many arguments efficiently", () => { + const manyArgs: any[] = Array.from({ length: 100 }, (_, i) => i); + manyArgs.push("test message"); + + const result = _extractArgs(manyArgs, -1, 1); + + assert.equal(result.act, 0); + assert.equal(result.msg, "test message"); + assert.equal(result.args.length, 99); + assert.equal(result.args[0], 1); + assert.equal(result.args[98], 99); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should create scopeArgs only when needed", () => { + // Single arg, no additional scope args + const result1 = _extractArgs([42], -1, 1); + assert.isArray(result1.args); + assert.equal(result1.args.length, 0); + assert.equal(result1.m, 2); // Single arg, empty scopeArgs mode + + // Single arg + message, no additional scope args + const result2 = _extractArgs([42, "msg"], -1, 1); + assert.isArray(result2.args); + assert.equal(result2.args.length, 0); + assert.equal(result2.m, 12); // Message at last, empty scopeArgs mode + }); + }); + + describe("real-world usage scenarios", () => { + it("should handle assert.equal(actual, expected, message) pattern", () => { + const result = _extractArgs([42, 42, "should be equal"], -1, 2); + + assert.equal(result.act, 42); + assert.equal(result.args[0], 42); + assert.equal(result.msg, "should be equal"); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should handle assert.throws(fn, error, message) pattern", () => { + const fn = () => {}; + const errorType = Error; + + const result = _extractArgs([fn, errorType, "should throw"], -1, 3); + + assert.equal(result.act, fn); + assert.equal(result.args[0], errorType); + // When numArgs (3) >= theArgs.length (3), NO message extracted + assert.isUndefined(result.msg); + assert.equal(result.args.length, 2); + assert.equal(result.args[1], "should throw"); // Message becomes a scope arg + assert.equal(result.m, 1); // No message extracted, simple slice mode + }); + + it("should handle assert.fail(message) pattern", () => { + const result = _extractArgs(["failure message"], -1, 0); + + assert.isUndefined(result.act); + assert.equal(result.msg, "failure message"); + assert.equal(result.args.length, 0); + assert.equal(result.m, 2); // Single arg extracted as message, empty scopeArgs + }); + + it("should handle assert.hasProperty(obj, prop, value, message) pattern", () => { + const obj = { a: 1 }; + + const result = _extractArgs([obj, "a", 1, "should have property"], -1, 3); + + assert.equal(result.act, obj); + assert.equal(result.args[0], "a"); + assert.equal(result.args[1], 1); + assert.equal(result.msg, "should have property"); + assert.equal(result.m, 11); // Message at last position, slice mode + }); + + it("should handle message at custom position (index 1)", () => { + // Simulating message at position 1, not at the end + const result = _extractArgs([42, "custom message", 100, 200], 1, 1); + + assert.equal(result.act, 42); + assert.equal(result.msg, "custom message"); + assert.isArray(result.args); + assert.equal(result.args.length, 2); + assert.equal(result.args[0], 100); + assert.equal(result.args[1], 200); + assert.equal(result.m, 10); // Message at middle position, iterate and skip mode + }); + }); +}); diff --git a/core/test/src/assert/members.test.ts b/core/test/src/assert/members.test.ts index 625615d..7cbd975 100644 --- a/core/test/src/assert/members.test.ts +++ b/core/test/src/assert/members.test.ts @@ -6,7 +6,8 @@ * Licensed under the MIT license. */ -import { assert, expect } from "../../../src/index"; +import { assert } from "../../../src/assert/assertClass"; +import { expect } from "../../../src/assert/expect"; describe("Member Comparison Tests", () => { describe("sameMembers", () => { diff --git a/core/test/src/config/maxDepth.test.ts b/core/test/src/config/maxDepth.test.ts index 58eb3cf..faecb9a 100644 --- a/core/test/src/config/maxDepth.test.ts +++ b/core/test/src/config/maxDepth.test.ts @@ -6,7 +6,9 @@ * Licensed under the MIT license. */ -import { assert, assertConfig, expect } from "../../../src/index"; +import { assert } from "../../../src/assert/assertClass"; +import { expect } from "../../../src/assert/expect"; +import { assertConfig } from "../../../src/config/assertConfig"; describe("Config max depth limits", () => { afterEach(() => { diff --git a/core/test/src/internal/formatManager.test.ts b/core/test/src/internal/formatManager.test.ts new file mode 100644 index 0000000..27233dd --- /dev/null +++ b/core/test/src/internal/formatManager.test.ts @@ -0,0 +1,90 @@ +/* + * @nevware21/tripwire + * https://github.com/nevware21/tripwire + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { assert } from "../../../src/assert/assertClass"; +import { assertConfig } from "../../../src/config/assertConfig"; + +describe("formatManager", function () { + describe("formatMgr.format() convenience method", function () { + afterEach(function () { + assertConfig.$ops.reset(); + }); + + it("should format simple objects", function () { + let formatMgr = assertConfig.$ops.formatMgr; + + let result = formatMgr.format({ a: 1, b: "test" }); + assert.equal(result, "{a:1,b:\"test\"}"); + }); + + it("should format circular objects", function () { + let formatMgr = assertConfig.$ops.formatMgr; + let circular = assertConfig.circularMsg?.(); + + let obj: any = { value: 42 }; + obj.self = obj; + + let result = formatMgr.format(obj); + // When using formatMgr.format(), circular tracking starts fresh for each call + // so the object is formatted one level deep before detecting circularity + assert.equal(result, `{value:42,self:{value:42,self:${circular}}}`); + }); + + it("should apply finalize option", function () { + let formatMgr = assertConfig.$ops.formatMgr; + + assertConfig.format = { + finalize: true + }; + + let obj = { text: "\x1b[31mRed\x1b[0m" }; + let result = formatMgr.format(obj); + + // Should escape ANSI codes + assert.includes(result, "\\x1b[31m"); + assert.includes(result, "\\x1b[0m"); + }); + + it("should work with custom formatters", function () { + let formatMgr = assertConfig.$ops.formatMgr; + + formatMgr.addFormatter({ + name: "testFormatter", + value: (ctx, value) => { + if (typeof value === "number" && value > 100) { + return { + res: 0, // eFormatResult.Ok + val: `BIG:${value}` + }; + } + return { res: 2 }; // eFormatResult.Skip + } + }); + + let result = formatMgr.format({ small: 10, large: 500 }); + assert.equal(result, "{small:10,large:BIG:500}"); + }); + + it("should handle multiple format calls independently", function () { + let formatMgr = assertConfig.$ops.formatMgr; + + let obj1 = { type: "first" }; + let obj2 = { type: "second" }; + + let result1 = formatMgr.format(obj1); + let result2 = formatMgr.format(obj2); + + assert.equal(result1, "{type:\"first\"}"); + assert.equal(result2, "{type:\"second\"}"); + + // Format obj1 again - should not be affected by previous calls + let result3 = formatMgr.format(obj1); + assert.equal(result3, "{type:\"first\"}"); + }); + }); +}); diff --git a/core/test/src/operations/includeOp.edgeCases.test.ts b/core/test/src/operations/includeOp.edgeCases.test.ts index 780c386..1618f3f 100644 --- a/core/test/src/operations/includeOp.edgeCases.test.ts +++ b/core/test/src/operations/includeOp.edgeCases.test.ts @@ -6,8 +6,8 @@ * Licensed under the MIT license. */ -import { expect } from "../../../src/index"; import { createAssertScope } from "../../../src/assert/assertScope"; +import { expect } from "../../../src/assert/expect"; import { includeOp, deepIncludeOp, ownIncludeOp, deepOwnIncludeOp } from "../../../src/assert/ops/includeOp"; import { createContext } from "../../../src/assert/scopeContext"; import { checkError } from "../support/checkError"; diff --git a/core/test/src/support/checkError.ts b/core/test/src/support/checkError.ts index 1e371e6..cac4cf8 100644 --- a/core/test/src/support/checkError.ts +++ b/core/test/src/support/checkError.ts @@ -2,7 +2,7 @@ * @nevware21/tripwire * https://github.com/nevware21/tripwire * - * Copyright (c) 2024-2025 NevWare21 Solutions LLC + * Copyright (c) 2024-2026 NevWare21 Solutions LLC * Licensed under the MIT license. */ @@ -242,7 +242,7 @@ export function checkError(fn: () => void, match: string | RegExp | Object, chec expect(theStack).is.string(); if (CHECK_INTERNAL_STACK_FRAME_REGEX.test(theStack)) { let scope = preContext || getScopeContext(assert); - throw new AssertionError("expected error stack to not contain internal frames - " + _formatValue(scope.opts, theStack), e, null, stackStart); + throw new AssertionError("expected error stack to not contain internal frames - " + _formatValue(scope, theStack), e, null, stackStart); } }