/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */ import chalk from '@casual-simulation/chalk' ; import { DIFF_DELETE , DIFF_EQUAL , DIFF_INSERT , Diff , DiffOptions as ImportDiffOptions , diff as diffDefault , diffStringsRaw , diffStringsUnified , } from '../diff' ; import { getType , isPrimitive } from '../get-type' ; import { format as prettyFormat , plugins as prettyFormatPlugins , } from '../pretty-format' ; import Replaceable from './Replaceable' ; import deepCyclicCopyReplaceable from './deepCyclicCopyReplaceable' ; const { AsymmetricMatcher , DOMCollection , DOMElement , Immutable , ReactTestComponent , } = prettyFormatPlugins ; const PLUGINS = [ ReactTestComponent , DOMElement , DOMCollection , Immutable , AsymmetricMatcher , ] ; type MatcherHintColor = ( arg : string ) => string ; export type MatcherHintOptions = { comment ? : string ; expectedColor ? : MatcherHintColor ; isDirectExpectCall ? : boolean ; isNot ? : boolean ; promise ? : string ; receivedColor ? : MatcherHintColor ; secondArgument ? : string ; secondArgumentColor ? : MatcherHintColor ; } ; export type DiffOptions = ImportDiffOptions ; export const EXPECTED_COLOR = chalk . green ; export const RECEIVED_COLOR = chalk . red ; export const INVERTED_COLOR = chalk . inverse ; export const BOLD_WEIGHT = chalk . bold ; export const DIM_COLOR = chalk . dim ; const MULTILINE_REGEXP = /\n/ ; const SPACE_SYMBOL = '\u{00B7}' ; const NUMBERS = [ 'zero' , 'one' , 'two' , 'three' , 'four' , 'five' , 'six' , 'seven' , 'eight' , 'nine' , 'ten' , 'eleven' , 'twelve' , 'thirteen' , ] ; export const SUGGEST_TO_CONTAIN_EQUAL = chalk . dim ( 'Looks like you wanted to test for object/array equality with the stricter `toContain` matcher. You probably need to use `toContainEqual` instead.' ) ; export const stringify = ( object , maxDepth = 10 ) : string => { const MAX_LENGTH = 10000 ; let result ; try { result = prettyFormat ( object , { maxDepth , min : true , plugins : PLUGINS , } ) ; } catch { result = prettyFormat ( object , { callToJSON : false , maxDepth , min : true , plugins : PLUGINS , } ) ; } return result . length >= MAX_LENGTH && maxDepth > 1 ? stringify ( object , Math . floor ( maxDepth / 2 ) ) : result ; } ; export const highlightTrailingWhitespace = ( text ) : string => text . replace ( /\s+$/gm , chalk . inverse ( '$&' ) ) ; const replaceTrailingSpaces = ( text ) : string => text . replace ( /\s+$/gm , ( spaces ) => SPACE_SYMBOL . repeat ( spaces . length ) ) ; export const printReceived = ( object ) : string => RECEIVED_COLOR ( replaceTrailingSpaces ( stringify ( object ) ) ) ; export const printExpected = ( value ) : string => EXPECTED_COLOR ( replaceTrailingSpaces ( stringify ( value ) ) ) ; export const printWithType = ( name , value , print ) : string => { const type = getType ( value ) ; const hasType = type !== 'null' && type !== 'undefined' ? `${ name } has type:  ${ type }\n` : '' ; const hasValue = `${ name } has value: ${ print ( value ) }` ; return hasType + hasValue ; } ; export const ensureNoExpected = ( expected , matcherName , options ? ) : void => { if ( typeof expected !== 'undefined' ) { const matcherString = ( options ? '' : '[.not]' ) + matcherName ; throw new Error ( matcherErrorMessage ( matcherHint ( matcherString , undefined , '' , options ) , 'this matcher must not have an expected argument' , printWithType ( 'Expected' , expected , printExpected ) ) ) ; } } ; /**
 * Ensures that `actual` is of type `number | bigint`
 */ export const ensureActualIsNumber = ( actual , matcherName , options ? ) : void => { if ( typeof actual !== 'number' && typeof actual !== 'bigint' ) { const matcherString = ( options ? '' : '[.not]' ) + matcherName ; throw new Error ( matcherErrorMessage ( matcherHint ( matcherString , undefined , undefined , options ) , `${ RECEIVED_COLOR ( 'received' ) } value must be a number or bigint` , printWithType ( 'Received' , actual , printReceived ) ) ) ; } } ; /**
 * Ensures that `expected` is of type `number | bigint`
 */ export const ensureExpectedIsNumber = ( expected , matcherName , options ? ) : void => { if ( typeof expected !== 'number' && typeof expected !== 'bigint' ) { const matcherString = ( options ? '' : '[.not]' ) + matcherName ; throw new Error ( matcherErrorMessage ( matcherHint ( matcherString , undefined , undefined , options ) , `${ EXPECTED_COLOR ( 'expected' ) } value must be a number or bigint` , printWithType ( 'Expected' , expected , printExpected ) ) ) ; } } ; /**
 * Ensures that `actual` & `expected` are of type `number | bigint`
 */ export const ensureNumbers = ( actual , expected , matcherName , options ? ) : void => { ensureActualIsNumber ( actual , matcherName , options ) ; ensureExpectedIsNumber ( expected , matcherName , options ) ; } ; export const ensureExpectedIsNonNegativeInteger = ( expected , matcherName , options ? ) : void => { if ( typeof expected !== 'number' || ! Number . isSafeInteger ( expected ) || expected < 0 ) { const matcherString = ( options ? '' : '[.not]' ) + matcherName ; throw new Error ( matcherErrorMessage ( matcherHint ( matcherString , undefined , undefined , options ) , `${ EXPECTED_COLOR ( 'expected' ) } value must be a non-negative integer` , printWithType ( 'Expected' , expected , printExpected ) ) ) ; } } ; const getCommonAndChangedSubstrings = ( diffs , op , hasCommonDiff ) : string => diffs . reduce ( ( reduced , diff ) : string => reduced + ( diff [ 0 ] === DIFF_EQUAL ? diff [ 1 ] : diff [ 0 ] !== op ? '' : hasCommonDiff ? INVERTED_COLOR ( diff [ 1 ] ) : diff [ 1 ] ) , '' ) ; const isLineDiffable = ( expected , received ) : boolean => { const expectedType = getType ( expected ) ; const receivedType = getType ( received ) ; if ( expectedType !== receivedType ) { return false ; } if ( isPrimitive ( expected ) ) { return ( typeof expected === 'string' && typeof received === 'string' && expected . length !== 0 && received . length !== 0 && ( MULTILINE_REGEXP . test ( expected ) || MULTILINE_REGEXP . test ( received ) ) ) ; } if ( expectedType === 'date' || expectedType === 'function' || expectedType === 'regexp' ) { return false ; } if ( expected instanceof Error && received instanceof Error ) { return false ; } if ( expectedType === 'object' && typeof ( expected as any ) . asymmetricMatch === 'function' ) { return false ; } if ( receivedType === 'object' && typeof ( received as any ) . asymmetricMatch === 'function' ) { return false ; } return true ; } ; const MAX_DIFF_STRING_LENGTH = 20000 ; export const printDiffOrStringify = ( expected , received , expectedLabel , receivedLabel , expand ) : string => { if ( typeof expected === 'string' && typeof received === 'string' && expected . length !== 0 && received . length !== 0 && expected . length <= MAX_DIFF_STRING_LENGTH && received . length <= MAX_DIFF_STRING_LENGTH && expected !== received ) { if ( expected . includes ( '\n' ) || received . includes ( '\n' ) ) { return diffStringsUnified ( expected , received , { aAnnotation : expectedLabel , bAnnotation : receivedLabel , changeLineTrailingSpaceColor : chalk . bgYellow , commonLineTrailingSpaceColor : chalk . bgYellow , emptyFirstOrLastLinePlaceholder : '↵' , expand , includeChangeCounts : true , } ) ; } const diffs = diffStringsRaw ( expected , received , true ) ; const hasCommonDiff = diffs . some ( ( diff ) => diff [ 0 ] === DIFF_EQUAL ) ; const printLabel = getLabelPrinter ( expectedLabel , receivedLabel ) ; const expectedLine = printLabel ( expectedLabel ) + printExpected ( getCommonAndChangedSubstrings ( diffs , DIFF_DELETE , hasCommonDiff ) ) ; const receivedLine = printLabel ( receivedLabel ) + printReceived ( getCommonAndChangedSubstrings ( diffs , DIFF_INSERT , hasCommonDiff ) ) ; return expectedLine + '\n' + receivedLine ; } if ( isLineDiffable ( expected , received ) ) { const { replacedExpected , replacedReceived } = replaceMatchedToAsymmetricMatcher ( deepCyclicCopyReplaceable ( expected ) , deepCyclicCopyReplaceable ( received ) , [ ] , [ ] ) ; const difference = diffDefault ( replacedExpected , replacedReceived , { aAnnotation : expectedLabel , bAnnotation : receivedLabel , expand , includeChangeCounts : true , } ) ; if ( typeof difference === 'string' && difference . includes ( '- ' + expectedLabel ) && difference . includes ( '+ ' + receivedLabel ) ) { return difference ; } } const printLabel = getLabelPrinter ( expectedLabel , receivedLabel ) ; const expectedLine = printLabel ( expectedLabel ) + printExpected ( expected ) ; const receivedLine = printLabel ( receivedLabel ) + ( stringify ( expected ) === stringify ( received ) ? 'serializes to the same string' : printReceived ( received ) ) ; return expectedLine + '\n' + receivedLine ; } ; const shouldPrintDiff = ( actual , expected ) => { if ( typeof actual === 'number' && typeof expected === 'number' ) { return false ; } if ( typeof actual === 'bigint' && typeof expected === 'bigint' ) { return false ; } if ( typeof actual === 'boolean' && typeof expected === 'boolean' ) { return false ; } return true ; } ; function replaceMatchedToAsymmetricMatcher ( replacedExpected , replacedReceived , expectedCycles , receivedCycles ) { if ( ! Replaceable . isReplaceable ( replacedExpected , replacedReceived ) ) { return { replacedExpected , replacedReceived } ; } if ( expectedCycles . includes ( replacedExpected ) || receivedCycles . includes ( replacedReceived ) ) { return { replacedExpected , replacedReceived } ; } expectedCycles . push ( replacedExpected ) ; receivedCycles . push ( replacedReceived ) ; const expectedReplaceable = new Replaceable ( replacedExpected ) ; const receivedReplaceable = new Replaceable ( replacedReceived ) ; expectedReplaceable . forEach ( ( expectedValue , key ) => { const receivedValue = receivedReplaceable . get ( key ) ; if ( isAsymmetricMatcher ( expectedValue ) ) { if ( expectedValue . asymmetricMatch ( receivedValue ) ) { receivedReplaceable . set ( key , expectedValue ) ; } } else if ( isAsymmetricMatcher ( receivedValue ) ) { if ( receivedValue . asymmetricMatch ( expectedValue ) ) { expectedReplaceable . set ( key , receivedValue ) ; } } else if ( Replaceable . isReplaceable ( expectedValue , receivedValue ) ) { const replaced = replaceMatchedToAsymmetricMatcher ( expectedValue , receivedValue , expectedCycles , receivedCycles ) ; expectedReplaceable . set ( key , replaced . replacedExpected ) ; receivedReplaceable . set ( key , replaced . replacedReceived ) ; } } ) ; return { replacedExpected : expectedReplaceable . object , replacedReceived : receivedReplaceable . object , } ; } type AsymmetricMatcher = { asymmetricMatch : Function ; } ; function isAsymmetricMatcher ( data ) { const type = getType ( data ) ; return type === 'object' && typeof data . asymmetricMatch === 'function' ; } export const diff = ( a , b , options ? ) : string | null => ( shouldPrintDiff ( a , b ) ? diffDefault ( a , b , options ) : null ) ; export const pluralize = ( word , count ) : string => ( NUMBERS [ count ] || count ) + ' ' + word + ( count === 1 ? '' : 's' ) ; type PrintLabel = ( string : string ) => string ; export const getLabelPrinter = ( ... strings ) : PrintLabel => { const maxLength = strings . reduce ( ( max , string ) => ( string . length > max ? string . length : max ) , 0 ) ; return ( string ) : string => `${ string }: ${ ' ' . repeat ( maxLength - string . length ) }` ; } ; export const matcherErrorMessage = ( hint , generic , specific ? ) : string => `${ hint }\n\n${ chalk . bold ( 'Matcher error' ) }: ${ generic }${ typeof specific === 'string' ? '\n\n' + specific : '' }` ; export const matcherHint = ( matcherName , received = 'received' , expected = 'expected' , options = { } ) : string => { const { comment = '' , expectedColor = EXPECTED_COLOR , isDirectExpectCall = false , isNot = false , promise = '' , receivedColor = RECEIVED_COLOR , secondArgument = '' , secondArgumentColor = EXPECTED_COLOR , } = options ; let hint = '' ; let dimString = 'expect' ; if ( ! isDirectExpectCall && received !== '' ) { hint += DIM_COLOR ( dimString + '(' ) + receivedColor ( received ) ; dimString = ')' ; } if ( promise !== '' ) { hint += DIM_COLOR ( dimString + '.' ) + promise ; dimString = '' ; } if ( isNot ) { hint += DIM_COLOR ( dimString + '.' ) + 'not' ; dimString = '' ; } if ( matcherName . includes ( '.' ) ) { dimString += matcherName ; } else { hint += DIM_COLOR ( dimString + '.' ) + matcherName ; dimString = '' ; } if ( expected === '' ) { dimString += '()' ; } else { hint += DIM_COLOR ( dimString + '(' ) + expectedColor ( expected ) ; if ( secondArgument ) { hint += DIM_COLOR ( ', ' ) + secondArgumentColor ( secondArgument ) ; } dimString = ')' ; } if ( comment !== '' ) { dimString += ' // ' + comment ; } if ( dimString !== '' ) { hint += DIM_COLOR ( dimString ) ; } return hint ; } ;