@@ -12,6 +12,9 @@ import {
1212 format as prettyFormat ,
1313 plugins as prettyFormatPlugins ,
1414} from '@vitest/pretty-format'
15+ import c from 'tinyrainbow'
16+ import { stringify } from '../display'
17+ import { deepClone , getOwnProperties , getType as getSimpleType } from '../helpers'
1518import { getType } from './getType'
1619import { DIFF_DELETE , DIFF_EQUAL , DIFF_INSERT , Diff } from './cleanupSemantic'
1720import { NO_DIFF_MESSAGE , SIMILAR_MESSAGE } from './constants'
@@ -211,3 +214,160 @@ function getObjectsDifference(
211214 )
212215 }
213216}
217+
218+ const MAX_DIFF_STRING_LENGTH = 20_000
219+
220+ function isAsymmetricMatcher ( data : any ) {
221+ const type = getSimpleType ( data )
222+ return type === 'Object' && typeof data . asymmetricMatch === 'function'
223+ }
224+
225+ function isReplaceable ( obj1 : any , obj2 : any ) {
226+ const obj1Type = getSimpleType ( obj1 )
227+ const obj2Type = getSimpleType ( obj2 )
228+ return (
229+ obj1Type === obj2Type && ( obj1Type === 'Object' || obj1Type === 'Array' )
230+ )
231+ }
232+
233+ export function printDiffOrStringify (
234+ expected : unknown ,
235+ received : unknown ,
236+ options ?: DiffOptions ,
237+ ) : string | null {
238+ const { aAnnotation, bAnnotation } = normalizeDiffOptions ( options )
239+
240+ if (
241+ typeof expected === 'string'
242+ && typeof received === 'string'
243+ && expected . length > 0
244+ && received . length > 0
245+ && expected . length <= MAX_DIFF_STRING_LENGTH
246+ && received . length <= MAX_DIFF_STRING_LENGTH
247+ && expected !== received
248+ ) {
249+ if ( expected . includes ( '\n' ) || received . includes ( '\n' ) ) {
250+ return diffStringsUnified ( received , expected , options )
251+ }
252+
253+ const [ diffs ] = diffStringsRaw ( received , expected , true )
254+ const hasCommonDiff = diffs . some ( diff => diff [ 0 ] === DIFF_EQUAL )
255+
256+ const printLabel = getLabelPrinter ( aAnnotation , bAnnotation )
257+ const expectedLine
258+ = printLabel ( aAnnotation )
259+ + printExpected (
260+ getCommonAndChangedSubstrings ( diffs , DIFF_DELETE , hasCommonDiff ) ,
261+ )
262+ const receivedLine
263+ = printLabel ( bAnnotation )
264+ + printReceived (
265+ getCommonAndChangedSubstrings ( diffs , DIFF_INSERT , hasCommonDiff ) ,
266+ )
267+
268+ return `${ expectedLine } \n${ receivedLine } `
269+ }
270+
271+ // if (isLineDiffable(expected, received)) {
272+ const clonedExpected = deepClone ( expected , { forceWritable : true } )
273+ const clonedReceived = deepClone ( received , { forceWritable : true } )
274+ const { replacedExpected, replacedActual } = replaceAsymmetricMatcher ( clonedExpected , clonedReceived )
275+ const difference = diff ( replacedExpected , replacedActual , options )
276+
277+ return difference
278+ // }
279+
280+ // const printLabel = getLabelPrinter(aAnnotation, bAnnotation)
281+ // const expectedLine = printLabel(aAnnotation) + printExpected(expected)
282+ // const receivedLine
283+ // = printLabel(bAnnotation)
284+ // + (stringify(expected) === stringify(received)
285+ // ? 'serializes to the same string'
286+ // : printReceived(received))
287+
288+ // return `${expectedLine}\n${receivedLine}`
289+ }
290+
291+ export function replaceAsymmetricMatcher (
292+ actual : any ,
293+ expected : any ,
294+ actualReplaced : WeakSet < WeakKey > = new WeakSet ( ) ,
295+ expectedReplaced : WeakSet < WeakKey > = new WeakSet ( ) ,
296+ ) : {
297+ replacedActual : any
298+ replacedExpected : any
299+ } {
300+ if ( ! isReplaceable ( actual , expected ) ) {
301+ return { replacedActual : actual , replacedExpected : expected }
302+ }
303+ if ( actualReplaced . has ( actual ) || expectedReplaced . has ( expected ) ) {
304+ return { replacedActual : actual , replacedExpected : expected }
305+ }
306+ actualReplaced . add ( actual )
307+ expectedReplaced . add ( expected )
308+ getOwnProperties ( expected ) . forEach ( ( key ) => {
309+ const expectedValue = expected [ key ]
310+ const actualValue = actual [ key ]
311+ if ( isAsymmetricMatcher ( expectedValue ) ) {
312+ if ( expectedValue . asymmetricMatch ( actualValue ) ) {
313+ actual [ key ] = expectedValue
314+ }
315+ }
316+ else if ( isAsymmetricMatcher ( actualValue ) ) {
317+ if ( actualValue . asymmetricMatch ( expectedValue ) ) {
318+ expected [ key ] = actualValue
319+ }
320+ }
321+ else if ( isReplaceable ( actualValue , expectedValue ) ) {
322+ const replaced = replaceAsymmetricMatcher (
323+ actualValue ,
324+ expectedValue ,
325+ actualReplaced ,
326+ expectedReplaced ,
327+ )
328+ actual [ key ] = replaced . replacedActual
329+ expected [ key ] = replaced . replacedExpected
330+ }
331+ } )
332+ return {
333+ replacedActual : actual ,
334+ replacedExpected : expected ,
335+ }
336+ }
337+
338+ type PrintLabel = ( string : string ) => string
339+ export function getLabelPrinter ( ...strings : Array < string > ) : PrintLabel {
340+ const maxLength = strings . reduce (
341+ ( max , string ) => ( string . length > max ? string . length : max ) ,
342+ 0 ,
343+ )
344+ return ( string : string ) : string =>
345+ `${ string } : ${ ' ' . repeat ( maxLength - string . length ) } `
346+ }
347+
348+ const SPACE_SYMBOL = '\u{00B7}' // middle dot
349+ function replaceTrailingSpaces ( text : string ) : string {
350+ return text . replace ( / \s + $ / gm, spaces => SPACE_SYMBOL . repeat ( spaces . length ) )
351+ }
352+
353+ function printReceived ( object : unknown ) : string {
354+ return c . red ( replaceTrailingSpaces ( stringify ( object ) ) )
355+ }
356+ function printExpected ( value : unknown ) : string {
357+ return c . green ( replaceTrailingSpaces ( stringify ( value ) ) )
358+ }
359+
360+ function getCommonAndChangedSubstrings ( diffs : Array < Diff > , op : number , hasCommonDiff : boolean ) : string {
361+ return diffs . reduce (
362+ ( reduced : string , diff : Diff ) : string =>
363+ reduced
364+ + ( diff [ 0 ] === DIFF_EQUAL
365+ ? diff [ 1 ]
366+ : diff [ 0 ] === op
367+ ? hasCommonDiff
368+ ? c . inverse ( diff [ 1 ] )
369+ : diff [ 1 ]
370+ : '' ) ,
371+ '' ,
372+ )
373+ }
0 commit comments