import { DEBUG } from '@glimmer/env'; import { get, set, watch, unwatch } from '../..'; import { meta as metaFor } from 'ember-meta'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; function hasMandatorySetter(object, property) { try { return Object.getOwnPropertyDescriptor(object, property).set.isMandatorySetter === true; } catch (e) { return false; } } function hasMetaValue(object, property) { return metaFor(object).peekValues(property) !== undefined; } if (DEBUG) { moduleFor( 'mandory-setters', class extends AbstractTestCase { ['@test does not assert if property is not being watched'](assert) { let obj = { someProp: null, toString() { return 'custom-object'; }, }; obj.someProp = 'blastix'; assert.equal(get(obj, 'someProp'), 'blastix'); } ['@test should not setup mandatory-setter if property is not writable'](assert) { assert.expect(6); let obj = {}; Object.defineProperty(obj, 'a', { value: true }); Object.defineProperty(obj, 'b', { value: false }); Object.defineProperty(obj, 'c', { value: undefined }); Object.defineProperty(obj, 'd', { value: undefined, writable: false }); Object.defineProperty(obj, 'e', { value: undefined, configurable: false, }); Object.defineProperty(obj, 'f', { value: undefined, configurable: true, }); watch(obj, 'a'); watch(obj, 'b'); watch(obj, 'c'); watch(obj, 'd'); watch(obj, 'e'); watch(obj, 'f'); assert.ok(!hasMandatorySetter(obj, 'a'), 'mandatory-setter should not be installed'); assert.ok(!hasMandatorySetter(obj, 'b'), 'mandatory-setter should not be installed'); assert.ok(!hasMandatorySetter(obj, 'c'), 'mandatory-setter should not be installed'); assert.ok(!hasMandatorySetter(obj, 'd'), 'mandatory-setter should not be installed'); assert.ok(!hasMandatorySetter(obj, 'e'), 'mandatory-setter should not be installed'); assert.ok(!hasMandatorySetter(obj, 'f'), 'mandatory-setter should not be installed'); } ['@test should not teardown non mandatory-setter descriptor'](assert) { assert.expect(1); let obj = { get a() { return 'hi'; }, }; watch(obj, 'a'); unwatch(obj, 'a'); assert.equal(obj.a, 'hi'); } ['@test should not confuse non descriptor watched gets'](assert) { assert.expect(2); let obj = { get a() { return 'hi'; }, }; watch(obj, 'a'); assert.equal(get(obj, 'a'), 'hi'); assert.equal(obj.a, 'hi'); } ['@test should not setup mandatory-setter if setter is already setup on property'](assert) { assert.expect(2); let obj = { someProp: null }; Object.defineProperty(obj, 'someProp', { get() { return null; }, set(value) { assert.equal(value, 'foo-bar', 'custom setter was called'); }, }); watch(obj, 'someProp'); assert.ok(!hasMandatorySetter(obj, 'someProp'), 'mandatory-setter should not be installed'); obj.someProp = 'foo-bar'; } ['@test watched ES5 setter should not be smashed by mandatory setter'](assert) { let value; let obj = { get foo() {}, set foo(_value) { value = _value; }, }; watch(obj, 'foo'); set(obj, 'foo', 2); assert.equal(value, 2); } ['@test should not setup mandatory-setter if setter is already setup on property in parent prototype']( assert ) { assert.expect(2); function Foo() {} Object.defineProperty(Foo.prototype, 'someProp', { get() { return null; }, set(value) { assert.equal(value, 'foo-bar', 'custom setter was called'); }, }); let obj = new Foo(); watch(obj, 'someProp'); assert.ok(!hasMandatorySetter(obj, 'someProp'), 'mandatory-setter should not be installed'); obj.someProp = 'foo-bar'; } ['@test should not setup mandatory-setter if setter is already setup on property in grandparent prototype']( assert ) { assert.expect(2); function Foo() {} Object.defineProperty(Foo.prototype, 'someProp', { get() { return null; }, set(value) { assert.equal(value, 'foo-bar', 'custom setter was called'); }, }); function Bar() {} Bar.prototype = Object.create(Foo.prototype); Bar.prototype.constructor = Bar; let obj = new Bar(); watch(obj, 'someProp'); assert.ok(!hasMandatorySetter(obj, 'someProp'), 'mandatory-setter should not be installed'); obj.someProp = 'foo-bar'; } ['@test should not setup mandatory-setter if setter is already setup on property in great grandparent prototype']( assert ) { assert.expect(2); function Foo() {} Object.defineProperty(Foo.prototype, 'someProp', { get() { return null; }, set(value) { assert.equal(value, 'foo-bar', 'custom setter was called'); }, }); function Bar() {} Bar.prototype = Object.create(Foo.prototype); Bar.prototype.constructor = Bar; function Qux() {} Qux.prototype = Object.create(Bar.prototype); Qux.prototype.constructor = Qux; let obj = new Qux(); watch(obj, 'someProp'); assert.ok(!hasMandatorySetter(obj, 'someProp'), 'mandatory-setter should not be installed'); obj.someProp = 'foo-bar'; } ['@test should assert if set without set when property is being watched']() { let obj = { someProp: null, toString() { return 'custom-object'; }, }; watch(obj, 'someProp'); expectAssertion(function() { obj.someProp = 'foo-bar'; }, 'You must use set() to set the `someProp` property (of custom-object) to `foo-bar`.'); } ['@test should not assert if set with set when property is being watched'](assert) { let obj = { someProp: null, toString() { return 'custom-object'; }, }; watch(obj, 'someProp'); set(obj, 'someProp', 'foo-bar'); assert.equal(get(obj, 'someProp'), 'foo-bar'); } ['@test does not setup mandatory-setter if non-configurable'](assert) { let obj = { someProp: null, toString() { return 'custom-object'; }, }; Object.defineProperty(obj, 'someProp', { configurable: false, enumerable: true, value: 'blastix', }); watch(obj, 'someProp'); assert.ok(!hasMandatorySetter(obj, 'someProp'), 'blastix'); } ['@test ensure after watch the property is restored (and the value is no-longer stored in meta) [non-enumerable]']( assert ) { let obj = { someProp: null, toString() { return 'custom-object'; }, }; Object.defineProperty(obj, 'someProp', { configurable: true, enumerable: false, value: 'blastix', }); watch(obj, 'someProp'); assert.equal(hasMandatorySetter(obj, 'someProp'), true, 'should have a mandatory setter'); let descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal(descriptor.enumerable, false, 'property should remain non-enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(obj.someProp, 'blastix', 'expected value to be the getter'); assert.equal(descriptor.value, undefined, 'expected existing value to NOT remain'); assert.ok(hasMetaValue(obj, 'someProp'), 'someProp is stored in meta.values'); unwatch(obj, 'someProp'); assert.ok(!hasMetaValue(obj, 'someProp'), 'someProp is no longer stored in meta.values'); descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal( hasMandatorySetter(obj, 'someProp'), false, 'should no longer have a mandatory setter' ); assert.equal(descriptor.enumerable, false, 'property should remain non-enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(obj.someProp, 'blastix', 'expected value to be the getter'); assert.equal(descriptor.value, 'blastix', 'expected existing value to remain'); obj.someProp = 'new value'; // make sure the descriptor remains correct (nothing funky, like a redefined, happened in the setter); descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal(descriptor.enumerable, false, 'property should remain non-enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(descriptor.value, 'new value', 'expected existing value to NOT remain'); assert.equal(obj.someProp, 'new value', 'expected value to be the getter'); assert.equal(obj.someProp, 'new value'); } ['@test ensure after watch the property is restored (and the value is no-longer stored in meta) [enumerable]']( assert ) { let obj = { someProp: null, toString() { return 'custom-object'; }, }; Object.defineProperty(obj, 'someProp', { configurable: true, enumerable: true, value: 'blastix', }); watch(obj, 'someProp'); assert.equal(hasMandatorySetter(obj, 'someProp'), true, 'should have a mandatory setter'); let descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal(descriptor.enumerable, true, 'property should remain enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(obj.someProp, 'blastix', 'expected value to be the getter'); assert.equal(descriptor.value, undefined, 'expected existing value to NOT remain'); assert.ok(hasMetaValue(obj, 'someProp'), 'someProp is stored in meta.values'); unwatch(obj, 'someProp'); assert.ok(!hasMetaValue(obj, 'someProp'), 'someProp is no longer stored in meta.values'); descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal( hasMandatorySetter(obj, 'someProp'), false, 'should no longer have a mandatory setter' ); assert.equal(descriptor.enumerable, true, 'property should remain enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(obj.someProp, 'blastix', 'expected value to be the getter'); assert.equal(descriptor.value, 'blastix', 'expected existing value to remain'); obj.someProp = 'new value'; // make sure the descriptor remains correct (nothing funky, like a redefined, happened in the setter); descriptor = Object.getOwnPropertyDescriptor(obj, 'someProp'); assert.equal(descriptor.enumerable, true, 'property should remain enumerable'); assert.equal(descriptor.configurable, true, 'property should remain configurable'); assert.equal(descriptor.value, 'new value', 'expected existing value to NOT remain'); assert.equal(obj.someProp, 'new value'); } ['@test sets up mandatory-setter if property comes from prototype'](assert) { assert.expect(2); let obj = { someProp: null, toString() { return 'custom-object'; }, }; let obj2 = Object.create(obj); watch(obj2, 'someProp'); assert.ok(hasMandatorySetter(obj2, 'someProp'), 'mandatory setter has been setup'); expectAssertion(function() { obj2.someProp = 'foo-bar'; }, 'You must use set() to set the `someProp` property (of custom-object) to `foo-bar`.'); } ['@test inheritance remains live'](assert) { function Parent() {} Parent.prototype.food = 'chips'; let child = new Parent(); assert.equal(child.food, 'chips'); watch(child, 'food'); assert.equal(child.food, 'chips'); Parent.prototype.food = 'icecreame'; assert.equal(child.food, 'icecreame'); unwatch(child, 'food'); assert.equal(child.food, 'icecreame'); Parent.prototype.food = 'chips'; assert.equal(child.food, 'chips'); } ['@test inheritance remains live and preserves this'](assert) { function Parent(food) { this._food = food; } Object.defineProperty(Parent.prototype, 'food', { get() { return this._food; }, }); let child = new Parent('chips'); assert.equal(child.food, 'chips'); watch(child, 'food'); assert.equal(child.food, 'chips'); child._food = 'icecreame'; assert.equal(child.food, 'icecreame'); unwatch(child, 'food'); assert.equal(child.food, 'icecreame'); let foodDesc = Object.getOwnPropertyDescriptor(Parent.prototype, 'food'); assert.ok(!foodDesc.configurable, 'Parent.prototype.food desc should be non configable'); assert.ok(!foodDesc.enumerable, 'Parent.prototype.food desc should be non enumerable'); assert.equal( foodDesc.get.call({ _food: 'hi', }), 'hi' ); assert.equal(foodDesc.set, undefined); assert.equal(child.food, 'icecreame'); } } ); }