Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions packages/utils/src/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ const FALLBACK_FORMAT_OPTIONS = {
plugins: PLUGINS,
} satisfies PrettyFormatOptions

export interface StringifiedMemory {

@sheremet-va sheremet-va May 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I initially just wanted to use WeakMap, but we are cloning and reorganizing (set/map) objects, so it's easier to just have an object that sets it

expected?: string
actual?: string
}

interface Memorize {
(pointer: 'expected' | 'actual', stringifiedValue: string): string
}
Comment on lines +70 to +72

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, I think what I felt odd is that memorize interface isn't even explicitly "stashing the result as argument" API.
So my question is, can this be diff(..., memory?: StringifiedMemory) directly instead of stashing through calling memorize: Memorize?

@sheremet-va sheremet-va May 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It could, but it’s just less code with a util. It’s ugly to have val = string after every stringify call 😬

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I get the compactness but it feels an unnecessary indirection for my brain. not blocking though.


const DEFAULT_MEMORIZE: Memorize = (_, v) => v

// Generate a string that will highlight the difference between two values
// with green and red. (similar to how github does code diffing)

Expand All @@ -71,7 +82,7 @@ const FALLBACK_FORMAT_OPTIONS = {
* @param options Diff options
* @returns {string | null} a string diff
*/
export function diff(a: any, b: any, options?: DiffOptions): string | undefined {
export function diff(a: any, b: any, options?: DiffOptions, memorize: Memorize = DEFAULT_MEMORIZE): string | undefined {
if (Object.is(a, b)) {
return ''
}
Expand Down Expand Up @@ -108,8 +119,8 @@ export function diff(a: any, b: any, options?: DiffOptions): string | undefined
function truncate(s: string) {
return s.length <= MAX_LENGTH ? s : (`${s.slice(0, MAX_LENGTH)}...`)
}
aDisplay = truncate(aDisplay)
bDisplay = truncate(bDisplay)
aDisplay = memorize('expected', truncate(aDisplay))
bDisplay = memorize('actual', truncate(bDisplay))
const aDiff = `${aColor(`${aIndicator} ${aAnnotation}:`)}\n${aDisplay}`
const bDiff = `${bColor(`${bIndicator} ${bAnnotation}:`)}\n${bDisplay}`
return `${aDiff}\n\n${bDiff}`
Expand All @@ -124,23 +135,31 @@ export function diff(a: any, b: any, options?: DiffOptions): string | undefined
return diffLinesUnified(a.split('\n'), b.split('\n'), options)
case 'boolean':
case 'number':
return comparePrimitive(a, b, options)
return comparePrimitive(a, b, options, memorize)
case 'map':
return compareObjects(sortMap(a), sortMap(b), options)
return compareObjects(sortMap(a), sortMap(b), options, memorize)
case 'set':
return compareObjects(sortSet(a), sortSet(b), options)
return compareObjects(sortSet(a), sortSet(b), options, memorize)
default:
return compareObjects(a, b, options)
return compareObjects(a, b, options, memorize)
}
}

function createMemorize(memory: StringifiedMemory) {
return (pointer: 'expected' | 'actual', stringifiedValue: string) => {
memory[pointer] = stringifiedValue
return stringifiedValue
}
}

function comparePrimitive(
a: number | boolean,
b: number | boolean,
options?: DiffOptions,
memorize: Memorize = DEFAULT_MEMORIZE,
) {
const aFormat = prettyFormat(a, FORMAT_OPTIONS)
const bFormat = prettyFormat(b, FORMAT_OPTIONS)
const aFormat = memorize('expected', prettyFormat(a, FORMAT_OPTIONS))
const bFormat = memorize('actual', prettyFormat(b, FORMAT_OPTIONS))
return aFormat === bFormat
? ''
: diffLinesUnified(aFormat.split('\n'), bFormat.split('\n'), options)
Expand All @@ -158,13 +177,14 @@ function compareObjects(
a: Record<string, any>,
b: Record<string, any>,
options?: DiffOptions,
memorize: Memorize = DEFAULT_MEMORIZE,
) {
let difference
let hasThrown = false

try {
const formatOptions = getFormatOptions(FORMAT_OPTIONS, options)
difference = getObjectsDifference(a, b, formatOptions, options)
difference = getObjectsDifference(a, b, formatOptions, options, memorize)
}
catch {
hasThrown = true
Expand All @@ -175,7 +195,7 @@ function compareObjects(
// without calling `toJSON`. It's also possible that toJSON might throw.
if (difference === undefined || difference === noDiffMessage) {
const formatOptions = getFormatOptions(FALLBACK_FORMAT_OPTIONS, options)
difference = getObjectsDifference(a, b, formatOptions, options)
difference = getObjectsDifference(a, b, formatOptions, options, memorize)

if (difference !== noDiffMessage && !hasThrown) {
difference = `${getCommonMessage(
Expand All @@ -188,6 +208,10 @@ function compareObjects(
return difference
}

export function getDefaultFormatOptions(options?: DiffOptions): PrettyFormatOptions {
return getFormatOptions(FORMAT_OPTIONS, options)
}

function getFormatOptions(
formatOptions: PrettyFormatOptions,
options?: DiffOptions,
Expand All @@ -207,6 +231,7 @@ function getObjectsDifference(
b: Record<string, any>,
formatOptions: PrettyFormatOptions,
options?: DiffOptions,
memorize: Memorize = DEFAULT_MEMORIZE,
): string {
const formatOptionsZeroIndent = { ...formatOptions, indent: 0 }
const aCompare = prettyFormat(a, formatOptionsZeroIndent)
Expand All @@ -216,8 +241,8 @@ function getObjectsDifference(
return getCommonMessage(NO_DIFF_MESSAGE, options)
}
else {
const aDisplay = prettyFormat(a, formatOptions)
const bDisplay = prettyFormat(b, formatOptions)
const aDisplay = memorize('expected', prettyFormat(a, formatOptions))
const bDisplay = memorize('actual', prettyFormat(b, formatOptions))

return diffLinesUnified2(
aDisplay.split('\n'),
Expand Down Expand Up @@ -248,6 +273,7 @@ export function printDiffOrStringify(
received: unknown,
expected: unknown,
options?: DiffOptions,
memory?: StringifiedMemory,
): string | undefined {
const { aAnnotation, bAnnotation } = normalizeDiffOptions(options)

Expand Down Expand Up @@ -286,7 +312,8 @@ export function printDiffOrStringify(
const clonedExpected = deepClone(expected, { forceWritable: true })
const clonedReceived = deepClone(received, { forceWritable: true })
const { replacedExpected, replacedActual } = replaceAsymmetricMatcher(clonedReceived, clonedExpected)
const difference = diff(replacedExpected, replacedActual, options)
const memorize = memory ? createMemorize(memory) : DEFAULT_MEMORIZE
const difference = diff(replacedExpected, replacedActual, options, memorize)

return difference
// }
Expand Down
35 changes: 24 additions & 11 deletions packages/utils/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DiffOptions } from './diff'
import type { DiffOptions, StringifiedMemory } from './diff'
import type { TestError } from './types'
import { printDiffOrStringify } from './diff'
import { stringify } from './display'
import { format as prettyFormat } from '@vitest/pretty-format'
import { getDefaultFormatOptions, printDiffOrStringify } from './diff'
import { serializeValue } from './serialize'

export { serializeValue as serializeError }
Expand All @@ -22,17 +22,20 @@ export function processError(
&& err.expected !== undefined
&& err.actual !== undefined)
) {
err.diff = printDiffOrStringify(err.actual, err.expected, {
const memory: StringifiedMemory = {}
const options = {
...diffOptions,
...err.diffOptions as DiffOptions,
})
}
}
err.diff = printDiffOrStringify(
err.actual,
err.expected,
options,
memory,
)

if ('expected' in err && typeof err.expected !== 'string') {
err.expected = stringify(err.expected, 10)
}
if ('actual' in err && typeof err.actual !== 'string') {
err.actual = stringify(err.actual, 10)
err.expected = prettifyValue('expected', err.expected, options, memory)
err.actual = prettifyValue('actual', err.actual, options, memory)
}

// some Error implementations may not allow rewriting cause
Expand All @@ -56,3 +59,13 @@ export function processError(
)
}
}

function prettifyValue(pointer: 'expected' | 'actual', value: unknown, options: DiffOptions, memory: StringifiedMemory): string | undefined {
if (pointer in memory) {
return memory[pointer]
}
if (typeof value !== 'string') {
return prettyFormat(value, getDefaultFormatOptions(options))
}
return value
}
5 changes: 5 additions & 0 deletions packages/utils/src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ declare class Element {
tagName: string
}

// buffer exists only in node
declare class Buffer {
length: number
}

const IS_RECORD_SYMBOL = '@@__IMMUTABLE_RECORD__@@'
const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'

Expand Down
Loading
Loading