diff --git a/docs/api/test.md b/docs/api/test.md
index 4dda739ea..18af801f2 100644
--- a/docs/api/test.md
+++ b/docs/api/test.md
@@ -618,7 +618,7 @@ test.each`
```
::: tip
-Vitest processes `$values` with Chai `format` method. If the value is too truncated, you can increase [chaiConfig.truncateThreshold](/config/chaiconfig#chaiconfig-truncatethreshold) in your config file.
+Vitest formats interpolated title values with its display formatter. If the value is too truncated, you can increase [taskTitleValueFormatTruncate](/config/tasktitlevalueformattruncate) in your config file.
:::
## test.for
diff --git a/docs/config/chaiconfig.md b/docs/config/chaiconfig.md
index 24cf085af..5a6f9b55e 100644
--- a/docs/config/chaiconfig.md
+++ b/docs/config/chaiconfig.md
@@ -29,6 +29,4 @@ Influences whether or not the `showDiff` flag should be included in the thrown A
- **Type:** `number`
- **Default:** `40`
-Sets length threshold for actual and expected values in assertion errors. If this threshold is exceeded, for example for large data structures, the value is replaced with something like `[ Array(3) ]` or `{ Object (prop1, prop2) }`. Set it to `0` if you want to disable truncating altogether.
-
-This config option affects truncating values in `test.each` titles and inside the assertion error message.
+Sets length threshold for actual and expected values in assertion error messages. If this threshold is exceeded, for example for large data structures, the value is replaced with something like `[ Array(3) ]` or `{ Object (prop1, prop2) }`. Set it to `0` if you want to disable truncating altogether.
diff --git a/docs/config/tasktitlevalueformattruncate.md b/docs/config/tasktitlevalueformattruncate.md
new file mode 100644
index 000000000..5bb7eb885
--- /dev/null
+++ b/docs/config/tasktitlevalueformattruncate.md
@@ -0,0 +1,15 @@
+---
+title: taskTitleValueFormatTruncate | Config
+outline: deep
+---
+
+# taskTitleValueFormatTruncate {#tasktitlevalueformattruncate}
+
+- **Type** `number`
+- **Default:** `40`
+
+Sets the length limit for formatted values interpolated into generated task titles.
+
+This affects values inserted by APIs like `test.each` and `test.for`, including both `$value` and `%` placeholder formatting.
+
+Set it to `0` to disable truncation.
diff --git a/packages/browser/src/client/tester/logger.ts b/packages/browser/src/client/tester/logger.ts
index 6d497cfc0..67601c7e9 100644
--- a/packages/browser/src/client/tester/logger.ts
+++ b/packages/browser/src/client/tester/logger.ts
@@ -1,4 +1,4 @@
-import { browserFormat } from 'vitest/internal/browser'
+import { format } from 'vitest/internal/browser'
import { getConfig } from '../utils'
import { rpc } from './rpc'
import { getBrowserRunner } from './runner'
@@ -30,7 +30,7 @@ export function setupConsoleLogSpy(): void {
console.dir = (item, options) => {
dir(item, options)
- sendLog('stdout', browserFormat(item))
+ sendLog('stdout', processLog([item]))
}
console.dirxml = (...args) => {
@@ -114,7 +114,7 @@ function stderr(base: (...args: unknown[]) => void) {
}
function processLog(args: unknown[]) {
- return browserFormat(...args)
+ return format(args, { multiline: true })
}
function sendLog(
diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts
index c0eab9773..1482b413f 100644
--- a/packages/expect/src/jest-expect.ts
+++ b/packages/expect/src/jest-expect.ts
@@ -4,6 +4,7 @@ import type { Constructable } from '@vitest/utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
import type { Assertion, ChaiPlugin } from './types'
import { isMockFunction } from '@vitest/spy'
+import { inspect } from '@vitest/utils/display'
import { assertTypes, ordinal } from '@vitest/utils/helpers'
import c from 'tinyrainbow'
import { JEST_MATCHERS_OBJECT } from './constants'
@@ -483,7 +484,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
&& (args.length === 1 || jestEquals(expected, value, customTesters))
const valueString
- = args.length === 1 ? '' : ` with value ${utils.objDisplay(expected)}`
+ = args.length === 1 ? '' : ` with value ${inspect(expected, { truncate: 40 })}`
return this.assert(
pass,
diff --git a/packages/pretty-format/USAGE.md b/packages/pretty-format/USAGE.md
index 64e18a847..6db43f871 100644
--- a/packages/pretty-format/USAGE.md
+++ b/packages/pretty-format/USAGE.md
@@ -51,11 +51,16 @@ Object {
| `printBasicPrototype` | `boolean` | `true` | Print `Object` and `Array` prefixes for plain objects and arrays |
| `printFunctionName` | `boolean` | `true` | Include or omit the function name |
| `printShadowRoot` | `boolean` | `true` | Include shadow-root contents when formatting DOM nodes |
+| `quoteKeys` | `boolean` | `true` | Always quote object property keys |
+| `singleQuote` | `boolean` | `false` | Print strings using single quotes instead of double quotes |
+| `spacingInner` | `string` | `\n` | Whitespace after commas between items or entries |
+| `spacingOuter` | `string` | `\n` | Whitespace just inside `[]` / `{}` delimiters |
Important:
- `plugins: []` means the package does not auto-enable its built-in plugins by default
- Vitest features opt into their own plugin stacks and option presets
+- `min: true` also changes the defaults of other options to `spacingInner: ' '`, `spacingOuter: ''`, and `printBasicPrototype: false`
## Built-in Plugins
diff --git a/packages/pretty-format/src/collections.ts b/packages/pretty-format/src/collections.ts
index b17e17fe8..44595ea80 100644
--- a/packages/pretty-format/src/collections.ts
+++ b/packages/pretty-format/src/collections.ts
@@ -21,7 +21,7 @@ function getKeysOfEnumerableProperties(object: Record, compareK
}
}
- return keys as Array
+ return keys
}
/**
@@ -37,6 +37,7 @@ export function printIteratorEntries(
refs: Refs,
printer: Printer,
separator = ': ',
+ length?: number,
): string {
let result = ''
let width = 0
@@ -51,7 +52,7 @@ export function printIteratorEntries(
result += indentationNext
if (width++ === config.maxWidth) {
- result += '…'
+ result += typeof length === 'number' ? `…(${length - width + 1})` : '…'
break
}
@@ -100,6 +101,7 @@ export function printIteratorValues(
depth: number,
refs: Refs,
printer: Printer,
+ length?: number,
): string {
let result = ''
let width = 0
@@ -114,7 +116,7 @@ export function printIteratorValues(
result += indentationNext
if (width++ === config.maxWidth) {
- result += '…'
+ result += typeof length === 'number' ? `…(${length - width + 1})` : '…'
break
}
@@ -163,7 +165,7 @@ export function printListItems(
result += indentationNext
if (i === config.maxWidth) {
- result += '…'
+ result += `…(${length - i})`
break
}
@@ -197,15 +199,16 @@ export function printListItems(
* without surrounding punctuation (for example, braces)
*/
export function printObjectProperties(
- val: Record,
+ val: Record,
config: Config,
indentation: string,
depth: number,
refs: Refs,
printer: Printer,
+ compareKeysOverride: CompareKeys = config.compareKeys,
): string {
let result = ''
- const keys = getKeysOfEnumerableProperties(val, config.compareKeys)
+ const keys = getKeysOfEnumerableProperties(val, compareKeysOverride)
if (keys.length > 0) {
result += config.spacingOuter
@@ -213,11 +216,20 @@ export function printObjectProperties(
const indentationNext = indentation + config.indent
for (let i = 0; i < keys.length; i++) {
+ result += indentationNext
+
+ if (i === config.maxWidth) {
+ result += `…(${keys.length - i})`
+ break
+ }
+
const key = keys[i]
- const name = printer(key, config, indentationNext, depth, refs)
+ const name = !config.quoteKeys && isUnquotableKey(key)
+ ? key as string
+ : printer(key, config, indentationNext, depth, refs)
const value = printer(val[key], config, indentationNext, depth, refs)
- result += `${indentationNext + name}: ${value}`
+ result += `${name}: ${value}`
if (i < keys.length - 1) {
result += `,${config.spacingInner}`
@@ -232,3 +244,11 @@ export function printObjectProperties(
return result
}
+
+// https://github.com/nodejs/node/blob/61102cdbb3d59155ad5bb4fc9419627a31e63f7a/lib/internal/util/inspect.js#L249
+// /^[a-zA-Z_][a-zA-Z_0-9]*$/
+const keyStrRegExp = /^[a-z_]\w*$/i
+
+function isUnquotableKey(key: string | symbol): boolean {
+ return typeof key === 'string' && key !== '__proto__' && keyStrRegExp.test(key)
+}
diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts
index 542cf0630..caf37d978 100644
--- a/packages/pretty-format/src/index.ts
+++ b/packages/pretty-format/src/index.ts
@@ -111,6 +111,7 @@ function printBasicValue(
printFunctionName: boolean,
escapeRegex: boolean,
escapeString: boolean,
+ singleQuote: boolean,
): string | null {
if (val === true || val === false) {
return `${val}`
@@ -131,10 +132,15 @@ function printBasicValue(
return printBigInt(val)
}
if (typeOf === 'string') {
+ const q = singleQuote ? '\'' : '"'
if (escapeString) {
- return `"${val.replaceAll(/"|\\/g, '\\$&')}"`
+ // escape quote in each case, e.g.
+ // it's me -> 'it\'s me'
+ // say "hi" -> "say \"hi\""
+ const escapePattern = singleQuote ? /['\\]/g : /["\\]/g
+ return `${q}${val.replaceAll(escapePattern, '\\$&')}${q}`
}
- return `"${val}"`
+ return `${q}${val}${q}`
}
if (typeOf === 'function') {
return printFunction(val, printFunctionName)
@@ -229,11 +235,9 @@ function printComplexValue(
return hitMaxDepth
? `[${val.constructor.name}]`
: `${
- min
+ !config.printBasicPrototype && val.constructor.name === 'Array'
? ''
- : !config.printBasicPrototype && val.constructor.name === 'Array'
- ? ''
- : `${val.constructor.name} `
+ : `${val.constructor.name} `
}[${printListItems(val, config, indentation, depth, refs, printer)}]`
}
if (toStringed === '[object Map]') {
@@ -247,6 +251,7 @@ function printComplexValue(
refs,
printer,
' => ',
+ val.size,
)}}`
}
if (toStringed === '[object Set]') {
@@ -259,6 +264,7 @@ function printComplexValue(
depth,
refs,
printer,
+ val.size,
)}}`
}
@@ -267,11 +273,9 @@ function printComplexValue(
return hitMaxDepth || isWindow(val)
? `[${getConstructorName(val)}]`
: `${
- min
+ !config.printBasicPrototype && getConstructorName(val) === 'Object'
? ''
- : !config.printBasicPrototype && getConstructorName(val) === 'Object'
- ? ''
- : `${getConstructorName(val)} `
+ : `${getConstructorName(val)} `
}{${printObjectProperties(
val,
config,
@@ -300,13 +304,14 @@ const ErrorPlugin: NewPlugin = {
const name = val.name !== 'Error' ? val.name : getConstructorName(val as any)
return hitMaxDepth
? `[${name}]`
- : `${name} {${printIteratorEntries(
- Object.entries(entries).values(),
+ : `${name} {${printObjectProperties(
+ entries,
config,
indentation,
depth,
refs,
printer,
+ null,
)}}`
},
}
@@ -392,6 +397,7 @@ function printer(
config.printFunctionName,
config.escapeRegex,
config.escapeString,
+ config.singleQuote,
)
if (basicResult !== null) {
result = basicResult
@@ -453,6 +459,10 @@ export const DEFAULT_OPTIONS: Options = {
printFunctionName: true,
printShadowRoot: true,
theme: DEFAULT_THEME,
+ singleQuote: false,
+ quoteKeys: true,
+ spacingInner: '\n',
+ spacingOuter: '\n',
} satisfies Options
function validateOptions(options: OptionsReceived) {
@@ -525,11 +535,13 @@ function getConfig(options?: OptionsReceived): Config {
maxWidth: options?.maxWidth ?? DEFAULT_OPTIONS.maxWidth,
min: options?.min ?? DEFAULT_OPTIONS.min,
plugins: options?.plugins ?? DEFAULT_OPTIONS.plugins,
- printBasicPrototype: options?.printBasicPrototype ?? true,
+ printBasicPrototype: options?.printBasicPrototype ?? !options?.min,
printFunctionName: getPrintFunctionName(options),
printShadowRoot: options?.printShadowRoot ?? true,
- spacingInner: options?.min ? ' ' : '\n',
- spacingOuter: options?.min ? '' : '\n',
+ spacingInner: options?.spacingInner ?? (options?.min ? ' ' : '\n'),
+ spacingOuter: options?.spacingOuter ?? (options?.min ? '' : '\n'),
+ singleQuote: options?.singleQuote ?? DEFAULT_OPTIONS.singleQuote,
+ quoteKeys: options?.quoteKeys ?? DEFAULT_OPTIONS.quoteKeys,
maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength,
_outputLengthPerDepth: [],
}
@@ -547,25 +559,26 @@ function createIndent(indent: number): string {
export function format(val: unknown, options?: OptionsReceived): string {
if (options) {
validateOptions(options)
- if (options.plugins) {
- const plugin = findPlugin(options.plugins, val)
- if (plugin !== null) {
- return printPlugin(plugin, val, getConfig(options), '', 0, [])
- }
- }
+ }
+
+ const config = getConfig(options)
+ const plugin = findPlugin(config.plugins, val)
+ if (plugin !== null) {
+ return printPlugin(plugin, val, config, '', 0, [])
}
const basicResult = printBasicValue(
val,
- getPrintFunctionName(options),
- getEscapeRegex(options),
- getEscapeString(options),
+ config.printFunctionName,
+ config.escapeRegex,
+ config.escapeString,
+ config.singleQuote,
)
if (basicResult !== null) {
return basicResult
}
- return printComplexValue(val, getConfig(options), '', 0, [])
+ return printComplexValue(val, config, '', 0, [])
}
export type {
diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts
index dfaea1c85..59dc06e3f 100644
--- a/packages/pretty-format/src/types.ts
+++ b/packages/pretty-format/src/types.ts
@@ -84,12 +84,19 @@ export interface PrettyFormatOptions {
maxOutputLength?: number
/**
* Whether to minimize added whitespace, including indentation and line breaks.
+ *
+ * When `true`, pretty-format defaults `spacingInner` to `' '`, `spacingOuter` to `''`,
+ * and ignores indentation. It also changes the default for `printBasicPrototype`
+ * from `true` to `false`, although an explicit `printBasicPrototype` still wins.
+ * Explicit `spacingInner` / `spacingOuter` overrides still apply.
* @default false
*/
min?: boolean
/**
* Whether to print `Object` / `Array` prefixes for plain objects and arrays.
- * @default true
+ *
+ * Defaults to `true`, unless `min` is `true`, in which case it defaults to `false`.
+ * An explicit `printBasicPrototype` value always overrides the `min` default.
*/
printBasicPrototype?: boolean
/**
@@ -111,6 +118,50 @@ export interface PrettyFormatOptions {
* @default []
*/
plugins?: Plugins
+ /**
+ * Whitespace inserted after commas between items or entries.
+ *
+ * For example, in `{a: 1, b: 2}` or `[1, 2]`, this controls the gap after each comma:
+ * `{a: 1,${spacingInner}b: 2}`
+ * `[1,${spacingInner}2]`
+ *
+ * Defaults to `'\n'` in regular mode and `' '` when `min` is `true`.
+ * Can be overridden independently of `min`.
+ */
+ spacingInner?: string
+ /**
+ * Whitespace inserted immediately inside collection/object delimiters.
+ *
+ * For example, this controls the space or newline right after the opening delimiter
+ * and right before the closing delimiter:
+ * `{${spacingOuter}a: 1${spacingOuter}}`
+ * `[${spacingOuter}1${spacingOuter}]`
+ *
+ * Defaults to `'\n'` in regular mode and `''` when `min` is `true`.
+ * Can be overridden independently of `min`.
+ */
+ spacingOuter?: string
+ /**
+ * Whether to print strings using single quotes instead of double quotes.
+ *
+ * For example:
+ * `"hello"` when `false`
+ * `'hello'` when `true`
+ *
+ * @default false
+ */
+ singleQuote?: boolean
+ /**
+ * Whether to always quote object property keys.
+ *
+ * For example:
+ * `{"a": 1}` when `true`
+ * `{a: 1}` when `false` and the key is a valid identifier
+ * `{"my-key": 1}` still stays quoted because it is not a valid identifier
+ *
+ * @default true
+ */
+ quoteKeys?: boolean
}
export type OptionsReceived = PrettyFormatOptions
@@ -131,6 +182,8 @@ export interface Config {
printShadowRoot: boolean
spacingInner: string
spacingOuter: string
+ singleQuote: boolean
+ quoteKeys: boolean
maxOutputLength: number
/**
* Per-depth budget accumulator for {@link maxOutputLength}.
diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts
index e5caa1e75..9e737d17d 100644
--- a/packages/runner/src/suite.ts
+++ b/packages/runner/src/suite.ts
@@ -1,3 +1,4 @@
+import type { InspectOptions } from '@vitest/utils/display'
import type { UserFixtures } from './fixture'
import type { VitestRunner } from './types/runner'
import type {
@@ -18,7 +19,7 @@ import type {
TestFunction,
TestOptions,
} from './types/tasks'
-import { format, formatRegExp, objDisplay } from '@vitest/utils/display'
+import { format, formatRegExp, inspect } from '@vitest/utils/display'
import {
isNegativeNaN,
isObject,
@@ -1019,6 +1020,10 @@ function formatTitle(template: string, items: any[], idx: number) {
})
}
+ const inspectOptions: InspectOptions = {
+ truncate: runner.config.taskTitleValueFormatTruncate,
+ }
+
const isObjectItem = isObject(items[0])
function formatAttribute(s: string) {
return s.replace(/\$([$\w.]+)/g, (_, key: string) => {
@@ -1028,9 +1033,7 @@ function formatTitle(template: string, items: any[], idx: number) {
}
const arrayElement = isArrayKey ? objectAttr(items, key) : undefined
const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement
- return objDisplay(value, {
- truncate: runner?.config?.chaiConfig?.truncateThreshold,
- })
+ return inspect(value, inspectOptions)
})
}
@@ -1042,7 +1045,7 @@ function formatTitle(template: string, items: any[], idx: number) {
// format "%"
(match) => {
if (i < count) {
- output += format(match[0], items[i++])
+ output += format([match[0], items[i++]], inspectOptions)
}
else {
output += match[0]
diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts
index d05ab8056..1a0be58f7 100644
--- a/packages/runner/src/types/runner.ts
+++ b/packages/runner/src/types/runner.ts
@@ -36,6 +36,7 @@ export interface VitestRunnerConfig {
chaiConfig: {
truncateThreshold?: number
} | undefined
+ taskTitleValueFormatTruncate: number | undefined
maxConcurrency: number
testTimeout: number
hookTimeout: number
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 7d78661fb..47423be00 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -96,7 +96,6 @@
"@jridgewell/trace-mapping": "catalog:",
"@types/convert-source-map": "^2.0.3",
"@types/estree": "catalog:",
- "diff-sequences": "^29.6.3",
- "loupe": "^3.2.1"
+ "diff-sequences": "^29.6.3"
}
}
diff --git a/packages/utils/src/display.ts b/packages/utils/src/display.ts
index 574da5667..2d2b65c2c 100644
--- a/packages/utils/src/display.ts
+++ b/packages/utils/src/display.ts
@@ -4,24 +4,6 @@ import {
format as prettyFormat,
plugins as prettyFormatPlugins,
} from '@vitest/pretty-format'
-import * as loupe from 'loupe'
-
-type Inspect = (value: unknown, options: Options) => string
-interface Options {
- showHidden: boolean
- depth: number
- colors: boolean
- customInspect: boolean
- showProxy: boolean
- maxArrayLength: number
- breakLength: number
- truncate: number
- seen: unknown[]
- inspect: Inspect
- stylize: (value: string, styleType: string) => string
-}
-
-export type LoupeOptions = Partial
const {
AsymmetricMatcher,
@@ -122,25 +104,13 @@ function createNodeFilterFromSelector(selector: string): (node: any) => boolean
export const formatRegExp: RegExp = /%[sdjifoOc%]/g
-interface FormatOptions {
- prettifyObject?: boolean
-}
-
-function baseFormat(args: unknown[], options: FormatOptions = {}): string {
- const formatArg = (item: unknown, inspecOptions?: LoupeOptions) => {
- if (options.prettifyObject) {
- return stringify(item, undefined, {
- printBasicPrototype: false,
- escapeString: false,
- })
- }
- return inspect(item, inspecOptions)
- }
+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], { depth: 0, colors: false }))
+ objects.push(formatArg(args[i]))
}
return objects.join(' ')
}
@@ -168,7 +138,7 @@ function baseFormat(args: unknown[], options: FormatOptions = {}): string {
if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) {
return value.toString()
}
- return formatArg(value, { depth: 0, colors: false })
+ return formatArg(value)
}
return String(value)
}
@@ -192,7 +162,6 @@ function baseFormat(args: unknown[], options: FormatOptions = {}): string {
case '%f':
return Number.parseFloat(String(args[i++])).toString()
case '%o':
- return formatArg(args[i++], { showHidden: true, showProxy: true })
case '%O':
return formatArg(args[i++])
case '%c': {
@@ -233,47 +202,100 @@ function baseFormat(args: unknown[], options: FormatOptions = {}): string {
return str
}
-export function format(...args: unknown[]): string {
- return baseFormat(args)
+export interface InspectOptions extends StringifyOptions {
+ truncate?: number
+ multiline?: boolean
}
-export function browserFormat(...args: unknown[]): string {
- return baseFormat(args, { prettifyObject: true })
-}
+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,
+ })
-export function inspect(obj: unknown, options: LoupeOptions = {}): string {
- if (options.truncate === 0) {
- options.truncate = Number.POSITIVE_INFINITY
+ if (threshold === 0 || formatted.length <= threshold) {
+ return formatted
}
- return loupe.inspect(obj, options)
+
+ // 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 objDisplay(obj: unknown, options: LoupeOptions = {}): string {
- if (typeof options.truncate === 'undefined') {
- options.truncate = 40
+function stringifyByMaxWidth(object: unknown, threshold: number, options: StringifyOptions): string {
+ function evaluate(x: number) {
+ return stringify(object, undefined, {
+ ...options,
+ maxWidth: x,
+ })
}
- const str = inspect(obj, options)
- const type = Object.prototype.toString.call(obj)
+ const opt = binarySearch(
+ 0,
+ threshold,
+ x => evaluate(x).length <= threshold,
+ )
+ return evaluate(opt)
+}
- if (options.truncate && str.length >= options.truncate) {
- if (type === '[object Function]') {
- const fn = obj as () => void
- return !fn.name ? '[Function]' : `[Function: ${fn.name}]`
- }
- else if (type === '[object Array]') {
- return `[ Array(${(obj as []).length}) ]`
- }
- else if (type === '[object Object]') {
- const keys = Object.keys(obj as object)
- const kstr
- = keys.length > 2
- ? `${keys.splice(0, 2).join(', ')}, ...`
- : keys.join(', ')
- return `{ Object (${kstr}) }`
+// 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 {
- return str
+ x1 = x
}
}
- return str
+ return x0
+}
+
+// https://github.com/chaijs/loupe/pull/79
+function isHighSurrogate(char: string): boolean {
+ return char >= '\uD800' && char <= '\uDBFF'
}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index a9c0a549f..ec8ffc24a 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -1,4 +1,4 @@
-export type { LoupeOptions, StringifyOptions } from './display'
+export type { StringifyOptions } from './display'
export type { DeferPromise } from './helpers'
export type { SafeTimers } from './timers'
export type {
diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts
index 4f06742b3..7471d73cc 100644
--- a/packages/vitest/src/defaults.ts
+++ b/packages/vitest/src/defaults.ts
@@ -96,6 +96,7 @@ export const configDefaults: Readonly<{
exclude: string[]
}
slowTestThreshold: number
+ taskTitleValueFormatTruncate: number
disableConsoleIntercept: boolean
detectAsyncLeaks: boolean
}> = Object.freeze({
@@ -134,6 +135,7 @@ export const configDefaults: Readonly<{
exclude: defaultExclude,
},
slowTestThreshold: 300,
+ taskTitleValueFormatTruncate: 40,
disableConsoleIntercept: false,
detectAsyncLeaks: false,
})
diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts
index daa83ffdc..e30ec47fe 100644
--- a/packages/vitest/src/node/cli/cli-config.ts
+++ b/packages/vitest/src/node/cli/cli-config.ts
@@ -963,6 +963,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
projects: null,
watchTriggerPatterns: null,
tags: null,
+ taskTitleValueFormatTruncate: null,
}
export const benchCliOptionsConfig: Pick<
diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts
index c49676612..5943e738a 100644
--- a/packages/vitest/src/node/config/serializeConfig.ts
+++ b/packages/vitest/src/node/config/serializeConfig.ts
@@ -21,6 +21,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
bail: config.bail,
defines: config.defines,
chaiConfig: config.chaiConfig,
+ taskTitleValueFormatTruncate: config.taskTitleValueFormatTruncate,
setupFiles: config.setupFiles,
allowOnly: config.allowOnly,
testTimeout: config.testTimeout,
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index 7fad9efd8..f61409396 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -800,6 +800,15 @@ export interface InlineConfig {
*/
chaiConfig?: ChaiConfig
+ /**
+ * Sets length limit for formatted values interpolated into generated task titles.
+ *
+ * This affects values inserted by APIs like `test.each` and `test.for`.
+ *
+ * @default 40
+ */
+ taskTitleValueFormatTruncate?: number
+
/**
* Stop test execution when given number of tests have failed.
*/
diff --git a/packages/vitest/src/public/browser.ts b/packages/vitest/src/public/browser.ts
index c0e5316e2..3405142e6 100644
--- a/packages/vitest/src/public/browser.ts
+++ b/packages/vitest/src/public/browser.ts
@@ -11,9 +11,8 @@ export {
export { type OTELCarrier, Traces } from '../utils/traces'
export { collectTests, startTests } from '@vitest/runner'
export * as SpyModule from '@vitest/spy'
-export type { LoupeOptions, ParsedStack, StringifyOptions } from '@vitest/utils'
+export type { ParsedStack, StringifyOptions } from '@vitest/utils'
export {
- browserFormat,
format,
inspect,
stringify,
diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts
index 27bdb2b75..0cf9ffd23 100644
--- a/packages/vitest/src/runtime/config.ts
+++ b/packages/vitest/src/runtime/config.ts
@@ -78,6 +78,7 @@ export interface SerializedConfig {
showDiff?: boolean
truncateThreshold?: number
} | undefined
+ taskTitleValueFormatTruncate: number | undefined
api: {
allowExec: boolean | undefined
allowWrite: boolean | undefined
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4d2c401c4..bd2dc3dbb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,6 +93,9 @@ catalogs:
istanbul-reports:
specifier: ^3.2.0
version: 3.2.0
+ loupe:
+ specifier: ^3.2.1
+ version: 3.2.1
magic-string:
specifier: ^0.30.21
version: 0.30.21
@@ -1005,9 +1008,6 @@ importers:
diff-sequences:
specifier: ^29.6.3
version: 29.6.3
- loupe:
- specifier: ^3.2.1
- version: 3.2.1
packages/vitest:
dependencies:
@@ -1515,6 +1515,9 @@ importers:
immutable:
specifier: 5.1.5
version: 5.1.5
+ loupe:
+ specifier: 'catalog:'
+ version: 3.2.1
memfs:
specifier: ^4.56.11
version: 4.56.11(tslib@2.8.1)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 24d4e6d10..8b8a03cb0 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -74,6 +74,7 @@ catalog:
istanbul-lib-report: ^3.0.1
istanbul-lib-source-maps: ^5.0.6
istanbul-reports: ^3.2.0
+ loupe: ^3.2.1
magic-string: ^0.30.21
magicast: ^0.5.2
msw: ^2.12.10
diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts
index 5808d9e98..c37a79258 100644
--- a/test/browser/specs/runner.test.ts
+++ b/test/browser/specs/runner.test.ts
@@ -139,12 +139,12 @@ describe('console logging tests', async () => {
expect(stdout).toContain('hello from console.debug')
expect(stdout).toContain(`
{
- "hello": "from dir",
+ hello: 'from dir',
}
`.trim())
expect(stdout).toContain(`
{
- "hello": "from dirxml",
+ hello: 'from dirxml',
}
`.trim())
expect(stdout).toContain('dom ')
@@ -160,7 +160,7 @@ describe('console logging tests', async () => {
expect(stdout).not.toContain('[console-time-fake]: 0 ms')
expect(stdout).toContain('hello from one')
expect(stdout).toContain(`hello from two {
- "hello": "object",
+ hello: 'object',
}`)
})
diff --git a/test/browser/test/utils.test.ts b/test/browser/test/utils.test.ts
index f0ca2a356..b4099511d 100644
--- a/test/browser/test/utils.test.ts
+++ b/test/browser/test/utils.test.ts
@@ -11,7 +11,7 @@ beforeEach(() => {
utils.configurePrettyDOM({})
})
-it('utils package correctly uses loupe', async () => {
+it('utils package correctly uses inspect', async () => {
expect(inspect({ test: 1 })).toBe('{ test: 1 }')
})
diff --git a/test/cli/fixtures/reporters/test-for-title-truncate.test.ts b/test/cli/fixtures/reporters/test-for-title-truncate.test.ts
new file mode 100644
index 000000000..3f355e181
--- /dev/null
+++ b/test/cli/fixtures/reporters/test-for-title-truncate.test.ts
@@ -0,0 +1,40 @@
+import { test } from 'vitest'
+
+test.for`
+ length | param
+ ${30} | ${'0123456789'.repeat(3)}
+ ${40} | ${'0123456789'.repeat(4)}
+ ${50} | ${'0123456789'.repeat(5)}
+`('$ (string: $length) $param', () => {});
+
+test.for`
+ length | param
+ ${3} | ${['one', 'two', 'three']}
+ ${4} | ${['one', 'two', 'three', 'four']}
+ ${5} | ${['one', 'two', 'three', 'four', 'five']}
+`('$ (array: $length) $param', () => {});
+
+test.for`
+ length | param
+ ${3} | ${{ one: 1, two: 2, three: 3 }}
+ ${4} | ${{ one: 1, two: 2, three: 3, four: 4 }}
+ ${5} | ${{ one: 1, two: 2, three: 3, four: 4, five: 5 }}
+`('$ (object: $length) $param', () => {});
+
+test.for([
+ [30, '0123456789'.repeat(3)],
+ [40, '0123456789'.repeat(4)],
+ [50, '0123456789'.repeat(5)],
+])(`% (string: %d) %o`, () => {})
+
+test.for([
+ [3, ['one', 'two', 'three']],
+ [4, ['one', 'two', 'three', 'four']],
+ [5, ['one', 'two', 'three', 'four', 'five']],
+])(`% (array: %d) %o`, () => {})
+
+test.for([
+ [3, { one: 1, two: 2, three: 3 }],
+ [4, { one: 1, two: 2, three: 3, four: 4 }],
+ [5, { one: 1, two: 2, three: 3, four: 4, five: 5 }],
+])(`% (object: %d) %o`, () => {})
diff --git a/test/cli/test/reporters/default.test.ts b/test/cli/test/reporters/default.test.ts
index 8fa8c1088..9037b097e 100644
--- a/test/cli/test/reporters/default.test.ts
+++ b/test/cli/test/reporters/default.test.ts
@@ -267,6 +267,100 @@ describe('default reporter', async () => {
`)
})
+ test('test.each/for title truncate', async () => {
+ // default (40)
+ let result = await runVitest({
+ include: ['fixtures/reporters/test-for-title-truncate.test.ts'],
+ config: false,
+ })
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ {
+ "fixtures/reporters/test-for-title-truncate.test.ts": {
+ "$ (array: 3) [ 'one', 'two', 'three' ]": "passed",
+ "$ (array: 4) [ 'one', 'two', 'three', 'four' ]": "passed",
+ "$ (array: 5) [ 'one', 'two', 'three', 'four', …(1) ]": "passed",
+ "$ (object: 3) { one: 1, two: 2, three: 3 }": "passed",
+ "$ (object: 4) { one: 1, two: 2, three: 3, four: 4 }": "passed",
+ "$ (object: 5) { one: 1, two: 2, three: 3, …(2) }": "passed",
+ "$ (string: 30) '012345678901234567890123456789'": "passed",
+ "$ (string: 40) '01234567890123456789012345678901234567…'": "passed",
+ "$ (string: 50) '01234567890123456789012345678901234567…'": "passed",
+ "% (array: 3) [ 'one', 'two', 'three' ]": "passed",
+ "% (array: 4) [ 'one', 'two', 'three', 'four' ]": "passed",
+ "% (array: 5) [ 'one', 'two', 'three', 'four', …(1) ]": "passed",
+ "% (object: 3) { one: 1, two: 2, three: 3 }": "passed",
+ "% (object: 4) { one: 1, two: 2, three: 3, four: 4 }": "passed",
+ "% (object: 5) { one: 1, two: 2, three: 3, …(2) }": "passed",
+ "% (string: 30) '012345678901234567890123456789'": "passed",
+ "% (string: 40) '01234567890123456789012345678901234567…'": "passed",
+ "% (string: 50) '01234567890123456789012345678901234567…'": "passed",
+ },
+ }
+ `)
+
+ // 20
+ result = await runVitest({
+ include: ['fixtures/reporters/test-for-title-truncate.test.ts'],
+ config: false,
+ taskTitleValueFormatTruncate: 20,
+ })
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ {
+ "fixtures/reporters/test-for-title-truncate.test.ts": {
+ "$ (array: 3) [ 'one', …(2) ]": "passed",
+ "$ (array: 4) [ 'one', …(3) ]": "passed",
+ "$ (array: 5) [ 'one', …(4) ]": "passed",
+ "$ (object: 3) { one: 1, …(2) }": "passed",
+ "$ (object: 4) { one: 1, …(3) }": "passed",
+ "$ (object: 5) { one: 1, …(4) }": "passed",
+ "$ (string: 30) '012345678901234567…'": "passed",
+ "$ (string: 40) '012345678901234567…'": "passed",
+ "$ (string: 50) '012345678901234567…'": "passed",
+ "% (array: 3) [ 'one', …(2) ]": "passed",
+ "% (array: 4) [ 'one', …(3) ]": "passed",
+ "% (array: 5) [ 'one', …(4) ]": "passed",
+ "% (object: 3) { one: 1, …(2) }": "passed",
+ "% (object: 4) { one: 1, …(3) }": "passed",
+ "% (object: 5) { one: 1, …(4) }": "passed",
+ "% (string: 30) '012345678901234567…'": "passed",
+ "% (string: 40) '012345678901234567…'": "passed",
+ "% (string: 50) '012345678901234567…'": "passed",
+ },
+ }
+ `)
+
+ // no truncate
+ result = await runVitest({
+ include: ['fixtures/reporters/test-for-title-truncate.test.ts'],
+ config: false,
+ taskTitleValueFormatTruncate: 0,
+ })
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ {
+ "fixtures/reporters/test-for-title-truncate.test.ts": {
+ "$ (array: 3) [ 'one', 'two', 'three' ]": "passed",
+ "$ (array: 4) [ 'one', 'two', 'three', 'four' ]": "passed",
+ "$ (array: 5) [ 'one', 'two', 'three', 'four', 'five' ]": "passed",
+ "$ (object: 3) { one: 1, two: 2, three: 3 }": "passed",
+ "$ (object: 4) { one: 1, two: 2, three: 3, four: 4 }": "passed",
+ "$ (object: 5) { one: 1, two: 2, three: 3, four: 4, five: 5 }": "passed",
+ "$ (string: 30) '012345678901234567890123456789'": "passed",
+ "$ (string: 40) '0123456789012345678901234567890123456789'": "passed",
+ "$ (string: 50) '01234567890123456789012345678901234567890123456789'": "passed",
+ "% (array: 3) [ 'one', 'two', 'three' ]": "passed",
+ "% (array: 4) [ 'one', 'two', 'three', 'four' ]": "passed",
+ "% (array: 5) [ 'one', 'two', 'three', 'four', 'five' ]": "passed",
+ "% (object: 3) { one: 1, two: 2, three: 3 }": "passed",
+ "% (object: 4) { one: 1, two: 2, three: 3, four: 4 }": "passed",
+ "% (object: 5) { one: 1, two: 2, three: 3, four: 4, five: 5 }": "passed",
+ "% (string: 30) '012345678901234567890123456789'": "passed",
+ "% (string: 40) '0123456789012345678901234567890123456789'": "passed",
+ "% (string: 50) '01234567890123456789012345678901234567890123456789'": "passed",
+ },
+ }
+ `)
+ })
+
test('project name color', async () => {
const { stdout } = await runVitestCli(
{ preserveAnsi: true },
diff --git a/test/config/fixtures/chai-config/test-each-title.test.ts b/test/config/fixtures/chai-config/test-each-title.test.ts
deleted file mode 100644
index 42750f67c..000000000
--- a/test/config/fixtures/chai-config/test-each-title.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect, test } from 'vitest'
-
-test.each`
- length | param
- ${30} | ${'0123456789'.repeat(3)}
- ${40} | ${'0123456789'.repeat(4)}
- ${50} | ${'0123456789'.repeat(5)}
-`('$param (length = $length)', () => {});
-
-test.each`
- param
- ${['one', 'two', 'three']}
- ${['one', 'two', 'three', 'four']}
- ${['one', 'two', 'three', 'four', 'five']}
-`('$param', () => {});
-
-test.each`
- param
- ${{ one: 1, two: 2, three: 3 }}
- ${{ one: 1, two: 2, three: 3, four: 4 }}
- ${{ one: 1, two: 2, three: 3, four: 4, five: 5 }}
-`('$param', () => {});
diff --git a/test/config/fixtures/chai-config/vitest.config.ts b/test/config/fixtures/chai-config/vitest.config.ts
deleted file mode 100644
index abed6b211..000000000
--- a/test/config/fixtures/chai-config/vitest.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { defineConfig } from 'vitest/config'
-
-export default defineConfig({})
diff --git a/test/config/test/chai-config.test.ts b/test/config/test/chai-config.test.ts
deleted file mode 100644
index 08c4f471d..000000000
--- a/test/config/test/chai-config.test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { runVitest } from '../../test-utils'
-
-describe('truncateThreshold', () => {
- it('default', async () => {
- const result = await runVitest({
- root: 'fixtures/chai-config',
- reporters: ['tap-flat'],
- })
- expect(cleanOutput(result.stdout)).toMatchInlineSnapshot(`
- "TAP version 13
- 1..9
- ok 1 - test-each-title.test.ts > '012345678901234567890123456789' (length = 30)
- ok 2 - test-each-title.test.ts > '0123456789012345678901234567890123456…' (length = 40)
- ok 3 - test-each-title.test.ts > '0123456789012345678901234567890123456…' (length = 50)
- ok 4 - test-each-title.test.ts > [ 'one', 'two', 'three' ]
- ok 5 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four' ]
- ok 6 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four', …(1) ]
- ok 7 - test-each-title.test.ts > { one: 1, two: 2, three: 3 }
- ok 8 - test-each-title.test.ts > { one: 1, two: 2, three: 3, four: 4 }
- ok 9 - test-each-title.test.ts > { one: 1, two: 2, three: 3, …(2) }"
- `)
- expect(result.exitCode).toBe(0)
- })
-
- it('40', async () => {
- const result = await runVitest({
- root: 'fixtures/chai-config',
- reporters: ['tap-flat'],
- chaiConfig: {
- truncateThreshold: 40,
- },
- })
- expect(cleanOutput(result.stdout)).toMatchInlineSnapshot(`
- "TAP version 13
- 1..9
- ok 1 - test-each-title.test.ts > '012345678901234567890123456789' (length = 30)
- ok 2 - test-each-title.test.ts > '0123456789012345678901234567890123456…' (length = 40)
- ok 3 - test-each-title.test.ts > '0123456789012345678901234567890123456…' (length = 50)
- ok 4 - test-each-title.test.ts > [ 'one', 'two', 'three' ]
- ok 5 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four' ]
- ok 6 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four', …(1) ]
- ok 7 - test-each-title.test.ts > { one: 1, two: 2, three: 3 }
- ok 8 - test-each-title.test.ts > { one: 1, two: 2, three: 3, four: 4 }
- ok 9 - test-each-title.test.ts > { one: 1, two: 2, three: 3, …(2) }"
- `)
- expect(result.exitCode).toBe(0)
- })
-
- it('0', async () => {
- const result = await runVitest({
- root: 'fixtures/chai-config',
- reporters: ['tap-flat'],
- chaiConfig: {
- truncateThreshold: 0,
- },
- })
- expect(cleanOutput(result.stdout)).toMatchInlineSnapshot(`
- "TAP version 13
- 1..9
- ok 1 - test-each-title.test.ts > '012345678901234567890123456789' (length = 30)
- ok 2 - test-each-title.test.ts > '0123456789012345678901234567890123456789' (length = 40)
- ok 3 - test-each-title.test.ts > '01234567890123456789012345678901234567890123456789' (length = 50)
- ok 4 - test-each-title.test.ts > [ 'one', 'two', 'three' ]
- ok 5 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four' ]
- ok 6 - test-each-title.test.ts > [ 'one', 'two', 'three', 'four', 'five' ]
- ok 7 - test-each-title.test.ts > { one: 1, two: 2, three: 3 }
- ok 8 - test-each-title.test.ts > { one: 1, two: 2, three: 3, four: 4 }
- ok 9 - test-each-title.test.ts > { one: 1, two: 2, three: 3, four: 4, five: 5 }"
- `)
- expect(result.exitCode).toBe(0)
- })
-})
-
-function cleanOutput(output: string) {
- // remove non-deterministic output
- return output.replaceAll(/\s*# time=.*/g, '').trim()
-}
diff --git a/test/core/package.json b/test/core/package.json
index 75d99e282..18538abd2 100644
--- a/test/core/package.json
+++ b/test/core/package.json
@@ -34,6 +34,7 @@
"@vueuse/integrations": "^14.2.1",
"axios": "^1.13.4",
"immutable": "5.1.5",
+ "loupe": "catalog:",
"memfs": "^4.56.11",
"obug": "^2.1.1",
"react": "^19.2.4",
diff --git a/test/core/test/exports.test.ts b/test/core/test/exports.test.ts
index f294229c8..7a365ed24 100644
--- a/test/core/test/exports.test.ts
+++ b/test/core/test/exports.test.ts
@@ -68,7 +68,6 @@ it('exports snapshot', async ({ skip, task }) => {
"SpyModule": "object",
"Traces": "function",
"__INTERNAL": "object",
- "browserFormat": "function",
"collectTests": "function",
"format": "function",
"getOriginalPosition": "function",
diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts
index 950a1cc97..38ec10c0f 100644
--- a/test/core/test/pretty-format.test.ts
+++ b/test/core/test/pretty-format.test.ts
@@ -1,4 +1,7 @@
+import { inspect as nodeInspect } from 'node:util'
import { format, plugins } from '@vitest/pretty-format'
+import { inspect as prettyInspect } from '@vitest/utils/display'
+import { inspect as loupeInspect } from 'loupe'
import { describe, expect, test } from 'vitest'
describe('maxOutputLength', () => {
@@ -707,6 +710,18 @@ describe('maxDepth option', () => {
})
describe('maxWidth option', () => {
+ test('object', () => {
+ const input = { one: 1, two: 2, three: 3, four: 4, five: 5 }
+ expect(format(input, { maxWidth: 3, compareKeys: null })).toMatchInlineSnapshot(`
+ "Object {
+ "one": 1,
+ "two": 2,
+ "three": 3,
+ …(2)
+ }"
+ `)
+ })
+
test('truncates arrays', () => {
expect(format([1, 2, 3, 4, 5], { maxWidth: 3 })).toMatchInlineSnapshot(
`
@@ -714,7 +729,7 @@ describe('maxWidth option', () => {
1,
2,
3,
- …
+ …(2)
]"
`,
)
@@ -727,7 +742,7 @@ describe('maxWidth option', () => {
1,
2,
3,
- …
+ …(2)
}"
`,
)
@@ -740,7 +755,7 @@ describe('maxWidth option', () => {
"Map {
"a" => 1,
"b" => 2,
- …
+ …(2)
}"
`,
)
@@ -886,6 +901,104 @@ describe('escapeRegex option', () => {
})
})
+describe('singleQuote option', () => {
+ test('uses double quotes by default', () => {
+ expect(format('hello')).toMatchInlineSnapshot(`""hello""`)
+ })
+
+ test('uses single quotes when true', () => {
+ expect(format('hello', { singleQuote: true })).toMatchInlineSnapshot(`"'hello'"`)
+ })
+
+ test('escapes single quotes inside string when singleQuote + escapeString', () => {
+ expect(format('it\'s', { singleQuote: true })).toMatchInlineSnapshot(`"'it\\'s'"`)
+ })
+
+ test('escapes backslash when singleQuote + escapeString', () => {
+ expect(format('a\\b', { singleQuote: true })).toMatchInlineSnapshot(`"'a\\\\b'"`)
+ })
+
+ test('does not escape double quotes when singleQuote', () => {
+ expect(format('say "hi"', { singleQuote: true })).toMatchInlineSnapshot(`"'say "hi"'"`)
+ })
+
+ test('applies to object values', () => {
+ expect(format({ a: 'b' }, { singleQuote: true, min: true })).toMatchInlineSnapshot(
+ `"{'a': 'b'}"`,
+ )
+ })
+
+ test('applies to Map keys and values', () => {
+ expect(format(new Map([['k', 'v']]), { singleQuote: true, min: true })).toMatchInlineSnapshot(
+ `"Map {'k' => 'v'}"`,
+ )
+ })
+})
+
+describe('quoteKeys option', () => {
+ test('forced quote', () => {
+ const input = {
+ '': 0,
+ '$a': 0,
+ '0a': 0,
+ 'a$': 0,
+ 'a-b': 0,
+ }
+ expect(format(input, { quoteKeys: false })).toMatchInlineSnapshot(`
+ "Object {
+ "": 0,
+ "$a": 0,
+ "0a": 0,
+ "a$": 0,
+ "a-b": 0,
+ }"
+ `)
+ })
+
+ test('no quote', () => {
+ const input = {
+ a: 0,
+ a0: 0,
+ a_b: 0,
+ }
+ expect(format(input, { quoteKeys: false })).toMatchInlineSnapshot(`
+ "Object {
+ a: 0,
+ a0: 0,
+ a_b: 0,
+ }"
+ `)
+ })
+
+ test('prototype', () => {
+ const input = Object.create(null)
+ // eslint-disable-next-line
+ input.__proto__ = 0
+ expect(format(input, { quoteKeys: false })).toMatchInlineSnapshot(`
+ "Object {
+ "__proto__": 0,
+ }"
+ `)
+ })
+})
+
+describe('spacingInner / spacingOuter options', () => {
+ test('min: true defaults', () => {
+ // min: true → spacingInner: ' ', spacingOuter: ''
+ expect(format({ a: 1, b: 2 }, { min: true })).toMatchInlineSnapshot(`"{"a": 1, "b": 2}"`)
+ })
+
+ test('spacingOuter override with min: true', () => {
+ // override spacingOuter to add space around braces (loupe-like)
+ expect(format({ a: 1 }, { min: true, spacingOuter: ' ' })).toMatchInlineSnapshot(`"{ "a": 1 }"`)
+ })
+
+ test('spacingInner override', () => {
+ // min: true still places comma before spacingInner
+ expect(format([1, 2], { min: true, spacingInner: ' | ' })).toMatchInlineSnapshot(`"[1, | 2]"`)
+ })
+})
+
describe('ErrorPlugin', () => {
test('Error with message', () => {
const err = new Error('boom')
@@ -990,8 +1103,6 @@ describe('plugins', () => {
})
})
-// -- validation --
-
describe('validation', () => {
test('throws on unknown option', () => {
expect(() => {
@@ -1000,3 +1111,291 @@ describe('validation', () => {
}).toThrowErrorMatchingInlineSnapshot(`[Error: pretty-format: Unknown option "badOption".]`)
})
})
+
+// -- prettyInspect --
+
+describe('prettyInspect', () => {
+ test('no truncation by default (truncate: 0)', () => {
+ const long = 'a'.repeat(200)
+ expect(prettyInspect(long)).toBe(`'${long}'`)
+ expect(prettyInspect(long, { truncate: 0 })).toBe(`'${long}'`)
+ })
+
+ test('no truncation when value fits within threshold', () => {
+ expect(prettyInspect('short', { truncate: 100 })).toMatchInlineSnapshot(`"'short'"`)
+ })
+
+ test('truncates string', () => {
+ const long = '0123456789012345678901234567890123456789'
+ expect(prettyInspect(long, { truncate: 20 })).toMatchInlineSnapshot(`"'012345678901234567…'"`)
+ })
+
+ test('truncates surragete pair correctly', () => {
+ expect(prettyInspect('😀'.repeat(5), { truncate: 14 })).toMatchInlineSnapshot(`"'😀😀😀😀😀'"`)
+ expect(prettyInspect('😀'.repeat(6), { truncate: 14 })).toMatchInlineSnapshot(`"'😀😀😀😀😀😀'"`)
+ expect(prettyInspect('😀'.repeat(7), { truncate: 14 })).toMatchInlineSnapshot(`"'😀😀😀😀😀😀…'"`)
+ expect(prettyInspect('😀'.repeat(8), { truncate: 14 })).toMatchInlineSnapshot(`"'😀😀😀😀😀😀…'"`)
+ expect(prettyInspect(`a${'😀'.repeat(5)}`, { truncate: 14 })).toMatchInlineSnapshot(`"'a😀😀😀😀😀'"`)
+ expect(prettyInspect(`a${'😀'.repeat(6)}`, { truncate: 14 })).toMatchInlineSnapshot(`"'a😀😀😀😀😀…'"`)
+ expect(prettyInspect(`a${'😀'.repeat(7)}`, { truncate: 14 })).toMatchInlineSnapshot(`"'a😀😀😀😀😀…'"`)
+ expect(prettyInspect(`a${'😀'.repeat(8)}`, { truncate: 14 })).toMatchInlineSnapshot(`"'a😀😀😀😀😀…'"`)
+ })
+
+ test('truncates array', () => {
+ expect(prettyInspect([1, 2, 3, 4, 5, 6], { truncate: 20 })).toMatchInlineSnapshot(`"[ 1, 2, 3, 4, 5, 6 ]"`)
+ expect(prettyInspect([1, 2, 3, 4, 5, 6, 7], { truncate: 20 })).toMatchInlineSnapshot(`"[ 1, 2, 3, 4, …(3) ]"`)
+ })
+
+ test('truncates object', () => {
+ expect(prettyInspect({ a: 1, b: 2, c: 3 }, { truncate: 20 })).toMatchInlineSnapshot(`"{ a: 1, b: 2, c: 3 }"`)
+ expect(prettyInspect({ a: 1, b: 2, c: 3, d: 4 }, { truncate: 20 })).toMatchInlineSnapshot(`"{ a: 1, b: 2, …(2) }"`)
+ })
+
+ test('truncate other types', () => {
+ expect(prettyInspect(new Map([['a', 1]]), { truncate: 25 })).toMatchInlineSnapshot(`"Map { 'a' => 1 }"`)
+ expect(prettyInspect(new Map([['a', 1], ['b', 2]]), { truncate: 25 })).toMatchInlineSnapshot(`"Map { 'a' => 1, …(1) }"`)
+ expect(prettyInspect(new Set([1, 2, 3, 4]), { truncate: 20 })).toMatchInlineSnapshot(`"Set { 1, 2, 3, 4 }"`)
+ expect(prettyInspect(new Set([1, 2, 3, 4, 5]), { truncate: 20 })).toMatchInlineSnapshot(`"Set { 1, 2, …(3) }"`)
+ })
+
+ test('multiline', () => {
+ expect(prettyInspect({ a: 1, b: 2 }, { multiline: true })).toMatchInlineSnapshot(`
+ "{
+ a: 1,
+ b: 2,
+ }"
+ `)
+ })
+})
+
+// -- three-way inspect comparison --
+// Compare prettyInspect output against node's util.inspect and loupe.inspect.
+// Organized by agreement pattern to show where outputs align or diverge.
+
+describe('inspect comparison (prettyInspect vs node vs loupe)', () => {
+ const nodeOpts = { depth: null, maxArrayLength: null } // depth 2 by default
+
+ // -- all three agree --
+
+ test.for([
+ null,
+ undefined,
+ true,
+ false,
+ -0,
+ 42,
+ -123,
+ 3.14,
+ Number.NaN,
+ Number.POSITIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ 123n,
+ -123n,
+ 'hello',
+ '',
+ /test/gi,
+ new Date(10e11),
+ Symbol('test'),
+ {},
+ [],
+ [1, 2, 3],
+ [[1], [2]],
+ { a: 1, b: 2 },
+ { b: 1, a: 2 },
+ { a: { b: { c: 1 } } },
+ [{ a: 1 }, { b: 2 }],
+ ])('all three match: %s', (val) => {
+ const result = prettyInspect(val)
+ expect(result).toBe(nodeInspect(val, nodeOpts))
+ expect(result).toBe(loupeInspect(val))
+ })
+
+ // -- prettyInspect matches node, diverges from loupe --
+
+ test('custom class — matches node (loupe omits space before brace)', () => {
+ class CustomClass {
+ public key: string
+ constructor() {
+ this.key = 'value'
+ }
+ }
+ expect(prettyInspect(new CustomClass())).toMatchInlineSnapshot(`"CustomClass { key: 'value' }"`)
+ expect(nodeInspect(new CustomClass(), nodeOpts)).toMatchInlineSnapshot(`"CustomClass { key: 'value' }"`)
+ expect(loupeInspect(new CustomClass())).toMatchInlineSnapshot(`"CustomClass{ key: 'value' }"`)
+ })
+
+ test('0 — matches node (loupe shows +0)', () => {
+ expect(prettyInspect(0)).toMatchInlineSnapshot(`"0"`)
+ expect(nodeInspect(0)).toMatchInlineSnapshot(`"0"`)
+ expect(loupeInspect(0)).toMatchInlineSnapshot(`"+0"`)
+ })
+
+ test('typed array', () => {
+ const input = new Uint8Array([1, 2, 3])
+ expect(prettyInspect(input)).toMatchInlineSnapshot(`"Uint8Array [ 1, 2, 3 ]"`)
+ expect(nodeInspect(input, nodeOpts)).toMatchInlineSnapshot(`"Uint8Array(3) [ 1, 2, 3 ]"`)
+ expect(loupeInspect(input)).toMatchInlineSnapshot(`"Uint8Array[ 1, 2, 3 ]"`)
+ })
+
+ test('non-enumerable properties — matches node (loupe shows them via getOwnPropertyNames)', () => {
+ const val = { visible: true }
+ Object.defineProperty(val, 'hidden', { enumerable: false, value: 'secret' })
+ expect(prettyInspect(val)).toMatchInlineSnapshot(`"{ visible: true }"`)
+ expect(nodeInspect(val, nodeOpts)).toMatchInlineSnapshot(`"{ visible: true }"`)
+ expect(loupeInspect(val)).toMatchInlineSnapshot(`"{ visible: true, hidden: 'secret' }"`)
+ })
+
+ // -- prettyInspect diverges from both --
+
+ test('string with single quotes — no escaping (escapeString: false)', () => {
+ expect(prettyInspect('it\'s')).toMatchInlineSnapshot(`"'it's'"`)
+ expect(nodeInspect('it\'s')).toMatchInlineSnapshot(`""it's""`)
+ expect(loupeInspect('it\'s')).toMatchInlineSnapshot(`"'it\\'s'"`)
+ })
+
+ test('named function — format differences', () => {
+ function myFn() {}
+ expect(prettyInspect(myFn)).toMatchInlineSnapshot(`"[Function myFn]"`)
+ expect(nodeInspect(myFn)).toMatchInlineSnapshot(`"[Function: myFn]"`)
+ expect(loupeInspect(myFn)).toMatchInlineSnapshot(`"[Function myFn]"`)
+ })
+
+ test('anonymous function', () => {
+ expect(prettyInspect((() => {}) as unknown)).toMatchInlineSnapshot(`"[Function anonymous]"`)
+ expect(nodeInspect(() => {})).toMatchInlineSnapshot(`"[Function (anonymous)]"`)
+ expect(loupeInspect(() => {})).toMatchInlineSnapshot(`"[Function]"`)
+ })
+
+ test('async function — loses AsyncFunction tag', () => {
+ async function asyncFn() {}
+ expect(prettyInspect(asyncFn)).toMatchInlineSnapshot(`"[Function asyncFn]"`)
+ expect(nodeInspect(asyncFn)).toMatchInlineSnapshot(`"[AsyncFunction: asyncFn]"`)
+ expect(loupeInspect(asyncFn)).toMatchInlineSnapshot(`"[AsyncFunction asyncFn]"`)
+ })
+
+ test('generator function — loses GeneratorFunction tag', () => {
+ function* genFn() {
+ yield 1
+ }
+ expect(prettyInspect(genFn)).toMatchInlineSnapshot(`"[Function genFn]"`)
+ expect(nodeInspect(genFn)).toMatchInlineSnapshot(`"[GeneratorFunction: genFn]"`)
+ expect(loupeInspect(genFn)).toMatchInlineSnapshot(`"[GeneratorFunction genFn]"`)
+ })
+
+ test('Error — bracket format', () => {
+ expect(prettyInspect(new Error('boom'))).toMatchInlineSnapshot(`"[Error: boom]"`)
+ expect(nodeInspect(new Error('boom'))).toMatch('Error: boom\n')
+ expect(loupeInspect(new Error('boom'))).toMatchInlineSnapshot(`"Error: boom"`)
+ })
+
+ test('Map — space before brace, no size prefix', () => {
+ const m = new Map([['a', 1], ['b', 2]])
+ expect(prettyInspect(m)).toMatchInlineSnapshot(`"Map { 'a' => 1, 'b' => 2 }"`)
+ expect(nodeInspect(m)).toMatchInlineSnapshot(`"Map(2) { 'a' => 1, 'b' => 2 }"`)
+ expect(loupeInspect(m)).toMatchInlineSnapshot(`"Map{ 'a' => 1, 'b' => 2 }"`)
+ })
+
+ test('Set — space before brace, no size prefix', () => {
+ const s = new Set([1, 2, 3])
+ expect(prettyInspect(s)).toMatchInlineSnapshot(`"Set { 1, 2, 3 }"`)
+ expect(nodeInspect(s)).toMatchInlineSnapshot(`"Set(3) { 1, 2, 3 }"`)
+ expect(loupeInspect(s)).toMatchInlineSnapshot(`"Set{ 1, 2, 3 }"`)
+ })
+
+ test('circular reference — no ref labels', () => {
+ const val: any = {}
+ val.self = val
+ expect(prettyInspect(val)).toMatchInlineSnapshot(`"{ self: [Circular] }"`)
+ expect(nodeInspect(val)).toMatchInlineSnapshot(`"[ { self: [Circular *1] }"`)
+ // loupe: would need circular-safe call, skip
+ })
+
+ test('WeakMap', () => {
+ expect(prettyInspect(new WeakMap())).toMatchInlineSnapshot(`"WeakMap {}"`)
+ expect(nodeInspect(new WeakMap())).toMatchInlineSnapshot(`"WeakMap { }"`)
+ expect(loupeInspect(new WeakMap())).toMatchInlineSnapshot(`"WeakMap{…}"`)
+ })
+
+ test('WeakSet', () => {
+ expect(prettyInspect(new WeakSet())).toMatchInlineSnapshot(`"WeakSet {}"`)
+ expect(nodeInspect(new WeakSet())).toMatchInlineSnapshot(`"WeakSet { }"`)
+ expect(loupeInspect(new WeakSet())).toMatchInlineSnapshot(`"WeakSet{…}"`)
+ })
+
+ test('Promise', () => {
+ expect(prettyInspect(Promise.resolve())).toMatchInlineSnapshot(`"Promise {}"`)
+ expect(nodeInspect(Promise.resolve())).toMatchInlineSnapshot(`"Promise { undefined }"`)
+ expect(loupeInspect(Promise.resolve())).toMatchInlineSnapshot(`"Promise{…}"`)
+ })
+
+ // -- truncation (prettyInspect vs loupe only) --
+ // loupe threads a character budget through recursion, truncating structurally.
+ // prettyInspect does surface-level truncation (structural summary for containers).
+
+ describe('truncation', () => {
+ test('short string — both fit', () => {
+ expect(prettyInspect('hi', { truncate: 40 })).toMatchInlineSnapshot(`"'hi'"`)
+ expect(loupeInspect('hi', { truncate: 40 })).toMatchInlineSnapshot(`"'hi'"`)
+ })
+
+ test('long string', () => {
+ const s = '0123456789012345678901234567890123456789'
+ expect(prettyInspect(s, { truncate: 20 })).toMatchInlineSnapshot(`"'012345678901234567…'"`)
+ expect(loupeInspect(s, { truncate: 20 })).toMatchInlineSnapshot(`"'01234567890123456…'"`)
+ })
+
+ test('short array — both fit', () => {
+ expect(prettyInspect([1, 2, 3], { truncate: 40 })).toMatchInlineSnapshot(`"[ 1, 2, 3 ]"`)
+ expect(loupeInspect([1, 2, 3], { truncate: 40 })).toMatchInlineSnapshot(`"[ 1, 2, 3 ]"`)
+ })
+
+ test('long array', () => {
+ const arr = [1, 2, 3, 4, 5]
+ expect(prettyInspect(arr, { truncate: 15 })).toMatchInlineSnapshot(`"[ 1, 2, …(3) ]"`)
+ expect(loupeInspect(arr, { truncate: 15 })).toMatchInlineSnapshot(`"[ 1, 2, …(3) ]"`)
+ })
+
+ test('array with long string values', () => {
+ const arr = ['one', 'two', 'three', 'four', 'five']
+ expect(prettyInspect(arr, { truncate: 40 })).toMatchInlineSnapshot(`"[ 'one', 'two', 'three', 'four', …(1) ]"`)
+ expect(loupeInspect(arr, { truncate: 40 })).toMatchInlineSnapshot(`"[ 'one', 'two', 'three', 'four', …(1) ]"`)
+ })
+
+ test('short object — both fit', () => {
+ expect(prettyInspect({ a: 1 }, { truncate: 40 })).toMatchInlineSnapshot(`"{ a: 1 }"`)
+ expect(loupeInspect({ a: 1 }, { truncate: 40 })).toMatchInlineSnapshot(`"{ a: 1 }"`)
+ })
+
+ test('long object', () => {
+ const obj = { one: 1, two: 2, three: 3, four: 4, five: 5 }
+ expect(prettyInspect(obj, { truncate: 40 })).toMatchInlineSnapshot(`"{ one: 1, two: 2, three: 3, …(2) }"`)
+ expect(loupeInspect(obj, { truncate: 40 })).toMatchInlineSnapshot(`"{ one: 1, two: 2, three: 3, …(2) }"`)
+ })
+
+ test('nested object — stringify adaptive maxDepth halves depth until it fits', () => {
+ const obj = { a: { b: { c: 'deep' } } }
+ // full output is "{ a: { b: { c: 'deep' } } }" (28 chars)
+ // stringify halves maxDepth, collapsing inner object to [Object]
+ expect(prettyInspect(obj, { truncate: 20 })).toMatchInlineSnapshot(`"{ a: [Object] }"`)
+ expect(loupeInspect(obj, { truncate: 20 })).toMatchInlineSnapshot(`"{ a: { …(1) } }"`)
+ })
+
+ test('Map', () => {
+ const m = new Map([['a', 1], ['b', 2], ['c', 3]])
+ expect(prettyInspect(m, { truncate: 20 })).toMatchInlineSnapshot(`"Map { …(3) }"`)
+ expect(loupeInspect(m, { truncate: 20 })).toMatchInlineSnapshot(`"Map{ …(3) }"`)
+ })
+
+ test('Set', () => {
+ const s = new Set([1, 2, 3, 4, 5])
+ expect(prettyInspect(s, { truncate: 15 })).toMatchInlineSnapshot(`"Set { 1, …(4) }"`)
+ expect(loupeInspect(s, { truncate: 15 })).toMatchInlineSnapshot(`"Set{ 1, …(4) }"`)
+ })
+
+ test('function', () => {
+ function myLongFunctionName() {}
+ expect(prettyInspect(myLongFunctionName, { truncate: 10 })).toMatchInlineSnapshot(`"[Function myLongFunctionName]"`)
+ expect(loupeInspect(myLongFunctionName, { truncate: 10 })).toMatchInlineSnapshot(`"[Function …]"`)
+ })
+ })
+})
diff --git a/test/core/test/utils-display.spec.ts b/test/core/test/utils-display.spec.ts
index d538edd07..dec630ce8 100644
--- a/test/core/test/utils-display.spec.ts
+++ b/test/core/test/utils-display.spec.ts
@@ -74,11 +74,11 @@ describe('format', () => {
['%%', 'string'],
['prefix', Symbol('test')],
])('format(%s)', (formatString, ...args) => {
- expect(format(formatString, ...args), `failed ${formatString}`).toBe(util.format(formatString, ...args))
+ expect(format([formatString, ...args]), `failed ${formatString}`).toBe(util.format(formatString, ...args))
})
test('cannot serialize some values', () => {
- expect(() => format('%j', 100n)).toThrowErrorMatchingInlineSnapshot(`[TypeError: Do not know how to serialize a BigInt]`)
+ expect(() => format(['%j', 100n])).toThrowErrorMatchingInlineSnapshot(`[TypeError: Do not know how to serialize a BigInt]`)
})
test.each(
@@ -105,6 +105,6 @@ describe('format', () => {
},
],
)('formats objects $name (loupe doesn\'t respect depth)', ({ args, result }) => {
- expect(format(...args)).toBe(result)
+ expect(format(args)).toBe(result)
})
})
diff --git a/test/core/test/utils.spec.ts b/test/core/test/utils.spec.ts
index 4e792d3a8..a9f135373 100644
--- a/test/core/test/utils.spec.ts
+++ b/test/core/test/utils.spec.ts
@@ -1,4 +1,3 @@
-import { objDisplay } from '@vitest/utils/display'
import { assertTypes, deepClone, deepMerge, isNegativeNaN, objectAttr, toArray } from '@vitest/utils/helpers'
import { parseSingleFFOrSafariStack } from '@vitest/utils/source-map'
import { EvaluatedModules } from 'vite/module-runner'
@@ -279,20 +278,6 @@ describe('objectAttr', () => {
})
})
-describe('objDisplay', () => {
- test.each`
- value | expected
- ${'a'.repeat(100)} | ${`'${'a'.repeat(37)}…'`}
- ${'🐱'.repeat(100)} | ${`'${'🐱'.repeat(18)}…'`}
- ${`a${'🐱'.repeat(100)}…`} | ${`'a${'🐱'.repeat(18)}…'`}
- `('Do not truncate strings anywhere but produce valid unicode strings for $value', ({ value, expected }) => {
- // encodeURI can be used to detect invalid strings including invalid code-points
- // note: our code should not split surrogate pairs, but may split graphemes
- expect(() => encodeURI(objDisplay(value))).not.toThrow()
- expect(objDisplay(value)).toEqual(expected)
- })
-})
-
describe('isNegativeNaN', () => {
test.each`
value | expected
]