diff --git a/src/helpers/helpers.config.js b/src/helpers/helpers.config.js index 2f61a7915..65c415b80 100644 --- a/src/helpers/helpers.config.js +++ b/src/helpers/helpers.config.js @@ -15,19 +15,46 @@ export function _createResolver(scopes, prefixes = ['']) { override: (scope) => _createResolver([scope].concat(scopes), prefixes), }; return new Proxy(cache, { + /** + * A trap for getting property values. + */ get(target, prop) { return _cached(target, prop, () => _resolveWithPrefixes(prop, prefixes, scopes)); }, - ownKeys(target) { - return getKeysFromAllScopes(target); - }, - + /** + * A trap for Object.getOwnPropertyDescriptor. + * Also used by Object.hasOwnProperty. + */ getOwnPropertyDescriptor(target, prop) { return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop); }, + /** + * A trap for Object.getPrototypeOf. + */ + getPrototypeOf() { + return Reflect.getPrototypeOf(scopes[0]); + }, + + /** + * A trap for the in operator. + */ + has(target, prop) { + return getKeysFromAllScopes(target).includes(prop); + }, + + /** + * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. + */ + ownKeys(target) { + return getKeysFromAllScopes(target); + }, + + /** + * A trap for setting property values. + */ set(target, prop, value) { scopes[0][prop] = value; return delete target[prop]; @@ -54,19 +81,46 @@ export function _attachContext(proxy, context, subProxy) { override: (scope) => _attachContext(proxy.override(scope), context, subProxy) }; return new Proxy(cache, { + /** + * A trap for getting property values. + */ get(target, prop, receiver) { return _cached(target, prop, () => _resolveWithContext(target, prop, receiver)); }, + /** + * A trap for Object.getOwnPropertyDescriptor. + * Also used by Object.hasOwnProperty. + */ + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(proxy, prop); + }, + + /** + * A trap for Object.getPrototypeOf. + */ + getPrototypeOf() { + return Reflect.getPrototypeOf(proxy); + }, + + /** + * A trap for the in operator. + */ + has(target, prop) { + return Reflect.has(proxy, prop); + }, + + /** + * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. + */ ownKeys() { return Reflect.ownKeys(proxy); }, - getOwnPropertyDescriptor(target, prop) { - return Reflect.getOwnPropertyDescriptor(proxy._scopes[0], prop); - }, - + /** + * A trap for setting property values. + */ set(target, prop, value) { proxy[prop] = value; return delete target[prop]; diff --git a/test/specs/helpers.config.tests.js b/test/specs/helpers.config.tests.js index 37213dc5e..2075af5e9 100644 --- a/test/specs/helpers.config.tests.js +++ b/test/specs/helpers.config.tests.js @@ -88,6 +88,41 @@ describe('Chart.helpers.config', function() { option3: 'defaults3' }); }); + + it('should support common object methods', function() { + const defaults = { + option1: 'defaults' + }; + class Options { + constructor() { + this.option2 = 'options'; + } + get getter() { + return 'options getter'; + } + } + const options = new Options(); + + const resolver = _createResolver([options, defaults]); + + expect(Object.prototype.hasOwnProperty.call(resolver, 'option2')).toBeTrue(); + + expect(Object.prototype.hasOwnProperty.call(resolver, 'option1')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(resolver, 'getter')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(resolver, 'nonexistent')).toBeFalse(); + + expect(Object.keys(resolver)).toEqual(['option2']); + expect(Object.getOwnPropertyNames(resolver)).toEqual(['option2', 'option1']); + + expect('option2' in resolver).toBeTrue(); + expect('option1' in resolver).toBeTrue(); + expect('getter' in resolver).toBeFalse(); + expect('nonexistent' in resolver).toBeFalse(); + + expect(resolver instanceof Options).toBeTrue(); + + expect(resolver.getter).toEqual('options getter'); + }); }); describe('_attachContext', function() { @@ -249,6 +284,41 @@ describe('Chart.helpers.config', function() { expect(opts.fn).toEqual(1); }); + it('should support common object methods', function() { + const defaults = { + option1: 'defaults' + }; + class Options { + constructor() { + this.option2 = () => 'options'; + } + get getter() { + return 'options getter'; + } + } + const options = new Options(); + const resolver = _createResolver([options, defaults]); + const opts = _attachContext(resolver, {index: 1}); + + expect(Object.prototype.hasOwnProperty.call(opts, 'option2')).toBeTrue(); + + expect(Object.prototype.hasOwnProperty.call(opts, 'option1')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(opts, 'getter')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(opts, 'nonexistent')).toBeFalse(); + + expect(Object.keys(opts)).toEqual(['option2']); + expect(Object.getOwnPropertyNames(opts)).toEqual(['option2', 'option1']); + + expect('option2' in opts).toBeTrue(); + expect('option1' in opts).toBeTrue(); + expect('getter' in opts).toBeFalse(); + expect('nonexistent' in opts).toBeFalse(); + + expect(opts instanceof Options).toBeTrue(); + + expect(opts.getter).toEqual('options getter'); + }); + describe('_indexable and _scriptable', function() { it('should default to true', function() { const options = {