Skip to content

Commit

Permalink
feat: show all spy calls on assertion error (#1091)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Apr 3, 2022
1 parent bec43fc commit b027d16
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 43 deletions.
94 changes: 67 additions & 27 deletions packages/vitest/src/integrations/chai/jest-expect.ts
@@ -1,11 +1,14 @@
import c from 'picocolors'
import type { EnhancedSpy } from '../spy'
import { isMockFunction } from '../spy'
import { addSerializer } from '../snapshot/port/plugins'
import type { Constructable } from '../../types'
import { assertTypes } from '../../utils'
import { unifiedDiff } from '../../node/diff'
import type { ChaiPlugin, MatcherState } from './types'
import { arrayBufferEquality, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
import { stringify } from './jest-matcher-utils'

const MATCHERS_OBJECT = Symbol.for('matchers-object')

Expand Down Expand Up @@ -285,6 +288,35 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
assertIsMock(assertion)
return assertion._obj as EnhancedSpy
}
const ordinalOf = (i: number) => {
const j = i % 10
const k = i % 100

if (j === 1 && k !== 11)
return `${i}st`

if (j === 2 && k !== 12)
return `${i}nd`

if (j === 3 && k !== 13)
return `${i}rd`

return `${i}th`
}
const formatCalls = (spy: EnhancedSpy, msg: string, actualCall?: any) => {
msg += c.gray(`\n\nReceived: \n${spy.mock.calls.map((callArg, i) => {
let methodCall = c.bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call:\n\n`)
if (actualCall)
methodCall += unifiedDiff(stringify(callArg), stringify(actualCall), { showLegend: false })
else
methodCall += stringify(callArg).split('\n').map(line => ` ${line}`).join('\n')
methodCall += '\n'
return methodCall
}).join('\n')}`)
msg += c.gray(`\n\nNumber of calls: ${c.bold(spy.mock.calls.length)}\n`)
return msg
}
def(['toHaveBeenCalledTimes', 'toBeCalledTimes'], function(number: number) {
const spy = getSpy(this)
const spyName = spy.getMockName()
Expand Down Expand Up @@ -313,41 +345,49 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
const spy = getSpy(this)
const spyName = spy.getMockName()
const called = spy.mock.calls.length > 0
return this.assert(
called,
`expected "${spyName}" to be called at least once`,
`expected "${spyName}" to not be called at all`,
true,
called,
const isNot = utils.flag(this, 'negate') as boolean
let msg = utils.getMessage(
this,
[
called,
`expected "${spyName}" to be called at least once`,
`expected "${spyName}" to not be called at all`,
true,
called,
],
)
if (called && isNot)
msg += formatCalls(spy, msg)

if ((called && isNot) || (!called && !isNot)) {
const err = new Error(msg)
err.name = 'AssertionError'
throw err
}
})
def(['toHaveBeenCalledWith', 'toBeCalledWith'], function(...args) {
const spy = getSpy(this)
const spyName = spy.getMockName()
const pass = spy.mock.calls.some(callArg => jestEquals(callArg, args, [iterableEquality]))
return this.assert(
pass,
`expected "${spyName}" to be called with arguments: #{exp}`,
`expected "${spyName}" to not be called with arguments: #{exp}`,
args,
spy.mock.calls,
)
})
const ordinalOf = (i: number) => {
const j = i % 10
const k = i % 100

if (j === 1 && k !== 11)
return `${i}st`

if (j === 2 && k !== 12)
return `${i}nd`
const isNot = utils.flag(this, 'negate') as boolean

if (j === 3 && k !== 13)
return `${i}rd`
let msg = utils.getMessage(
this,
[
pass,
`expected "${spyName}" to be called with arguments: #{exp}`,
`expected "${spyName}" to not be called with arguments: #{exp}`,
args,
],
)

return `${i}th`
}
if ((pass && isNot) || (!pass && !isNot)) {
msg += formatCalls(spy, msg, args)
const err = new Error(msg)
err.name = 'AssertionError'
throw err
}
})
def(['toHaveBeenNthCalledWith', 'nthCalledWith'], function(times: number, ...args: any[]) {
const spy = getSpy(this)
const spyName = spy.getMockName()
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/integrations/chai/jest-matcher-utils.ts
Expand Up @@ -3,6 +3,7 @@

import c from 'picocolors'
import type { Formatter } from 'picocolors/types'
import type { PrettyFormatOptions } from 'pretty-format'
import { format as prettyFormat, plugins as prettyFormatPlugins } from 'pretty-format'
import { unifiedDiff } from '../../node/diff'

Expand Down Expand Up @@ -112,7 +113,7 @@ const SPACE_SYMBOL = '\u{00B7}' // middle dot
const replaceTrailingSpaces = (text: string): string =>
text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length))

