From 6673209a38674b51a2760358bf6f7c5d2f09587d Mon Sep 17 00:00:00 2001 From: Nils Haberkamp Date: Thu, 30 Nov 2023 17:49:56 +0100 Subject: [PATCH] fix: ignore prototype methods when using setData on objects --- src/utils.ts | 26 ++++++++++- tests/utils.spec.ts | 105 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/utils.spec.ts diff --git a/src/utils.ts b/src/utils.ts index 7b65a7ec78..e70a673e1c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -72,6 +72,25 @@ export function mergeGlobalProperties( export const isObject = (obj: unknown): obj is Record => !!obj && typeof obj === 'object' +function isClass(obj: unknown) { + if (!(obj instanceof Object)) return + + const isCtorClass = + obj.constructor && obj.constructor.toString().substring(0, 5) === 'class' + + if (!('prototype' in obj)) { + return isCtorClass + } + + const prototype = obj.prototype as any + const isPrototypeCtorClass = + prototype.constructor && + prototype.constructor.toString && + prototype.constructor.toString().substring(0, 5) === 'class' + + return isCtorClass || isPrototypeCtorClass +} + // https://stackoverflow.com/a/48218209 export const mergeDeep = ( target: Record, @@ -80,8 +99,13 @@ export const mergeDeep = ( if (!isObject(target) || !isObject(source)) { return source } + Object.keys(source) - .concat(Object.getOwnPropertyNames(Object.getPrototypeOf(source) ?? {})) + .concat( + isClass(source) + ? Object.getOwnPropertyNames(Object.getPrototypeOf(source) ?? {}) + : Object.getOwnPropertyNames(source) + ) .forEach((key) => { const targetValue = target[key] const sourceValue = source[key] diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts new file mode 100644 index 0000000000..7678ce79f1 --- /dev/null +++ b/tests/utils.spec.ts @@ -0,0 +1,105 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { mergeDeep } from '../src/utils' + +describe('utils', () => { + it('should be possible to replace a primitive value with another', () => { + // ARRANGE + const state = { + foo: 'bar' + } + + // ACT + const result = mergeDeep(state, { + foo: true + }) + + // ASSERT + expect(result).toStrictEqual({ + foo: true + }) + }) + + it('should be possible to merge a nested object', () => { + // ARRANGE + const state = { + foo: { + bar: 'baz' + } + } + + // ACT + const result = mergeDeep(state, { + foo: { + bar: 'bar', + foo: 'foo' + } + }) + + // ASSERT + expect(result).toStrictEqual({ + foo: { + bar: 'bar', + foo: 'foo' + } + }) + }) + + it('should be possible to replace an array', () => { + // ARRANGE + const state = { + foo: [] + } + + // ACT + const result = mergeDeep(state, { + foo: ['bar', 'baz'] + }) + + // ASSERT + expect(result).toStrictEqual({ + foo: ['bar', 'baz'] + }) + }) + + it('should be possible to add additional key', () => { + // ARRANGE + const state = { + foo: 'bar' + } + + // ACT + const result = mergeDeep(state, { + baz: 'foo' + }) + + // ASSERT + expect(result).toStrictEqual({ + foo: 'bar', + baz: 'foo' + }) + }) + + it('should result in the same value as before merging', () => { + // ARRANGE + const state = { + foo: [], + bar: [] + } + + // ACT + const result = mergeDeep(state, { + foo: [], + bar: [] + }) + + // ASSERT + expect(result).toStrictEqual({ + foo: [], + bar: [] + }) + }) +})