Skip to content

Commit

Permalink
fix: diff output is incorrectly when using expect.any (#1197)
Browse files Browse the repository at this point in the history
  • Loading branch information
nieyuyao committed May 2, 2022
1 parent 4956713 commit d09de8f
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 40 deletions.
51 changes: 50 additions & 1 deletion packages/vitest/src/runtime/error.ts
@@ -1,5 +1,7 @@
import { format } from 'util'
import { util } from 'chai'
import { stringify } from '../integrations/chai/jest-matcher-utils'
import { clone, getType } from '../utils'

const OBJECT_PROTO = Object.getPrototypeOf({})

Expand Down Expand Up @@ -57,15 +59,62 @@ export function processError(err: any) {
if (err.name)
err.nameStr = String(err.name)

const clonedActual = clone(err.actual)
const clonedExpected = clone(err.expected)

const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected)

err.actual = replacedActual
err.expected = replacedExpected

if (typeof err.expected !== 'string')
err.expected = stringify(err.expected)
if (typeof err.actual !== 'string')
err.actual = stringify(err.actual)

try {
return serializeError(err)
}
catch (e: any) {
return serializeError(new Error(`Failed to fully serialize error: ${e?.message}.\nInner error message: ${err?.message}`))
}
}

function isAsymmetricMatcher(data: any) {
const type = getType(data)
return type === 'Object' && typeof data.asymmetricMatch === 'function'
}

function isReplaceable(obj1: any, obj2: any) {
const obj1Type = getType(obj1)
const obj2Type = getType(obj2)
return obj1Type === obj2Type && obj1Type === 'Object'
}

export function replaceAsymmetricMatcher(actual: any, expected: any) {
if (!isReplaceable(actual, expected))
return { replacedActual: actual, replacedExpected: expected }
util.getOwnEnumerableProperties(expected).forEach((key) => {
const expectedValue = expected[key]
const actualValue = actual[key]
if (isAsymmetricMatcher(expectedValue)) {
if (expectedValue.asymmetricMatch(actualValue))
actual[key] = expectedValue
}
else if (isAsymmetricMatcher(actualValue)) {
if (actualValue.asymmetricMatch(expectedValue))
expected[key] = actualValue
}
else if (isReplaceable(actualValue, expectedValue)) {
const replaced = replaceAsymmetricMatcher(
actualValue,
expectedValue,
)
actual[key] = replaced.replacedActual
expected[key] = replaced.replacedExpected
}
})
return {
replacedActual: actual,
replacedExpected: expected,
}
}
28 changes: 3 additions & 25 deletions packages/vitest/src/runtime/mocker.ts
Expand Up @@ -3,35 +3,13 @@ import { isNodeBuiltin } from 'mlly'
import { basename, dirname, resolve } from 'pathe'
import { normalizeRequestId, toFilePath } from 'vite-node/utils'
import type { ModuleCacheMap } from 'vite-node/client'
import { getWorkerState, isWindows, mergeSlashes, slash } from '../utils'
import { getAllProperties, getType, getWorkerState, isWindows, mergeSlashes, slash } from '../utils'
import { distDir } from '../constants'
import type { PendingSuiteMock } from '../types/mocker'
import type { ExecuteOptions } from './execute'

type Callback = (...args: any[]) => unknown

function getType(value: unknown): string {
return Object.prototype.toString.apply(value).slice(8, -1)
}

function getAllProperties(obj: any) {
const allProps = new Set<string | symbol>()
let curr = obj
do {
// we don't need propterties from these
if (curr === Object.prototype || curr === Function.prototype || curr === RegExp.prototype)
break
const props = Object.getOwnPropertyNames(curr)
const symbs = Object.getOwnPropertySymbols(curr)

props.forEach(prop => allProps.add(prop))
symbs.forEach(symb => allProps.add(symb))

// eslint-disable-next-line no-cond-assign
} while (curr = Object.getPrototypeOf(curr))
return Array.from(allProps)
}