export const stringify = (object: unknown, maxDepth = 10): string => {
export const stringify = (object: unknown, maxDepth = 10, options?: PrettyFormatOptions): string => {
const MAX_LENGTH = 10000
let result

Expand All @@ -121,6 +122,7 @@ export const stringify = (object: unknown, maxDepth = 10): string => {
maxDepth,
// min: true,
plugins: PLUGINS,
...options,
})
}
catch {
Expand All @@ -129,6 +131,7 @@ export const stringify = (object: unknown, maxDepth = 10): string => {
maxDepth,
// min: true,
plugins: PLUGINS,
...options,
})
}

Expand Down
37 changes: 23 additions & 14 deletions packages/vitest/src/node/diff.ts
Expand Up @@ -6,6 +6,11 @@ export function formatLine(line: string, outputTruncateLength?: number) {
return cliTruncate(line, (outputTruncateLength ?? (process.stdout.columns || 80)) - 4)
}

export interface DiffOptions {
outputTruncateLength?: number
showLegend?: boolean
}

/**
* Returns unified diff between two strings with coloured ANSI output.
*
Expand All @@ -15,10 +20,12 @@ export function formatLine(line: string, outputTruncateLength?: number) {
* @return {string} The diff.
*/

export function unifiedDiff(actual: string, expected: string, outputTruncateLength?: number) {
export function unifiedDiff(actual: string, expected: string, options: DiffOptions = {}) {
if (actual === expected)
return ''

const { outputTruncateLength, showLegend = true } = options

const indent = ' '
const diffLimit = 15

Expand Down Expand Up @@ -70,19 +77,21 @@ export function unifiedDiff(actual: string, expected: string, outputTruncateLeng
return ` ${line}`
})

// Compact mode
if (isCompact) {
formatted = [
`${c.green('- Expected')} ${formatted[0]}`,
`${c.red('+ Received')} ${formatted[1]}`,
]
}
else {
formatted.unshift(
c.green(`- Expected - ${counts['-']}`),
c.red(`+ Received + ${counts['+']}`),
'',
)
if (showLegend) {
// Compact mode
if (isCompact) {
formatted = [
`${c.green('- Expected')} ${formatted[0]}`,
`${c.red('+ Received')} ${formatted[1]}`,
]
}
else {
formatted.unshift(
c.green(`- Expected - ${counts['-']}`),
c.red(`+ Received + ${counts['+']}`),
'',
)
}
}

return formatted.map(i => indent + i).join('\n')
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/error.ts
Expand Up @@ -86,7 +86,7 @@ function handleImportOutsideModuleError(stack: string, ctx: Vitest) {
}

function displayDiff(actual: string, expected: string, console: Console, outputTruncateLength?: number) {
console.error(c.gray(unifiedDiff(actual, expected, outputTruncateLength)) + '\n')
console.error(c.gray(unifiedDiff(actual, expected, { outputTruncateLength })) + '\n')
}

function printErrorMessage(error: ErrorWithDiff, console: Console) {
Expand Down

0 comments on commit b027d16

Please sign in to comment.