diff --git a/packages/vitest/src/runtime/error.ts b/packages/vitest/src/runtime/error.ts index 2d7deeb7fe51..a4f559dfef44 100644 --- a/packages/vitest/src/runtime/error.ts +++ b/packages/vitest/src/runtime/error.ts @@ -1,7 +1,7 @@ import { format } from 'util' import { util } from 'chai' import { stringify } from '../integrations/chai/jest-matcher-utils' -import { clone, getType } from '../utils' +import { deepClone, getType } from '../utils' const OBJECT_PROTO = Object.getPrototypeOf({}) @@ -63,8 +63,8 @@ export function processError(err: any) { if (err.name) err.nameStr = String(err.name) - const clonedActual = clone(err.actual) - const clonedExpected = clone(err.expected) + const clonedActual = deepClone(err.actual) + const clonedExpected = deepClone(err.expected) const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected) @@ -105,9 +105,13 @@ function isReplaceable(obj1: any, obj2: any) { return obj1Type === obj2Type && obj1Type === 'Object' } -export function replaceAsymmetricMatcher(actual: any, expected: any) { +export function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced = new WeakMap(), expectedReplaced = new WeakMap()) { if (!isReplaceable(actual, expected)) return { replacedActual: actual, replacedExpected: expected } + if (actualReplaced.has(actual) || expectedReplaced.has(expected)) + return { replacedActual: actual, replacedExpected: expected } + actualReplaced.set(actual, true) + expectedReplaced.set(expected, true) util.getOwnEnumerableProperties(expected).forEach((key) => { const expectedValue = expected[key] const actualValue = actual[key] @@ -123,6 +127,8 @@ export function replaceAsymmetricMatcher(actual: any, expected: any) { const replaced = replaceAsymmetricMatcher( actualValue, expectedValue, + actualReplaced, + expectedReplaced, ) actual[key] = replaced.replacedActual expected[key] = replaced.replacedExpected diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index b82d6fd9edd3..f872c3d74372 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -51,25 +51,30 @@ function getOwnProperties(obj: any) { return Array.from(ownProps) } -export function clone(val: T): T { - let k: any, out: any, tmp: any +export function deepClone(val: T): T { + const seen = new WeakMap() + return clone(val, seen) +} +export function clone(val: T, seen: WeakMap): T { + let k: any, out: any + if (seen.has(val)) + return seen.get(val) if (Array.isArray(val)) { out = Array(k = val.length) + seen.set(val, out) while (k--) - // eslint-disable-next-line no-cond-assign - out[k] = (tmp = val[k]) && typeof tmp === 'object' ? clone(tmp) : tmp + out[k] = clone(val[k], seen) return out as any } if (Object.prototype.toString.call(val) === '[object Object]') { out = Object.create(Object.getPrototypeOf(val)) + seen.set(val, out) // we don't need properties from prototype const props = getOwnProperties(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 - } + for (const k of props) + out[k] = clone((val as any)[k], seen) return out } diff --git a/test/core/test/replace-matcher.test.ts b/test/core/test/replace-matcher.test.ts index d61a461fa47a..c4aa53e9a94e 100644 --- a/test/core/test/replace-matcher.test.ts +++ b/test/core/test/replace-matcher.test.ts @@ -45,5 +45,8 @@ describe('replace asymmetric matcher', () => { str: expect.any(String), arr: [1, expect.anything()], }) + const circleObj: any = { name: 'circle', ref: null } + circleObj.ref = circleObj + expectReplaceAsymmetricMatcher(circleObj, circleObj) }) }) diff --git a/test/core/test/utils.spec.ts b/test/core/test/utils.spec.ts index 6135e1a024f3..e82880d93032 100644 --- a/test/core/test/utils.spec.ts +++ b/test/core/test/utils.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { assertTypes, clone, deepMerge, toArray } from '../../../packages/vitest/src/utils' +import { assertTypes, deepClone, deepMerge, toArray } from '../../../packages/vitest/src/utils' import { deepMergeSnapshot } from '../../../packages/vitest/src/integrations/snapshot/port/utils' describe('assertTypes', () => { @@ -122,16 +122,16 @@ describe('toArray', () => { }) }) -describe('clone', () => { +describe('deepClone', () => { test('various types should be cloned correctly', () => { - expect(clone(1)).toBe(1) - expect(clone(true)).toBe(true) - expect(clone(undefined)).toBe(undefined) - expect(clone(null)).toBe(null) - expect(clone({ a: 1 })).toEqual({ a: 1 }) - expect(clone([1, 2])).toEqual([1, 2]) + expect(deepClone(1)).toBe(1) + expect(deepClone(true)).toBe(true) + expect(deepClone(undefined)).toBe(undefined) + expect(deepClone(null)).toBe(null) + expect(deepClone({ a: 1 })).toEqual({ a: 1 }) + expect(deepClone([1, 2])).toEqual([1, 2]) const symbolA = Symbol('a') - expect(clone(symbolA)).toBe(symbolA) + expect(deepClone(symbolA)).toBe(symbolA) const objB: any = {} Object.defineProperty(objB, 'value', { configurable: false, @@ -139,8 +139,11 @@ describe('clone', () => { value: 1, writable: false, }) - expect(clone(objB).value).toEqual(objB.value) + expect(deepClone(objB).value).toEqual(objB.value) const objC = Object.create(objB) - expect(clone(objC).value).toEqual(objC.value) + expect(deepClone(objC).value).toEqual(objC.value) + const objD: any = { name: 'd', ref: null } + objD.ref = objD + expect(deepClone(objD)).toEqual(objD) }) })