export class VitestMocker {
private static pendingIds: PendingSuiteMock[] = []
private static spyModule?: typeof import('../integrations/spy')
Expand Down Expand Up @@ -174,9 +152,9 @@ export class VitestMocker {

const newObj: Record<string | symbol, any> = {}

const proproperties = getAllProperties(value)
const properties = getAllProperties(value)

for (const k of proproperties) {
for (const k of properties) {
newObj[k] = this.mockValue(value[k])
const type = getType(value[k])

Expand Down
41 changes: 27 additions & 14 deletions packages/vitest/src/utils/base.ts
@@ -1,5 +1,23 @@
import type { Arrayable, DeepMerge, Nullable } from '../types'

export function getAllProperties(obj: any) {
const allProps = new Set<string | symbol>()
let curr = obj
do {
// we don't need propterties from these
if (curr === Object.prototype || curr === Function.prototype || curr === RegExp.prototype)
break
const props = Object.getOwnPropertyNames(curr)
const symbs = Object.getOwnPropertySymbols(curr)

props.forEach(prop => allProps.add(prop))
symbs.forEach(symb => allProps.add(symb))

// eslint-disable-next-line no-cond-assign
} while (curr = Object.getPrototypeOf(curr))
return Array.from(allProps)
}

export function notNullish<T>(v: T | null | undefined): v is NonNullable<T> {
return v != null
}
Expand All @@ -14,6 +32,10 @@ export function mergeSlashes(str: string) {

export const noop = () => { }

export function getType(value: unknown): string {
return Object.prototype.toString.apply(value).slice(8, -1)
}

export function clone<T>(val: T): T {
let k: any, out: any, tmp: any

Expand All @@ -26,20 +48,11 @@ export function clone<T>(val: T): T {
}

if (Object.prototype.toString.call(val) === '[object Object]') {
out = {} // null
for (k in val) {
if (k === '__proto__') {
Object.defineProperty(out, k, {
value: clone((val as any)[k]),
configurable: true,
enumerable: true,
writable: true,
})
}
else {
// eslint-disable-next-line no-cond-assign
out[k] = (tmp = (val as any)[k]) && typeof tmp === 'object' ? clone(tmp) : tmp
}
out = Object.create(Object.getPrototypeOf(val))
const props = getAllProperties(val)
for (const k of props) {
// eslint-disable-next-line no-cond-assign
out[k] = (tmp = (val as any)[k]) && typeof tmp === 'object' ? clone(tmp) : tmp
}
return out
}
Expand Down
49 changes: 49 additions & 0 deletions test/core/test/replace-matcher.test.ts
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest'
import { replaceAsymmetricMatcher } from '../../../packages/vitest/src/runtime/error'

describe('replace asymmetric matcher', () => {
const expectReplaceAsymmetricMatcher = (actual: any, expected: any) => {
const replaced = replaceAsymmetricMatcher(actual, expected)
expect(replaced.replacedActual).toEqual(replaced.replacedExpected)
}
it('should work when various types are passed in', () => {
expectReplaceAsymmetricMatcher(null, null)
expectReplaceAsymmetricMatcher(undefined, undefined)
expectReplaceAsymmetricMatcher({}, {})
expectReplaceAsymmetricMatcher([1, 2], [1, 2])
expectReplaceAsymmetricMatcher({}, expect.any(Object))
expectReplaceAsymmetricMatcher(() => {}, expect.any(Function))
expectReplaceAsymmetricMatcher(Promise, expect.any(Function))
expectReplaceAsymmetricMatcher(false, expect.any(Boolean))
expectReplaceAsymmetricMatcher([1, 2], [1, expect.any(Number)])
expectReplaceAsymmetricMatcher(false, expect.anything())
expectReplaceAsymmetricMatcher({}, expect.anything())
expectReplaceAsymmetricMatcher(Symbol, expect.anything())
expectReplaceAsymmetricMatcher(Promise, expect.anything())
expectReplaceAsymmetricMatcher(new Map([['a', 1]]), expect.anything())
expectReplaceAsymmetricMatcher(new Set([1, 2]), expect.anything())
expectReplaceAsymmetricMatcher(new ArrayBuffer(8), expect.anything())
expectReplaceAsymmetricMatcher([1, 2], [1, expect.anything()])
expectReplaceAsymmetricMatcher({
str: 'a',
arr: [1, 2],
}, {
str: expect.any(String),
arr: expect.anything(),
})
expectReplaceAsymmetricMatcher({
str: expect.any(String),
arr: expect.anything(),
}, {
str: expect.any(String),
arr: expect.anything(),
})
expectReplaceAsymmetricMatcher({
str: 'a',
arr: [1, 2],
}, {
str: expect.any(String),
arr: [1, expect.anything()],
})
})
})

0 comments on commit d09de8f

Please sign in to comment.