import type { PrettyFormatOptions } from '@vitest/pretty-format' import { createDOMElementFilter, format as prettyFormat, plugins as prettyFormatPlugins, } from '@vitest/pretty-format' const { AsymmetricMatcher, DOMCollection, DOMElement, Immutable, ReactElement, ReactTestComponent, } = prettyFormatPlugins const PLUGINS = [ ReactTestComponent, ReactElement, DOMElement, DOMCollection, Immutable, AsymmetricMatcher, ] export interface StringifyOptions extends PrettyFormatOptions { maxLength?: number filterNode?: string | ((node: any) => boolean) } export function stringify( object: unknown, maxDepth = 10, { maxLength, filterNode, ...options }: StringifyOptions = {}, ): string { const MAX_LENGTH = maxLength ?? 10000 let result // Convert string selector to filter function const filterFn = typeof filterNode === 'string' ? createNodeFilterFromSelector(filterNode) : filterNode const plugins = filterFn ? [ ReactTestComponent, ReactElement, createDOMElementFilter(filterFn), DOMCollection, Immutable, AsymmetricMatcher, ] : PLUGINS try { result = prettyFormat(object, { maxDepth, escapeString: false, // min: true, plugins, ...options, }) } catch { result = prettyFormat(object, { callToJSON: false, maxDepth, escapeString: false, // min: true, plugins, ...options, }) } // Prevents infinite loop https://github.com/vitest-dev/vitest/issues/7249 return result.length >= MAX_LENGTH && maxDepth > 1 ? stringify(object, Math.floor(Math.min(maxDepth, Number.MAX_SAFE_INTEGER) / 2), { maxLength, filterNode, ...options, }) : result } function createNodeFilterFromSelector(selector: string): (node: any) => boolean { const ELEMENT_NODE = 1 const COMMENT_NODE = 8 return (node: any) => { // Filter out comments if (node.nodeType === COMMENT_NODE) { return false } // Filter out elements matching the selector if (node.nodeType === ELEMENT_NODE && node.matches) { try { return !node.matches(selector) } catch { return true } } return true } } export const formatRegExp: RegExp = /%[sdjifoOc%]/g export function format(args: unknown[], options: InspectOptions = {}): string { const formatArg = (item: unknown) => inspect(item, options) if (typeof args[0] !== 'string') { const objects = [] for (let i = 0; i < args.length; i++) { objects.push(formatArg(args[i])) } return objects.join(' ') } const len = args.length let i = 1 const template = args[0] let str = String(template).replace(formatRegExp, (x) => { if (x === '%%') { return '%' } if (i >= len) { return x } switch (x) { case '%s': { const value = args[i++] if (typeof value === 'bigint') { return `${value.toString()}n` } if (typeof value === 'number' && value === 0 && 1 / value < 0) { return '-0' } if (typeof value === 'object' && value !== null) { if ( typeof value.toString === 'function' && value.toString !== Object.prototype.toString ) { return value.toString() } return formatArg(value) } return String(value) } case '%d': { const value = args[i++] if (typeof value === 'bigint') { return `${value.toString()}n` } if (typeof value === 'symbol') { return 'NaN' } return Number(value).toString() } case '%i': { const value = args[i++] if (typeof value === 'bigint') { return `${value.toString()}n` } return Number.parseInt(String(value)).toString() } case '%f': return Number.parseFloat(String(args[i++])).toString() case '%o': case '%O': return formatArg(args[i++]) case '%c': { i++ return '' } case '%j': try { return JSON.stringify(args[i++]) } catch (err: any) { const m = err.message if ( // chromium m.includes('circular structure') || // safari m.includes('cyclic structures') || // firefox m.includes('cyclic object') ) { return '[Circular]' } throw err } default: return x } }) for (let x = args[i]; i < len; x = args[++i]) { if (x === null || typeof x !== 'object') { str += ` ${typeof x === 'symbol' ? x.toString() : x}` } else { str += ` ${formatArg(x)}` } } return str } export interface InspectOptions extends StringifyOptions { truncate?: number multiline?: boolean } export function inspect(obj: unknown, options?: InspectOptions): string { const { truncate, multiline, ...stringifyOptions } = options ?? {} const prettyFormatOptions: PrettyFormatOptions = { singleQuote: true, quoteKeys: false, min: true, spacingInner: ' ', spacingOuter: ' ', printBasicPrototype: false, compareKeys: null, ...(multiline ? { min: false, spacingInner: undefined, spacingOuter: undefined } : {}), } const threshold = truncate ?? 0 const formatted = stringify(obj, undefined, { ...prettyFormatOptions, ...stringifyOptions, maxLength: threshold || undefined, }) if (threshold === 0 || formatted.length <= threshold) { return formatted } // if stringify's adaptive maxDepth (down to 1) fails to truncate enough, // - for known types (e.g. string, object, array, etc), apply best effort truncation. // - for other values, fallback to maxDepth = 0 which should can show minimal output. const type = Object.prototype.toString.call(obj) if (typeof obj === 'string') { let end = threshold - 1 if (end > 0 && isHighSurrogate(formatted[end - 1])) { end = end - 1 } return `'${formatted.slice(1, end)}…'` } if ( type === '[object Array]' || type === '[object Object]' || type === '[object Set]' || type === '[object Map]' ) { return stringifyByMaxWidth(obj, threshold, { ...prettyFormatOptions, ...stringifyOptions, maxDepth: 1, }) } return stringify(obj, undefined, { ...prettyFormatOptions, ...stringifyOptions, maxDepth: 0, }) } export function truncateString(string: string, maxLength: number): string { if (string.length <= maxLength) { return string } let end = maxLength - 1 if (isHighSurrogate(string[end - 1])) { end = end - 1 } return `${string.slice(0, end)}…` } function stringifyByMaxWidth( object: unknown, threshold: number, options: StringifyOptions, ): string { function evaluate(x: number) { return stringify(object, undefined, { ...options, maxWidth: x, }) } const opt = binarySearch(0, threshold, (x) => evaluate(x).length <= threshold) return evaluate(opt) } // find max(x \in [x, y) | f(x) = true) // if f(x0) is false, then returns x0. function binarySearch(x0: number, x1: number, f: (x: number) => boolean): number { while (x0 + 1 < x1) { const x = Math.floor((x0 + x1) / 2) if (f(x)) { x0 = x } else { x1 = x } } return x0 } // https://github.com/chaijs/loupe/pull/79 function isHighSurrogate(char: string): boolean { return char >= '\uD800' && char <= '\uDBFF' }