import { Object as EmberObject } from 'ember-runtime'; import { ComputedProperty, computed, getCachedValueFor, Descriptor, defineProperty, get, set, isWatching, addObserver, notifyPropertyChange, } from '..'; import { meta as metaFor } from 'ember-meta'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; let obj, count; moduleFor( 'computed', class extends AbstractTestCase { ['@test computed property should be an instance of descriptor'](assert) { assert.ok(computed(function() {}) instanceof Descriptor); } ['@test computed properties assert the presence of a getter or setter function']() { expectAssertion(function() { computed('nogetternorsetter', {}); }, 'Computed properties must receive a getter or a setter, you passed none.'); } ['@test computed properties check for the presence of a function or configuration object']() { expectAssertion(function() { computed('nolastargument'); }, 'computed expects a function or an object as last argument.'); } ['@test computed properties defined with an object only allow `get` and `set` keys']() { expectAssertion(function() { computed({ get() {}, set() {}, other() {}, }); }, 'Config object passed to computed can only contain `get` and `set` keys.'); } ['@test computed property can be accessed without `get`'](assert) { let obj = {}; let count = 0; defineProperty( obj, 'foo', computed(function(key) { count++; return 'computed ' + key; }) ); assert.equal(obj.foo, 'computed foo', 'should return value'); assert.equal(count, 1, 'should have invoked computed property'); } ['@test `notifyPropertyChange` works for a computed property not setup using Ember.defineProperty #GH16427']( assert ) { let obj = { a: 50, b: computed(function() { return this.a / 5; }), }; expectDeprecation(function() { assert.equal(get(obj, 'b'), 10); }); obj.a = 10; notifyPropertyChange(obj, 'b'); assert.equal(obj.b, 2); } ['@test set works for a computed property not setup using Ember.defineProperty'](assert) { let obj = { a: 50, b: computed('a', { get() { return this.a * 2; }, set(_, value) { set(this, 'a', value / 2); return value; }, }), }; assert.equal(obj.a, 50); expectDeprecation(function() { set(obj, 'b', 80); }); assert.equal(obj.b, 80); assert.equal(obj.a, 40); set(obj, 'a', 100); assert.equal(obj.a, 100); assert.equal(obj.b, 200); } ['@test defining computed property should invoke property on get'](assert) { let obj = {}; let count = 0; defineProperty( obj, 'foo', computed(function(key) { count++; return 'computed ' + key; }) ); assert.equal(get(obj, 'foo'), 'computed foo', 'should return value'); assert.equal(count, 1, 'should have invoked computed property'); } ['@test defining computed property should invoke property on set'](assert) { let obj = {}; let count = 0; defineProperty( obj, 'foo', computed({ get(key) { return this['__' + key]; }, set(key, value) { count++; this['__' + key] = 'computed ' + value; return this['__' + key]; }, }) ); assert.equal(set(obj, 'foo', 'bar'), 'bar', 'should return set value'); assert.equal(count, 1, 'should have invoked computed property'); assert.equal(get(obj, 'foo'), 'computed bar', 'should return new value'); } ['@test defining a computed property with a dependent key ending with @each is expanded to []']( assert ) { let cp = computed('blazo.@each', function() {}); assert.deepEqual(cp._dependentKeys, ['blazo.[]']); cp = computed('qux', 'zoopa.@each', function() {}); assert.deepEqual(cp._dependentKeys, ['qux', 'zoopa.[]']); } ['@test defining a computed property with a dependent key more than one level deep beyond @each is not supported']() { expectNoWarning(() => { computed('todos', () => {}); }); expectNoWarning(() => { computed('todos.@each.owner', () => {}); }); expectWarning(() => { computed('todos.@each.owner.name', () => {}); }, /You used the key "todos\.@each\.owner\.name" which is invalid\. /); expectWarning(() => { computed('todos.@each.owner.@each.name', () => {}); }, /You used the key "todos\.@each\.owner\.@each\.name" which is invalid\. /); } } ); let objA, objB; moduleFor( 'computed should inherit through prototype', class extends AbstractTestCase { beforeEach() { objA = { __foo: 'FOO' }; defineProperty( objA, 'foo', computed({ get(key) { return this['__' + key]; }, set(key, value) { this['__' + key] = 'computed ' + value; return this['__' + key]; }, }) ); objB = Object.create(objA); objB.__foo = 'FOO'; // make a copy; } afterEach() { objA = objB = null; } ['@test using get() and set()'](assert) { assert.equal(get(objA, 'foo'), 'FOO', 'should get FOO from A'); assert.equal(get(objB, 'foo'), 'FOO', 'should get FOO from B'); set(objA, 'foo', 'BIFF'); assert.equal(get(objA, 'foo'), 'computed BIFF', 'should change A'); assert.equal(get(objB, 'foo'), 'FOO', 'should NOT change B'); set(objB, 'foo', 'bar'); assert.equal(get(objB, 'foo'), 'computed bar', 'should change B'); assert.equal(get(objA, 'foo'), 'computed BIFF', 'should NOT change A'); set(objA, 'foo', 'BAZ'); assert.equal(get(objA, 'foo'), 'computed BAZ', 'should change A'); assert.equal(get(objB, 'foo'), 'computed bar', 'should NOT change B'); } } ); moduleFor( 'redefining computed property to normal', class extends AbstractTestCase { beforeEach() { objA = { __foo: 'FOO' }; defineProperty( objA, 'foo', computed({ get(key) { return this['__' + key]; }, set(key, value) { this['__' + key] = 'computed ' + value; return this['__' + key]; }, }) ); objB = Object.create(objA); defineProperty(objB, 'foo'); // make this just a normal property. } afterEach() { objA = objB = null; } ['@test using get() and set()'](assert) { assert.equal(get(objA, 'foo'), 'FOO', 'should get FOO from A'); assert.equal(get(objB, 'foo'), undefined, 'should get undefined from B'); set(objA, 'foo', 'BIFF'); assert.equal(get(objA, 'foo'), 'computed BIFF', 'should change A'); assert.equal(get(objB, 'foo'), undefined, 'should NOT change B'); set(objB, 'foo', 'bar'); assert.equal(get(objB, 'foo'), 'bar', 'should change B'); assert.equal(get(objA, 'foo'), 'computed BIFF', 'should NOT change A'); set(objA, 'foo', 'BAZ'); assert.equal(get(objA, 'foo'), 'computed BAZ', 'should change A'); assert.equal(get(objB, 'foo'), 'bar', 'should NOT change B'); } } ); moduleFor( 'redefining computed property to another property', class extends AbstractTestCase { beforeEach() { objA = { __foo: 'FOO' }; defineProperty( objA, 'foo', computed({ get(key) { return this['__' + key]; }, set(key, value) { this['__' + key] = 'A ' + value; return this['__' + key]; }, }) ); objB = Object.create(objA); objB.__foo = 'FOO'; defineProperty( objB, 'foo', computed({ get(key) { return this['__' + key]; }, set(key, value) { this['__' + key] = 'B ' + value; return this['__' + key]; }, }) ); } afterEach() { objA = objB = null; } ['@test using get() and set()'](assert) { assert.equal(get(objA, 'foo'), 'FOO', 'should get FOO from A'); assert.equal(get(objB, 'foo'), 'FOO', 'should get FOO from B'); set(objA, 'foo', 'BIFF'); assert.equal(get(objA, 'foo'), 'A BIFF', 'should change A'); assert.equal(get(objB, 'foo'), 'FOO', 'should NOT change B'); set(objB, 'foo', 'bar'); assert.equal(get(objB, 'foo'), 'B bar', 'should change B'); assert.equal(get(objA, 'foo'), 'A BIFF', 'should NOT change A'); set(objA, 'foo', 'BAZ'); assert.equal(get(objA, 'foo'), 'A BAZ', 'should change A'); assert.equal(get(objB, 'foo'), 'B bar', 'should NOT change B'); } } ); moduleFor( 'computed - metadata', class extends AbstractTestCase { ['@test can set metadata on a computed property'](assert) { let computedProperty = computed(function() {}); computedProperty.meta({ key: 'keyValue' }); assert.equal( computedProperty.meta().key, 'keyValue', 'saves passed meta hash to the _meta property' ); } ['@test meta should return an empty hash if no meta is set'](assert) { let computedProperty = computed(function() {}); assert.deepEqual(computedProperty.meta(), {}, 'returned value is an empty hash'); } } ); // .......................................................... // CACHEABLE // moduleFor( 'computed - cacheable', class extends AbstractTestCase { beforeEach() { obj = {}; count = 0; let func = function() { count++; return 'bar ' + count; }; defineProperty(obj, 'foo', computed({ get: func, set: func })); } afterEach() { obj = count = null; } ['@test cacheable should cache'](assert) { assert.equal(get(obj, 'foo'), 'bar 1', 'first get'); assert.equal(get(obj, 'foo'), 'bar 1', 'second get'); assert.equal(count, 1, 'should only invoke once'); } ['@test modifying a cacheable property should update cache'](assert) { assert.equal(get(obj, 'foo'), 'bar 1', 'first get'); assert.equal(get(obj, 'foo'), 'bar 1', 'second get'); assert.equal(set(obj, 'foo', 'baz'), 'baz', 'setting'); assert.equal(get(obj, 'foo'), 'bar 2', 'third get'); assert.equal(count, 2, 'should not invoke again'); } ['@test inherited property should not pick up cache'](assert) { let objB = Object.create(obj); assert.equal(get(obj, 'foo'), 'bar 1', 'obj first get'); assert.equal(get(objB, 'foo'), 'bar 2', 'objB first get'); assert.equal(get(obj, 'foo'), 'bar 1', 'obj second get'); assert.equal(get(objB, 'foo'), 'bar 2', 'objB second get'); set(obj, 'foo', 'baz'); // modify A assert.equal(get(obj, 'foo'), 'bar 3', 'obj third get'); assert.equal(get(objB, 'foo'), 'bar 2', 'objB third get'); } ['@test getCachedValueFor should return the cached value'](assert) { assert.equal(getCachedValueFor(obj, 'foo'), undefined, 'should not yet be a cached value'); get(obj, 'foo'); assert.equal(getCachedValueFor(obj, 'foo'), 'bar 1', 'should retrieve cached value'); } ['@test getCachedValueFor should return falsy cached values'](assert) { defineProperty( obj, 'falsy', computed(function() { return false; }) ); assert.equal(getCachedValueFor(obj, 'falsy'), undefined, 'should not yet be a cached value'); get(obj, 'falsy'); assert.equal(getCachedValueFor(obj, 'falsy'), false, 'should retrieve cached value'); } ['@test setting a cached computed property passes the old value as the third argument']( assert ) { let obj = { foo: 0, }; let receivedOldValue; defineProperty( obj, 'plusOne', computed({ get() {}, set(key, value, oldValue) { receivedOldValue = oldValue; return value; }, }).property('foo') ); set(obj, 'plusOne', 1); assert.strictEqual(receivedOldValue, undefined, 'oldValue should be undefined'); set(obj, 'plusOne', 2); assert.strictEqual(receivedOldValue, 1, 'oldValue should be 1'); set(obj, 'plusOne', 3); assert.strictEqual(receivedOldValue, 2, 'oldValue should be 2'); } } ); // .......................................................... // DEPENDENT KEYS // moduleFor( 'computed - dependentkey', class extends AbstractTestCase { beforeEach() { obj = { bar: 'baz' }; count = 0; let getterAndSetter = function() { count++; get(this, 'bar'); return 'bar ' + count; }; defineProperty( obj, 'foo', computed({ get: getterAndSetter, set: getterAndSetter, }).property('bar') ); } afterEach() { obj = count = null; } ['@test should lazily watch dependent keys on set'](assert) { assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); set(obj, 'foo', 'bar'); assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); } ['@test should lazily watch dependent keys on get'](assert) { assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); get(obj, 'foo'); assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); } ['@test local dependent key should invalidate cache'](assert) { assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); set(obj, 'bar', 'BIFF'); // should invalidate foo assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); } ['@test should invalidate multiple nested dependent keys'](assert) { let count = 0; defineProperty( obj, 'bar', computed(function() { count++; get(this, 'baz'); return 'baz ' + count; }).property('baz') ); assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); assert.equal(isWatching(obj, 'baz'), false, 'precond not watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); set(obj, 'baz', 'BIFF'); // should invalidate bar -> foo assert.equal( isWatching(obj, 'bar'), false, 'should not be watching dependent key after cache cleared' ); assert.equal( isWatching(obj, 'baz'), false, 'should not be watching dependent key after cache cleared' ); assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); } ['@test circular keys should not blow up'](assert) { let func = function() { count++; return 'bar ' + count; }; defineProperty(obj, 'bar', computed({ get: func, set: func }).property('foo')); defineProperty( obj, 'foo', computed(function() { count++; return 'foo ' + count; }).property('bar') ); assert.equal(get(obj, 'foo'), 'foo 1', 'get once'); assert.equal(get(obj, 'foo'), 'foo 1', 'cached retrieve'); set(obj, 'bar', 'BIFF'); // should invalidate bar -> foo -> bar assert.equal(get(obj, 'foo'), 'foo 3', 'should recache'); assert.equal(get(obj, 'foo'), 'foo 3', 'cached retrieve'); } ['@test redefining a property should undo old dependent keys'](assert) { assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1'); assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); defineProperty( obj, 'foo', computed(function() { count++; return 'baz ' + count; }).property('baz') ); assert.equal( isWatching(obj, 'bar'), false, 'after redefining should not be watching dependent key' ); assert.equal(get(obj, 'foo'), 'baz 2'); set(obj, 'bar', 'BIFF'); // should not kill cache assert.equal(get(obj, 'foo'), 'baz 2'); set(obj, 'baz', 'BOP'); assert.equal(get(obj, 'foo'), 'baz 3'); } ['@test can watch multiple dependent keys specified declaratively via brace expansion']( assert ) { defineProperty( obj, 'foo', computed(function() { count++; return 'foo ' + count; }).property('qux.{bar,baz}') ); assert.equal(get(obj, 'foo'), 'foo 1', 'get once'); assert.equal(get(obj, 'foo'), 'foo 1', 'cached retrieve'); set(obj, 'qux', {}); set(obj, 'qux.bar', 'bar'); // invalidate foo assert.equal(get(obj, 'foo'), 'foo 2', 'foo invalidated from bar'); set(obj, 'qux.baz', 'baz'); // invalidate foo assert.equal(get(obj, 'foo'), 'foo 3', 'foo invalidated from baz'); set(obj, 'qux.quux', 'quux'); // do not invalidate foo assert.equal(get(obj, 'foo'), 'foo 3', 'foo not invalidated by quux'); } ['@test throws assertion if brace expansion notation has spaces']() { expectAssertion(function() { defineProperty( obj, 'roo', computed(function() { count++; return 'roo ' + count; }).property('fee.{bar, baz,bop , }') ); }, /cannot contain spaces/); } ['@test throws an assertion if an uncached `get` is called after object is destroyed'](assert) { assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); let meta = metaFor(obj); meta.destroy(); obj.toString = () => ''; expectAssertion(() => { get(obj, 'foo'); }, 'Cannot modify dependent keys for `foo` on `` after it has been destroyed.'); assert.equal(isWatching(obj, 'bar'), false, 'deps were not updated'); } } ); // .......................................................... // CHAINED DEPENDENT KEYS // let func; moduleFor( 'computed - dependentkey with chained properties', class extends AbstractTestCase { beforeEach() { obj = { foo: { bar: { baz: { biff: 'BIFF', }, }, }, }; count = 0; func = function() { count++; return get(obj, 'foo.bar.baz.biff') + ' ' + count; }; } afterEach() { obj = count = func = null; } ['@test depending on simple chain'](assert) { // assign computed property defineProperty(obj, 'prop', computed(func).property('foo.bar.baz.biff')); assert.equal(get(obj, 'prop'), 'BIFF 1'); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); assert.equal(get(obj, 'prop'), 'BUZZ 2'); assert.equal(get(obj, 'prop'), 'BUZZ 2'); set(get(obj, 'foo.bar'), 'baz', { biff: 'BLOB' }); assert.equal(get(obj, 'prop'), 'BLOB 3'); assert.equal(get(obj, 'prop'), 'BLOB 3'); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); assert.equal(get(obj, 'prop'), 'BUZZ 4'); assert.equal(get(obj, 'prop'), 'BUZZ 4'); set(get(obj, 'foo'), 'bar', { baz: { biff: 'BOOM' } }); assert.equal(get(obj, 'prop'), 'BOOM 5'); assert.equal(get(obj, 'prop'), 'BOOM 5'); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); assert.equal(get(obj, 'prop'), 'BUZZ 6'); assert.equal(get(obj, 'prop'), 'BUZZ 6'); set(obj, 'foo', { bar: { baz: { biff: 'BLARG' } } }); assert.equal(get(obj, 'prop'), 'BLARG 7'); assert.equal(get(obj, 'prop'), 'BLARG 7'); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); assert.equal(get(obj, 'prop'), 'BUZZ 8'); assert.equal(get(obj, 'prop'), 'BUZZ 8'); defineProperty(obj, 'prop'); set(obj, 'prop', 'NONE'); assert.equal(get(obj, 'prop'), 'NONE'); set(obj, 'foo', { bar: { baz: { biff: 'BLARG' } } }); assert.equal(get(obj, 'prop'), 'NONE'); // should do nothing assert.equal(count, 8, 'should be not have invoked computed again'); } ['@test chained dependent keys should evaluate computed properties lazily'](assert) { defineProperty(obj.foo.bar, 'b', computed(func)); defineProperty(obj.foo, 'c', computed(function() {}).property('bar.b')); assert.equal(count, 0, 'b should not run'); } } ); // .......................................................... // improved-cp-syntax // moduleFor( 'computed - improved cp syntax', class extends AbstractTestCase { ['@test setter and getters are passed using an object'](assert) { let testObj = EmberObject.extend({ a: '1', b: '2', aInt: computed('a', { get(keyName) { assert.equal(keyName, 'aInt', 'getter receives the keyName'); return parseInt(this.get('a')); }, set(keyName, value, oldValue) { assert.equal(keyName, 'aInt', 'setter receives the keyName'); assert.equal(value, 123, 'setter receives the new value'); assert.equal(oldValue, 1, 'setter receives the old value'); this.set('a', '' + value); // side effect return parseInt(this.get('a')); }, }), }).create(); assert.ok(testObj.get('aInt') === 1, 'getter works'); testObj.set('aInt', 123); assert.ok(testObj.get('a') === '123', 'setter works'); assert.ok(testObj.get('aInt') === 123, 'cp has been updated too'); } ['@test setter can be omited'](assert) { let testObj = EmberObject.extend({ a: '1', b: '2', aInt: computed('a', { get(keyName) { assert.equal(keyName, 'aInt', 'getter receives the keyName'); return parseInt(this.get('a')); }, }), }).create(); assert.ok(testObj.get('aInt') === 1, 'getter works'); assert.ok(testObj.get('a') === '1'); testObj.set('aInt', '123'); assert.ok(testObj.get('aInt') === '123', 'cp has been updated too'); } ['@test getter can be omited'](assert) { let testObj = EmberObject.extend({ com: computed({ set(key, value) { return value; }, }), }).create(); assert.ok(testObj.get('com') === undefined); testObj.set('com', '123'); assert.ok(testObj.get('com') === '123', 'cp has been updated'); } ['@test the return value of the setter gets cached'](assert) { let testObj = EmberObject.extend({ a: '1', sampleCP: computed('a', { get() { assert.ok(false, 'The getter should not be invoked'); return 'get-value'; }, set() { return 'set-value'; }, }), }).create(); testObj.set('sampleCP', 'abcd'); assert.ok(testObj.get('sampleCP') === 'set-value', 'The return value of the CP was cached'); } } ); // .......................................................... // BUGS // moduleFor( 'computed edge cases', class extends AbstractTestCase { ['@test adding a computed property should show up in key iteration'](assert) { let obj = {}; defineProperty(obj, 'foo', computed(function() {})); let found = []; for (let key in obj) { found.push(key); } assert.ok( found.indexOf('foo') >= 0, 'should find computed property in iteration found=' + found ); assert.ok('foo' in obj, 'foo in obj should pass'); } ["@test when setting a value after it had been retrieved empty don't pass function UNDEFINED as oldValue"]( assert ) { let obj = {}; let oldValueIsNoFunction = true; defineProperty( obj, 'foo', computed({ get() {}, set(key, value, oldValue) { if (typeof oldValue === 'function') { oldValueIsNoFunction = false; } return undefined; }, }) ); get(obj, 'foo'); set(obj, 'foo', undefined); assert.ok(oldValueIsNoFunction); } } ); moduleFor( 'computed - setter', class extends AbstractTestCase { ['@test setting a watched computed property'](assert) { let obj = { firstName: 'Yehuda', lastName: 'Katz', }; defineProperty( obj, 'fullName', computed({ get() { return get(this, 'firstName') + ' ' + get(this, 'lastName'); }, set(key, value) { let values = value.split(' '); set(this, 'firstName', values[0]); set(this, 'lastName', values[1]); return value; }, }).property('firstName', 'lastName') ); let fullNameDidChange = 0; let firstNameDidChange = 0; let lastNameDidChange = 0; addObserver(obj, 'fullName', function() { fullNameDidChange++; }); addObserver(obj, 'firstName', function() { firstNameDidChange++; }); addObserver(obj, 'lastName', function() { lastNameDidChange++; }); assert.equal(get(obj, 'fullName'), 'Yehuda Katz'); set(obj, 'fullName', 'Yehuda Katz'); set(obj, 'fullName', 'Kris Selden'); assert.equal(get(obj, 'fullName'), 'Kris Selden'); assert.equal(get(obj, 'firstName'), 'Kris'); assert.equal(get(obj, 'lastName'), 'Selden'); assert.equal(fullNameDidChange, 1); assert.equal(firstNameDidChange, 1); assert.equal(lastNameDidChange, 1); } ['@test setting a cached computed property that modifies the value you give it'](assert) { let obj = { foo: 0, }; defineProperty( obj, 'plusOne', computed({ get() { return get(this, 'foo') + 1; }, set(key, value) { set(this, 'foo', value); return value + 1; }, }).property('foo') ); let plusOneDidChange = 0; addObserver(obj, 'plusOne', function() { plusOneDidChange++; }); assert.equal(get(obj, 'plusOne'), 1); set(obj, 'plusOne', 1); assert.equal(get(obj, 'plusOne'), 2); set(obj, 'plusOne', 1); assert.equal(get(obj, 'plusOne'), 2); assert.equal(plusOneDidChange, 1); set(obj, 'foo', 5); assert.equal(get(obj, 'plusOne'), 6); assert.equal(plusOneDidChange, 2); } } ); moduleFor( 'computed - default setter', class extends AbstractTestCase { ["@test when setting a value on a computed property that doesn't handle sets"](assert) { let obj = {}; let observerFired = false; defineProperty( obj, 'foo', computed(function() { return 'foo'; }) ); addObserver(obj, 'foo', null, () => (observerFired = true)); set(obj, 'foo', 'bar'); assert.equal(get(obj, 'foo'), 'bar', 'The set value is properly returned'); assert.ok(typeof obj.foo === 'string', 'The computed property was removed'); assert.ok(observerFired, 'The observer was still notified'); } } ); moduleFor( 'computed - readOnly', class extends AbstractTestCase { ['@test is chainable'](assert) { let cp = computed(function() {}).readOnly(); assert.ok(cp instanceof Descriptor); assert.ok(cp instanceof ComputedProperty); } ['@test throws assertion if called over a CP with a setter defined with the new syntax']() { expectAssertion(() => { computed({ get() {}, set() {}, }).readOnly(); }, /Computed properties that define a setter using the new syntax cannot be read-only/); } ['@test protects against setting'](assert) { let obj = {}; defineProperty( obj, 'bar', computed(function() { return 'barValue'; }).readOnly() ); assert.equal(get(obj, 'bar'), 'barValue'); assert.throws(() => { set(obj, 'bar', 'newBar'); }, /Cannot set read\-only property "bar" on object:/); assert.equal(get(obj, 'bar'), 'barValue'); } } );