Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolve circle ref when cloning object #1444

Merged
merged 1 commit into from Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 10 additions & 4 deletions 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({})

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
21 changes: 13 additions & 8 deletions packages/vitest/src/utils/base.ts
Expand Up @@ -51,25 +51,30 @@ function getOwnProperties(obj: any) {
return Array.from(ownProps)
}

export function clone<T>(val: T): T {
let k: any, out: any, tmp: any
export function deepClone<T>(val: T): T {
const seen = new WeakMap()
return clone(val, seen)
}

export function clone<T>(val: T, seen: WeakMap<any, any>): 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
}

Expand Down
3 changes: 3 additions & 0 deletions test/core/test/replace-matcher.test.ts
Expand Up @@ -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)
})
})
25 changes: 14 additions & 11 deletions 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', () => {
Expand Down Expand Up @@ -122,25 +122,28 @@ 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,
enumerable: false,
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)
})
})