import type { Formatter } from 'tinyrainbow' import type { MatcherHintOptions, Tester } from './types' import { diff, printDiffOrStringify } from '@vitest/utils/diff' import { stringify } from '@vitest/utils/display' import { getType } from '@vitest/utils/helpers' import c from 'tinyrainbow' import { JEST_MATCHERS_OBJECT } from './constants' export { diff } from '@vitest/utils/diff' export { stringify } const EXPECTED_COLOR = c.green const RECEIVED_COLOR = c.red const INVERTED_COLOR = c.inverse const BOLD_WEIGHT = c.bold const DIM_COLOR = c.dim function matcherHint( matcherName: string, received = 'received', expected = 'expected', options: MatcherHintOptions = {}, ): string { const { comment = '', isDirectExpectCall = false, // seems redundant with received === '' isNot = false, promise = '', secondArgument = '', expectedColor = EXPECTED_COLOR, receivedColor = RECEIVED_COLOR, secondArgumentColor = EXPECTED_COLOR, } = options let hint = '' let dimString = 'expect' // concatenate adjacent dim substrings 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('.')) { // Old format: for backward compatibility, // especially without promise or isNot options dimString += matcherName } else { // New format: omit period from matcherName arg 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 } const SPACE_SYMBOL = '\u{00B7}' // middle dot // Instead of inverse highlight which now implies a change, // replace common spaces with middle dot at the end of any line. function replaceTrailingSpaces(text: string): string { return text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length)) } function printReceived(object: unknown): string { return RECEIVED_COLOR(replaceTrailingSpaces(stringify(object))) } function printExpected(value: unknown): string { return EXPECTED_COLOR(replaceTrailingSpaces(stringify(value))) } export function getMatcherUtils(): { EXPECTED_COLOR: Formatter RECEIVED_COLOR: Formatter INVERTED_COLOR: Formatter BOLD_WEIGHT: Formatter DIM_COLOR: Formatter diff: typeof diff matcherHint: typeof matcherHint printReceived: typeof printReceived printExpected: typeof printExpected printDiffOrStringify: typeof printDiffOrStringify printWithType: typeof printWithType } { return { EXPECTED_COLOR, RECEIVED_COLOR, INVERTED_COLOR, BOLD_WEIGHT, DIM_COLOR, diff, matcherHint, printReceived, printExpected, printDiffOrStringify, printWithType, } } export function printWithType( name: string, value: T, print: (value: T) => string, ): 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 function addCustomEqualityTesters(newTesters: Array): void { if (!Array.isArray(newTesters)) { throw new TypeError( `expect.customEqualityTesters: Must be set to an array of Testers. Was given "${getType( newTesters, )}"`, ) } (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters.push( ...newTesters, ) } export function getCustomEqualityTesters(): Array { return (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters }