UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

1,438 lines (1,389 loc) 35.6 kB
steal('can/util', "can/observe", 'can/map', 'can/list', "can/test", "steal-qunit", function () { QUnit.module('can/observe map+list'); test('Basic Map', 9, function () { var state = new can.Map({ category: 5, productType: 4, properties: { brand: [], model: [], price: [] } }); var added; state.bind('change', function (ev, attr, how, val, old) { equal(attr, 'properties.brand.0', 'correct change name'); equal(how, 'add'); equal(val[0].attr('foo'), 'bar', 'correct'); added = val[0]; }); state.attr('properties.brand') .push({ foo: 'bar' }); state.unbind('change'); added.bind('change', function (ev, attr, how, val, old) { equal(attr, 'foo', 'foo property set on added'); equal(how, 'set', 'added'); equal(val, 'zoo', 'added'); }); state.bind('change', function (ev, attr, how, val, old) { equal(attr, 'properties.brand.0.foo'); equal(how, 'set'); equal(val, 'zoo'); }); added.attr('foo', 'zoo'); }); test('list attr changes length', function () { var l = new can.List([ 0, 1, 2 ]); l.attr(3, 3); equal(l.length, 4); }); test('list splice', function () { var l = new can.List([ 0, 1, 2, 3 ]), first = true; l.bind('change', function (ev, attr, how, newVals, oldVals) { equal(attr, '1'); if (first) { equal(how, 'remove', 'removing items'); equal(newVals, undefined, 'no new Vals'); } else { deepEqual(newVals, [ 'a', 'b' ], 'got the right newVals'); equal(how, 'add', 'adding items'); } first = false; }); l.splice(1, 2, 'a', 'b'); deepEqual(l.serialize(), [ 0, 'a', 'b', 3 ], 'serialized'); }); test('list pop', function () { var l = new can.List([ 0, 1, 2, 3 ]); l.bind('change', function (ev, attr, how, newVals, oldVals) { equal(attr, '3'); equal(how, 'remove'); equal(newVals, undefined); deepEqual(oldVals, [3]); }); l.pop(); deepEqual(l.serialize(), [ 0, 1, 2 ]); }); test('changing an object unbinds', 4, function () { var state = new can.Map({ category: 5, productType: 4, properties: { brand: [], model: [], price: [] } }), count = 0; var brand = state.attr('properties.brand'); state.bind('change', function (ev, attr, how, val, old) { equal(attr, 'properties.brand'); equal(count, 0, 'count called once'); count++; equal(how, 'set'); equal(val[0], 'hi'); }); state.attr('properties.brand', ['hi']); brand.push(1, 2, 3); }); test('replacing with an object that object becomes observable', function () { var state = new can.Map({ properties: { brand: [], model: [], price: [] } }); ok(state.attr('properties') .bind, 'has bind function'); state.attr('properties', {}); ok(state.attr('properties') .bind, 'has bind function'); }); test('attr does not blow away old observable', function () { var state = new can.Map({ properties: { brand: ['gain'] } }); var oldCid = state.attr('properties.brand') ._cid; state.attr({ properties: { brand: [] } }, true); deepEqual(state.attr('properties.brand') ._cid, oldCid, 'should be the same map, so that views bound to the old one get updates'); equal(state.attr('properties.brand') .length, 0, 'list should be empty'); }); test('sub observes respect attr remove parameter', function () { var bindCalled = 0, state = new can.Map({ monkey: { tail: 'brain' } }); state.bind('change', function (ev, attr, how, newVal, old) { bindCalled++; equal(attr, 'monkey.tail'); equal(old, 'brain'); equal(how, 'remove'); }); state.attr({ monkey: {} }); equal('brain', state.attr('monkey.tail'), 'should not remove attribute of sub map when remove param is false'); equal(0, bindCalled, 'remove event not fired for sub map when remove param is false'); state.attr({ monkey: {} }, true); equal(undefined, state.attr('monkey.tail'), 'should remove attribute of sub map when remove param is false'); equal(1, bindCalled, 'remove event fired for sub map when remove param is false'); }); test('remove attr', function () { var state = new can.Map({ properties: { brand: [], model: [], price: [] } }); state.bind('change', function (ev, attr, how, newVal, old) { equal(attr, 'properties'); equal(how, 'remove'); deepEqual(old.serialize(), { brand: [], model: [], price: [] }); }); state.removeAttr('properties'); equal(undefined, state.attr('properties')); }); test('remove nested attr', function () { var state = new can.Map({ properties: { nested: true } }); state.bind('change', function (ev, attr, how, newVal, old) { equal(attr, 'properties.nested'); equal(how, 'remove'); deepEqual(old, true); }); state.removeAttr('properties.nested'); equal(undefined, state.attr('properties.nested')); }); test('remove item in nested array', function () { var state = new can.Map({ array: [ 'a', 'b' ] }); state.bind('change', function (ev, attr, how, newVal, old) { equal(attr, 'array.1'); equal(how, 'remove'); deepEqual(old, ['b']); }); state.removeAttr('array.1'); equal(state.attr('array.length'), 1); }); test('remove nested property in item of array', function () { var state = new can.Map({ array: [{ nested: true }] }); state.bind('change', function (ev, attr, how, newVal, old) { equal(attr, 'array.0.nested'); equal(how, 'remove'); deepEqual(old, true); }); state.removeAttr('array.0.nested'); equal(undefined, state.attr('array.0.nested')); }); test('remove nested property in item of array map', function () { var state = new can.List([{ nested: true }]); state.bind('change', function (ev, attr, how, newVal, old) { equal(attr, '0.nested'); equal(how, 'remove'); deepEqual(old, true); }); state.removeAttr('0.nested'); equal(undefined, state.attr('0.nested')); }); test('attr with an object', function () { var state = new can.Map({ properties: { foo: 'bar', brand: [] } }); state.bind('change', function (ev, attr, how, newVal) { equal(attr, 'properties.foo', 'foo has changed'); equal(newVal, 'bad'); }); state.attr({ properties: { foo: 'bar', brand: [] } }); state.attr({ properties: { foo: 'bad', brand: [] } }); state.unbind('change'); state.bind('change', function (ev, attr, how, newVal) { equal(attr, 'properties.brand.0'); equal(how, 'add'); deepEqual(newVal, ['bad']); }); state.attr({ properties: { foo: 'bad', brand: ['bad'] } }); }); test('empty get', function () { var state = new can.Map({}); equal(state.attr('foo.bar'), undefined); }); test('attr deep array ', function () { var state = new can.Map({}); var arr = [{ foo: 'bar' }], thing = { arr: arr }; state.attr({ thing: thing }, true); ok(thing.arr === arr, 'thing unmolested'); }); test('attr semi-serialize', function () { var first = { foo: { bar: 'car' }, arr: [ 1, 2, 3, { four: '5' } ] }, compare = { foo: { bar: 'car' }, arr: [ 1, 2, 3, { four: '5' } ] }; var res = new can.Map(first) .attr(); deepEqual(res, compare, 'test'); }); test('attr sends events after it is done', function () { var state = new can.Map({ foo: 1, bar: 2 }); state.bind('change', function () { equal(state.attr('foo'), -1, 'foo set'); equal(state.attr('bar'), -2, 'bar set'); }); state.attr({ foo: -1, bar: -2 }); }); test('direct property access', function () { var state = new can.Map({ foo: 1, attr: 2 }); equal(state.foo, 1); equal(typeof state.attr, 'function'); }); test('pop unbinds', function () { var l = new can.List([{ foo: 'bar' }]); var o = l.attr(0), count = 0; l.bind('change', function (ev, attr, how, newVal, oldVal) { count++; if (count === 1) { equal(attr, '0.foo', 'count is set'); } else if (count === 2) { equal(how, 'remove', 'remove event called'); equal(attr, '0', 'remove event called with correct index'); } else { ok(false, 'change handler called too many times'); } }); equal(o.attr('foo'), 'bar'); o.attr('foo', 'car'); l.pop(); o.attr('foo', 'bad'); }); test('splice unbinds', function () { var l = new can.List([{ foo: 'bar' }]); var o = l.attr(0), count = 0; l.bind('change', function (ev, attr, how, newVal, oldVal) { count++; if (count === 1) { equal(attr, '0.foo', 'count is set'); } else if (count === 2) { equal(how, 'remove'); equal(attr, '0'); } else { ok(false, 'called too many times'); } }); equal(o.attr('foo'), 'bar'); o.attr('foo', 'car'); l.splice(0, 1); o.attr('foo', 'bad'); }); test('always gets right attr even after moving array items', function () { var l = new can.List([{ foo: 'bar' }]); var o = l.attr(0); l.unshift('A new Value'); l.bind('change', function (ev, attr, how) { equal(attr, '1.foo'); }); o.attr('foo', 'led you'); }); test('recursive observers do not cause stack overflow', function () { expect(0); var a = new can.Map(); var b = new can.Map({ a: a }); a.attr('b', b); }); test('bind to specific attribute changes when an existing attribute\'s value is changed', function () { var paginate = new can.Map({ offset: 100, limit: 100, count: 2000 }); paginate.bind('offset', function (ev, newVal, oldVal) { equal(newVal, 200); equal(oldVal, 100); }); paginate.attr('offset', 200); }); test('bind to specific attribute changes when an attribute is removed', 2, function () { var paginate = new can.Map({ offset: 100, limit: 100, count: 2000 }); paginate.bind('offset', function (ev, newVal, oldVal) { equal(newVal, undefined); equal(oldVal, 100); }); paginate.removeAttr('offset'); }); test('Array accessor methods', 11, function () { var l = new can.List([ 'a', 'b', 'c' ]), sliced = l.slice(2), joined = l.join(' | '), concatenated = l.concat([ 2, 1 ], new can.List([0])); ok(sliced instanceof can.List, 'Slice is an Observable list'); equal(sliced.length, 1, 'Sliced off two elements'); equal(sliced[0], 'c', 'Single element as expected'); equal(joined, 'a | b | c', 'Joined list properly'); ok(concatenated instanceof can.List, 'Concatenated is an Observable list'); deepEqual(concatenated.serialize(), [ 'a', 'b', 'c', 2, 1, 0 ], 'List concatenated properly'); l.forEach(function (letter, index) { ok(true, 'Iteration'); if (index === 0) { equal(letter, 'a', 'First letter right'); } if (index === 2) { equal(letter, 'c', 'Last letter right'); } }); }); test('instantiating can.List of correct type', function () { var Ob = can.Map({ getName: function () { return this.attr('name'); } }); var list = new Ob.List([{ name: 'Tester' }]); equal(list.length, 1, 'List length is correct'); ok(list[0] instanceof can.Map, 'Initialized list item converted to can.Map'); ok(list[0] instanceof Ob, 'Initialized list item converted to Ob'); equal(list[0].getName(), 'Tester', 'Converted to extended Map instance, could call getName()'); list.push({ name: 'Another test' }); equal(list[1].getName(), 'Another test', 'Pushed item gets converted as well'); }); test('can.List.prototype.splice converts objects (#253)', function () { var Ob = can.Map({ getAge: function () { return this.attr('age') + 10; } }); var list = new Ob.List([{ name: 'Tester', age: 23 }, { name: 'Tester 2', age: 44 }]); equal(list[0].getAge(), 33, 'Converted age'); list.splice(1, 1, { name: 'Spliced', age: 92 }); equal(list[1].getAge(), 102, 'Converted age of spliced'); }); test('removing an already missing attribute does not cause an event', function () { expect(0); var ob = new can.Map(); ob.bind('change', function () { ok(false); }); ob.removeAttr('foo'); }); test('Only plain objects should be converted to Observes', function () { var ob = new can.Map(); ob.attr('date', new Date()); ok(ob.attr('date') instanceof Date, 'Date should not be converted'); var selected = can.$('body'); ob.attr('sel', selected); if (can.isArray(selected)) { ok(ob.attr('sel') instanceof can.List, 'can.$() as array converted into List'); } else { equal(ob.attr('sel'), selected, 'can.$() should not be converted'); } ob.attr('element', document.getElementsByTagName('body')[0]); equal(ob.attr('element'), document.getElementsByTagName('body')[0], 'HTMLElement should not be converted'); ob.attr('window', window); equal(ob.attr('window'), window, 'Window object should not be converted'); }); test('bind on deep properties', function () { expect(2); var ob = new can.Map({ name: { first: 'Brian' } }); ob.bind('name.first', function (ev, newVal, oldVal) { equal(newVal, 'Justin'); equal(oldVal, 'Brian'); }); ob.attr('name.first', 'Justin'); }); test('startBatch and stopBatch and changed event', 5, function () { var ob = new can.Map({ name: { first: 'Brian' }, age: 29 }), bothSet = false, changeCallCount = 0, changedCalled = false; ob.bind('change', function () { ok(bothSet, 'both properties are set before the changed event was called'); ok(!changedCalled, 'changed not called yet'); changeCallCount++; }); stop(); can.batch.start(function () { ok(true, 'batch callback called'); }); ob.attr('name.first', 'Justin'); setTimeout(function () { ob.attr('age', 30); bothSet = true; can.batch.stop(); start(); }, 1); }); test('startBatch callback', 4, function () { var ob = new can.Map({ game: { name: 'Legend of Zelda' }, hearts: 15 }), callbackCalled = false; ob.bind('change', function () { equal(callbackCalled, false, 'startBatch callback not called yet'); }); can.batch.start(function () { ok(true, 'startBatch callback called'); callbackCalled = true; }); ob.attr('hearts', 16); equal(callbackCalled, false, 'startBatch callback not called yet'); can.batch.stop(); equal(callbackCalled, true, 'startBatch callback called'); }); test('nested map attr', function () { var person1 = new can.Map({ name: { first: 'Josh' } }), person2 = new can.Map({ name: { first: 'Justin', last: 'Meyer' } }), count = 0; person1.bind('change', function (ev, attr, how, val, old) { equal(count, 0, 'change called once'); count++; equal(attr, 'name'); equal(val.attr('first'), 'Justin'); equal(val.attr('last'), 'Meyer'); }); person1.attr('name', person2.attr('name')); person1.attr('name', person2.attr('name')); }); test('Nested array conversion (#172)', 4, function () { var original = [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ], list = new can.List(original); equal(list.length, 3, 'list length is correct'); deepEqual(list.serialize(), original, 'Lists are the same'); list.unshift([ 10, 11 ], [ 12, 13 ]); ok(list[0] instanceof can.List, 'Unshifted array converted to map list'); deepEqual(list.serialize(), [ [ 10, 11 ], [ 12, 13 ] ].concat(original), 'Arrays unshifted properly'); }); test('can.List.prototype.replace (#194)', 7, function () { var list = new can.List([ 'a', 'b', 'c' ]), replaceList = [ 'd', 'e', 'f', 'g' ], dfd = new can.Deferred(); list.bind('remove', function (ev, arr) { equal(arr.length, 3, 'Three elements removed'); }); list.bind('add', function (ev, arr) { equal(arr.length, 4, 'Four new elements added'); }); list.replace(replaceList); deepEqual(list.serialize(), replaceList, 'Lists are the same'); list.unbind('remove'); list.unbind('add'); list.replace(); equal(list.length, 0, 'List has been emptied'); list.push('D'); stop(); list.replace(dfd); setTimeout(function () { var newList = [ 'x', 'y' ]; list.bind('remove', function (ev, arr) { equal(arr.length, 1, 'One element removed'); }); list.bind('add', function (ev, arr) { equal(arr.length, 2, 'Two new elements added from Deferred'); }); dfd.resolve(newList); deepEqual(list.serialize(), newList, 'Lists are the same'); start(); }, 100); }); test('replace with a deferred that resolves to an List', function () { var def = new can.Deferred(); def.resolve(new can.List([{ name: 'foo' }, { name: 'bar' }])); var list = new can.List([{ name: '1' }, { name: '2' }]); list.bind('length', function () { equal(list.length, 2, 'length is still 2'); equal(list[0].attr('name'), 'foo', 'set to foo'); }); list.replace(def); }); test('.attr method doesn\'t merge nested objects (#207)', function () { var test = new can.Map({ a: { a1: 1, a2: 2 }, b: { b1: 1, b2: 2 } }); test.attr({ a: { a2: 3 }, b: { b1: 3 } }); deepEqual(test.attr(), { 'a': { 'a1': 1, 'a2': 3 }, 'b': { 'b1': 3, 'b2': 2 } }, 'Object merged as expected'); }); test('IE8 error on list setup with List (#226)', function () { var list = new can.List([ 'first', 'second', 'third' ]), otherList = new can.List(list); deepEqual(list.attr(), otherList.attr(), 'Lists are the same'); }); test('initialize List with a deferred', function () { stop(); var def = new can.Deferred(); var list = new can.List(def); list.bind('add', function (ev, items, index) { deepEqual(items, [ 'a', 'b' ]); equal(index, 0); start(); }); setTimeout(function () { def.resolve([ 'a', 'b' ]); }, 10); }); test('triggering a event while in a batch (#291)', function () { expect(0); stop(); var map = new can.Map(); can.batch.start(); can.trigger(map, 'change', 'random'); setTimeout(function () { can.batch.stop(); start(); }, 10); }); test('dot separated keys (#257, #296)', function () { var ob = new can.Map({ 'test.value': 'testing', other: { test: 'value' } }); equal(ob['test.value'], 'testing', 'Set value with dot separated key properly'); equal(ob.attr('test.value'), 'testing', 'Could retrieve value with .attr'); equal(ob.attr('other.test'), 'value', 'Still getting dot separated value'); ob.attr({ 'other.bla': 'othervalue' }); equal(ob['other.bla'], 'othervalue', 'Key is not split'); equal(ob.attr('other.bla'), 'othervalue', 'Could retrieve value with .attr'); ob.attr('other.stuff', 'thinger'); equal(ob.attr('other.stuff'), 'thinger', 'Set dot separated value'); deepEqual(ob.attr('other') .serialize(), { test: 'value', stuff: 'thinger' }, 'Object set properly'); }); test('cycle binding', function () { var first = new can.Map(), second = new can.Map(); first.attr('second', second); second.attr('first', second); var handler = function () {}; first.bind('change', handler); ok(first._bindings, 'has bindings'); first.unbind('change', handler); ok(!first._bindings, 'bindings removed'); }); test('Deferreds are not converted', function () { var dfd = can.Deferred(), ob = new can.Map({ test: dfd }); ok(can.isPromise(ob.attr('test')), 'Attribute is a deferred'); ok(!ob.attr('test') ._cid, 'Does not have a _cid'); }); test('Setting property to undefined', function () { var ob = new can.Map({ 'foo': 'bar' }); ob.attr('foo', undefined); equal(ob.attr('foo'), undefined, 'foo has a value.'); }); test('removing list items containing computes', function () { var list = new can.List([{ comp: can.compute(function () { return false; }) }]); list.pop(); equal(list.length, 0, 'list is empty'); }); QUnit.module('can/observe compute'); test('Basic Compute', function () { var o = new can.Map({ first: 'Justin', last: 'Meyer' }); var prop = can.compute(function () { return o.attr('first') + ' ' + o.attr('last'); }); equal(prop(), 'Justin Meyer'); var handler = function (ev, newVal, oldVal) { equal(newVal, 'Brian Meyer'); equal(oldVal, 'Justin Meyer'); }; prop.bind('change', handler); o.attr('first', 'Brian'); prop.unbind('change', handler); o.attr('first', 'Brian'); }); test('compute on prototype', function () { var Person = can.Map({ fullName: function () { return this.attr('first') + ' ' + this.attr('last'); } }); var me = new Person({ first: 'Justin', last: 'Meyer' }); var fullName = can.compute(me.fullName, me); equal(fullName(), 'Justin Meyer'); var called = 0; fullName.bind('change', function (ev, newVal, oldVal) { called++; equal(called, 1, 'called only once'); equal(newVal, 'Justin Shah'); equal(oldVal, 'Justin Meyer'); }); me.attr('last', 'Shah'); }); test('setter compute', function () { var project = new can.Map({ progress: 0.5 }); var computed = can.compute(function (val) { if (val) { project.attr('progress', val / 100); } else { return parseInt(project.attr('progress') * 100, 10); } }); equal(computed(), 50, 'the value is right'); computed(25); equal(project.attr('progress'), 0.25); equal(computed(), 25); computed.bind('change', function (ev, newVal, oldVal) { equal(newVal, 75); equal(oldVal, 25); }); computed(75); }); test('compute a compute', function () { var project = new can.Map({ progress: 0.5 }); var percent = can.compute(function (val) { if (val) { project.attr('progress', val / 100); } else { return parseInt(project.attr('progress') * 100, 10); } }); percent.named = 'PERCENT'; equal(percent(), 50, 'percent starts right'); percent.bind('change', function () {}); var fraction = can.compute(function (val) { if (val) { percent(parseInt(val.split('/')[0], 10)); } else { return percent() + '/100'; } }); fraction.named = 'FRACTIOn'; fraction.bind('change', function () {}); equal(fraction(), '50/100', 'fraction starts right'); percent(25); equal(percent(), 25); equal(project.attr('progress'), 0.25, 'progress updated'); equal(fraction(), '25/100', 'fraction updated'); fraction('15/100'); equal(fraction(), '15/100'); equal(project.attr('progress'), 0.15, 'progress updated'); equal(percent(), 15, '% updated'); }); test('compute with a simple compute', function () { expect(4); var a = can.compute(5); var b = can.compute(function () { return a() * 2; }); equal(b(), 10, 'b starts correct'); a(3); equal(b(), 6, 'b updates'); b.bind('change', function () { equal(b(), 24, 'b fires change'); }); a(12); equal(b(), 24, 'b updates when bound'); }); test('empty compute', function () { var c = can.compute(); c.bind('change', function (ev, newVal, oldVal) { ok(oldVal === undefined, 'was undefined'); ok(newVal === 0, 'now zero'); }); c(0); }); test('only one update on a batchTransaction', function () { var person = new can.Map({ first: 'Justin', last: 'Meyer' }); var func = can.compute(function () { return person.attr('first') + ' ' + person.attr('last') + Math.random(); }); var callbacks = 0; func.bind("change", function (ev, newVal, oldVal) { callbacks++; }); person.attr({ first: 'Brian', last: 'Moschel' }); equal(callbacks, 1, 'only one callback'); }); test('only one update on a start and end transaction', function () { var person = new can.Map({ first: 'Justin', last: 'Meyer' }), age = can.compute(5); var func = can.compute(function (newVal, oldVal) { return person.attr('first') + ' ' + person.attr('last') + age() + Math.random(); }); var callbacks = 0; func.bind("change",function (ev, newVal, oldVal) { callbacks++; }); can.batch.start(); person.attr('first', 'Brian'); stop(); setTimeout(function () { person.attr('last', 'Moschel'); age(12); can.batch.stop(); equal(callbacks, 1, 'only one callback'); start(); }); }); test('Compute emits change events when an embbedded observe has properties added or removed', 4, function () { var obs = new can.Map(), compute1 = can.compute(function () { var txt = obs.attr('foo'); obs.each(function (val) { txt += val.toString(); }); return txt; }); compute1.bind('change', function (ev, newVal, oldVal) { ok(true, 'change handler fired: ' + newVal); }); obs.attr('foo', 1); obs.attr('bar', 2); obs.attr('foo', 3); obs.removeAttr('bar'); obs.removeAttr('bar'); }); test('compute only updates once when a list\'s contents are replaced', function () { var list = new can.List([{ name: 'Justin' }]), computedCount = 0; var compute = can.compute(function () { computedCount++; list.each(function (item) { item.attr('name'); }); }); equal(0, computedCount, 'computes are not called until their value is read'); compute.bind('change', function (ev, newVal, oldVal) {}); equal(1, computedCount, 'binding computes to store the value'); list.replace([{ name: 'hank' }]); equal(2, computedCount, 'only one compute'); }); test('Generate computes from Observes with can.Map.prototype.compute (#203)', 6, function () { var obs = new can.Map({ test: 'testvalue' }); var compute = obs.compute('test'); ok(compute.isComputed, '`test` is computed'); equal(compute(), 'testvalue', 'Value is as expected'); obs.attr('test', 'observeValue'); equal(compute(), 'observeValue', 'Value is as expected'); compute.bind('change', function (ev, newVal) { equal(newVal, 'computeValue', 'new value from compute'); }); obs.bind('change', function (ev, name, how, newVal) { equal(newVal, 'computeValue', 'Got new value from compute'); }); compute('computeValue'); equal(compute(), 'computeValue', 'Got updated value'); }); test('compute of computes', function () { expect(2); var suggestedSearch = can.compute(null), searchQuery = can.compute(''), searchText = can.compute(function () { var suggested = suggestedSearch(); if (suggested) { return suggested; } else { return searchQuery(); } }); equal('', searchText(), 'inital set'); searchText.bind('change', function (ev, newVal) { equal(newVal, 'food', 'food set'); }); searchQuery('food'); }); test('compute doesn\'t rebind and leak with 0 bindings', function () { var state = new can.Map({ foo: 'bar' }); var computedA = 0, computedB = 0; var computeA = can.compute(function () { computedA++; return state.attr('foo') === 'bar'; }); var computeB = can.compute(function () { computedB++; return state.attr('foo') === 'bar' || 15; }); function aChange(ev, newVal) { if (newVal) { computeB.bind('change.computeA', function () {}); } else { computeB.unbind('change.computeA'); } } computeA.bind('change', aChange); aChange(null, computeA()); equal(computedA, 1, 'binding A computes the value'); equal(computedB, 1, 'A=true, so B is bound, computing the value'); state.attr('foo', 'baz'); equal(computedA, 2, 'A recomputed and unbound B'); equal(computedB, 1, 'B was unbound, so not recomputed'); state.attr('foo', 'bar'); equal(computedA, 3, 'A recomputed => true'); equal(computedB, 2, 'A=true so B is rebound and recomputed'); computeA.unbind('change', aChange); computeB.unbind('change.computeA'); state.attr('foo', 'baz'); equal(computedA, 3, 'unbound, so didn\'t recompute A'); equal(computedB, 2, 'unbound, so didn\'t recompute B'); }); test('compute setter without external value', function () { var age = can.compute(0, function (newVal, oldVal) { var num = +newVal; if (!isNaN(num) && 0 <= num && num <= 120) { return num; } else { return oldVal; } }); equal(age(), 0, 'initial value set'); age.bind('change', function (ev, newVal, oldVal) { equal(5, newVal); age.unbind('change', this.Constructor); }); age(5); equal(age(), 5, '5 set'); age('invalid'); equal(age(), 5, '5 kept'); }); test('compute value', function () { expect(9); var input = { value: 1 }; var value = can.compute('', { get: function () { return input.value; }, set: function (newVal) { input.value = newVal; }, on: function (update) { input.onchange = update; }, off: function () { delete input.onchange; } }); equal(value(), 1, 'original value'); ok(!input.onchange, 'nothing bound'); value(2); equal(value(), 2, 'updated value'); equal(input.value, 2, 'updated input.value'); value.bind('change', function (ev, newVal, oldVal) { equal(newVal, 3, 'newVal'); equal(oldVal, 2, 'oldVal'); value.unbind('change', this.Constructor); }); ok(input.onchange, 'binding to onchange'); value(3); ok(!input.onchange, 'removed binding'); equal(value(), 3); }); test('compute bound to observe', function () { var me = new can.Map({ name: 'Justin' }); var bind = me.bind, unbind = me.unbind, bindCount = 0; me.bind = function () { bindCount++; bind.apply(this, arguments); }; me.unbind = function () { bindCount--; unbind.apply(this, arguments); }; var name = can.compute(me, 'name'); equal(bindCount, 0); equal(name(), 'Justin'); var handler = function (ev, newVal, oldVal) { equal(newVal, 'Justin Meyer'); equal(oldVal, 'Justin'); }; name.bind('change', handler); equal(bindCount, 1); name.unbind('change', handler); stop(); setTimeout(function () { start(); equal(bindCount, 0); }, 100); }); test('binding to a compute on an observe before reading', function () { var me = new can.Map({ name: 'Justin' }); var name = can.compute(me, 'name'); var handler = function (ev, newVal, oldVal) { equal(newVal, 'Justin Meyer'); equal(oldVal, 'Justin'); }; name.bind('change', handler); equal(name(), 'Justin'); }); test('compute bound to input value', function () { var input = document.createElement('input'); input.value = 'Justin'; var value = can.compute(input, 'value', 'change'); equal(value(), 'Justin'); value('Justin M.'); equal(input.value, 'Justin M.', 'input change correctly'); var handler = function (ev, newVal, oldVal) { equal(newVal, 'Justin Meyer'); equal(oldVal, 'Justin M.'); }; value.bind('change', handler); input.value = 'Justin Meyer'; value.unbind('change', handler); stop(); setTimeout(function () { input.value = 'Brian Moschel'; equal(value(), 'Brian Moschel'); start(); }, 50); }); test('compute on the prototype', function () { expect(4); var Person = can.Map.extend({ fullName: can.compute(function (fullName) { if (arguments.length) { var parts = fullName.split(' '); this.attr({ first: parts[0], last: parts[1] }); } else { return this.attr('first') + ' ' + this.attr('last'); } }) }); var me = new Person(); var fn = me.attr({ first: 'Justin', last: 'Meyer' }) .attr('fullName'); equal(fn, 'Justin Meyer', 'can read attr'); me.attr('fullName', 'Brian Moschel'); equal(me.attr('first'), 'Brian', 'set first name'); equal(me.attr('last'), 'Moschel', 'set last name'); var handler = function (ev, newVal, oldVal) { ok(newVal, 'Brian M'); }; me.bind('fullName', handler); me.attr('last', 'M'); me.unbind('fullName', handler); me.attr('first', 'B'); }); test('join is computable (#519)', function () { expect(2); var l = new can.List([ 'a', 'b' ]); var joined = can.compute(function () { return l.join(','); }); joined.bind('change', function (ev, newVal, oldVal) { equal(oldVal, 'a,b'); equal(newVal, 'a,b,c'); }); l.push('c'); }); test("nested computes", function () { var data = new can.Map({}); var compute = data.compute('summary.button'); compute.bind('change', function () { ok(true, "compute changed"); }); data.attr({ summary: { button: 'hey' } }, true); }); test("can.each works with replacement of index (#815)", function(){ var items = new can.List(["a","b"]); var value = can.compute(function(){ var res = ""; items.each(function(item){ res += item; }); return res; }); value.bind("change", function(ev, newValue){ equal(newValue, "Ab", "updated value"); }); items.attr(0,"A"); }); test("When adding a property and using .each only a single update runs (#815)", function(){ var items = new can.Map({}), computedCount = 0; var value = can.compute(function(){ computedCount++; var res = ""; items.each(function(item){ res += item; }); return res; }); value.bind("change", function(){}); items.attr("a","b"); equal(computedCount, 2, "recalculated twice"); }); test("compute(obs, prop) doesn't read attr", function(){ var map = new can.Map({name: "foo"}); var name = can.compute(map, "name"); var oldAttr = map.attr; var count = 0; map.attr= function(){ count++; return oldAttr.apply(this, arguments); }; name.bind("change", function(){}); equal(count, 1, "attr only called once to get cached value"); oldAttr.call(map,"name","bar"); equal(count, 1, "attr only called once to get cached value"); }); test("computes in observes leak handlers (#1676)", function(){ var handler = function() {}; // 1. Create a Map var person = new can.Map({ pet: {type: 'dog', name: 'fluffy'} }); equal(person._bindings || 0, 0, "no bindings"); // 2. Manually add a compute that references a nested property of the same Map person.attr('petInfo', can.compute(function() { return person.attr('pet.type') + ': ' + person.attr('pet.name'); })); equal(person._bindings || 0, 0, "After adding compute no bindings"); // 3. Bind to the Map's 'change' event person.bind('change', handler); equal(person._bindings, 2, "After adding compute no bindings"); // 4. Unbind the 'change' event person.unbind('change', handler); equal(person._bindings, 0, "After unbinding no bindings"); }); test('compute bound to object property (#1719)', 4, function () { var obj = {}; obj.foo = 'bar'; var value = can.compute(obj, 'foo', 'change'); equal(value(), 'bar', 'property retrieved correctly'); value('baz'); equal(obj.foo, 'baz', 'property changed correctly'); var handler = function(ev, newVal, oldVal) { equal(newVal, 'qux', 'change handler newVal correct'); equal(oldVal, 'baz', 'change handler oldVal correct'); }; value.bind('change', handler); value('qux'); value.unbind('change', handler); }); test('compute bound to nested object property (#1719)', 4, function () { var obj = { prop: { subprop: { foo: 'bar' } } }; var value = can.compute(obj, 'prop.subprop.foo', 'change'); equal(value(), 'bar', 'property retrieved correctly'); value('baz'); equal(obj.prop.subprop.foo, 'baz', 'property changed correctly'); var handler = function(ev, newVal, oldVal) { equal(newVal, 'qux', 'change handler newVal correct'); equal(oldVal, 'baz', 'change handler oldVal correct'); }; value.bind('change', handler); value('qux'); value.unbind('change', handler); }); });