diff --git a/packages/runner/src/utils/error.ts b/packages/runner/src/utils/error.ts index d52e98f26d2..45891976f8d 100644 --- a/packages/runner/src/utils/error.ts +++ b/packages/runner/src/utils/error.ts @@ -97,8 +97,8 @@ export function processError(err: any, options: DiffOptions = {}) { if (err.name) err.nameStr = String(err.name) - const clonedActual = deepClone(err.actual) - const clonedExpected = deepClone(err.expected) + const clonedActual = deepClone(err.actual, { forceWritable: true }) + const clonedExpected = deepClone(err.expected, { forceWritable: true }) const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected) diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index dd698bda3d9..9e3d0470c05 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -1,5 +1,9 @@ import type { Arrayable, Nullable } from './types' +interface CloneOptions { + forceWritable?: boolean +} + export function notNullish(v: T | null | undefined): v is NonNullable { return v != null } @@ -72,20 +76,28 @@ export function getOwnProperties(obj: any) { return Array.from(ownProps) } -export function deepClone(val: T): T { +const defaultCloneOptions: CloneOptions = { forceWritable: false } + +export function deepClone( + val: T, + options: CloneOptions = defaultCloneOptions, +): T { const seen = new WeakMap() - return clone(val, seen) + return clone(val, seen, options) } -export function clone(val: T, seen: WeakMap): T { +export function clone( + val: T, + seen: WeakMap, + options: CloneOptions = defaultCloneOptions, +): T { let k: any, out: any if (seen.has(val)) return seen.get(val) if (Array.isArray(val)) { - out = Array(k = val.length) + out = Array((k = val.length)) seen.set(val, out) - while (k--) - out[k] = clone(val[k], seen) + while (k--) out[k] = clone(val[k], seen) return out as any } @@ -110,6 +122,7 @@ export function clone(val: T, seen: WeakMap): T { else { Object.defineProperty(out, k, { ...descriptor, + writable: options.forceWritable ? true : descriptor.writable, value: cloned, }) } diff --git a/test/core/test/error.test.ts b/test/core/test/error.test.ts new file mode 100644 index 00000000000..eb186ea60a8 --- /dev/null +++ b/test/core/test/error.test.ts @@ -0,0 +1,24 @@ +import { processError } from '@vitest/runner/utils' +import { expect, test } from 'vitest' + +test('Can correctly process error where actual and expected contains non writable properties', () => { + const actual = {} + const expected = {} + Object.defineProperty(actual, 'root', { + value: { foo: 'bar' }, + writable: false, + enumerable: true, + }) + Object.defineProperty(expected, 'root', { + value: { foo: 'NOT BAR' }, + writable: false, + enumerable: true, + }) + + const err = { + actual, + expected, + } + + expect(() => processError(err)).not.toThrow(TypeError) +}) diff --git a/test/core/test/utils.spec.ts b/test/core/test/utils.spec.ts index ac62a948b76..f06679215fe 100644 --- a/test/core/test/utils.spec.ts +++ b/test/core/test/utils.spec.ts @@ -142,7 +142,20 @@ describe('deepClone', () => { value: 1, writable: false, }) + Object.defineProperty(objB, 'writableValue', { + configurable: false, + enumerable: false, + value: 1, + writable: true, + }) expect(deepClone(objB).value).toEqual(objB.value) + expect(Object.getOwnPropertyDescriptor(deepClone(objB), 'value')?.writable).toEqual(false) + expect( + Object.getOwnPropertyDescriptor(deepClone(objB), 'writableValue')?.writable, + ).toEqual(true) + expect( + Object.getOwnPropertyDescriptor(deepClone(objB, { forceWritable: true }), 'value')?.writable, + ).toEqual(true) const objC = Object.create(objB) expect(deepClone(objC).value).toEqual(objC.value) const objD: any = { name: 'd', ref: null }