From 7c281719a98e1a69213d2b94dfc78640614a69eb Mon Sep 17 00:00:00 2001 From: Christian Bewernitz Date: Tue, 15 Feb 2022 23:11:27 +0100 Subject: [PATCH] feat: Add minimal `Object.assign` ponyfill (#379) since we can not rely on it being present in all supported runtimes. Even though the interface is the same as `Object.assign`, it behaves slightly differently from the one provided by browsers (see tests). This was extracted from #338 to support development in #367 --- lib/conventions.js | 26 +++++++++++++++++ test/conventions/assign.test.js | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 test/conventions/assign.test.js diff --git a/lib/conventions.js b/lib/conventions.js index 137353dd3..0db33da67 100644 --- a/lib/conventions.js +++ b/lib/conventions.js @@ -22,6 +22,31 @@ function freeze(object, oc) { return oc && typeof oc.freeze === 'function' ? oc.freeze(object) : object } +/** + * Since we can not rely on `Object.assign` we provide a simplified version + * that is sufficient for our needs. + * + * @param {Object} target + * @param {Object | null | undefined} source + * + * @returns {Object} target + * @throws TypeError if target is not an object + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + * @see https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.assign + */ +function assign(target, source) { + if (target === null || typeof target !== 'object') { + throw new TypeError('target is not an object') + } + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + return target +} + /** * All mime types that are allowed as input to `DOMParser.parseFromString` * @@ -139,6 +164,7 @@ var NAMESPACE = freeze({ XMLNS: 'http://www.w3.org/2000/xmlns/', }) +exports.assign = assign; exports.freeze = freeze; exports.MIME_TYPE = MIME_TYPE; exports.NAMESPACE = NAMESPACE; diff --git a/test/conventions/assign.test.js b/test/conventions/assign.test.js new file mode 100644 index 000000000..5aba47f9b --- /dev/null +++ b/test/conventions/assign.test.js @@ -0,0 +1,52 @@ +'use strict' +const { assign } = require('../../lib/conventions') + +describe('assign', () => { + test.each([null, undefined, true, false, 0, NaN])( + 'should throw when `target` is `%s`', + (target) => { + expect(() => assign(target, {})).toThrow(TypeError) + } + ) + test('should return target', () => { + const target = {} + expect(assign(target, undefined)).toBe(target) + }) + test('should copy all enumerable fields from source to target', () => { + const target = {} + const source = { a: 'A', 0: 0 } + + assign(target, source) + + expect(target).toEqual(source) + }) + test('should not copy prototype properties to source', () => { + const target = {} + function Clazz(yes) { + this.yes = yes + } + Clazz.prototype.dont = 5 + Clazz.prototype.hasOwnProperty = () => true + const source = new Clazz(1) + + assign(target, source) + + expect(target).toEqual({ yes: 1 }) + }) + test('should have no issue with null source', () => { + const target = {} + assign(target, null) + }) + test('should have no issue with undefined source', () => { + const target = {} + assign(target, undefined) + }) + test('should override existing keys', () => { + const target = { key: 4, same: 'same' } + const source = { key: undefined } + + assign(target, source) + + expect(target).toEqual({ key: undefined, same: 'same' }) + }) +})