can-compute
Version:
CanJS’s legacy way of composing observables. Use can-observation instead.
702 lines (538 loc) • 15.9 kB
JavaScript
require("./proto-compute_test");
var compute = require('can-compute');
var QUnit = require('steal-qunit');
var ObservationRecorder = require("can-observation-recorder");
var canSymbol = require("can-symbol");
var canReflect = require("can-reflect");
var eventQueue = require("can-event-queue/map/map");
var queues = require("can-queues");
var domEvents = require("can-dom-events");
var metaSymbol = canSymbol.for("can.meta");
var domDispatch = domEvents.dispatch;
QUnit.module('can/compute');
QUnit.test('single value compute', function(assert) {
var num = compute(1);
num.on('change', function (ev, newVal, oldVal) {
assert.equal(newVal, 2, 'newVal');
assert.equal(oldVal, 1, 'oldVal');
});
num(2);
});
QUnit.test('inner computes values are not bound to', function(assert) {
var num = compute(1);
var outer = compute(function() {
var inner = compute(function() {
return num() + 1;
});
return 2 * inner();
});
var handler = function () {};
outer.on('change', handler);
// We do a timeout because we temporarily bind on num so that we can use its cached value.
var done = assert.async();
setTimeout(function () {
assert.equal(num.computeInstance[metaSymbol].handlers.get([]).length, 1, 'inner compute only bound once');
assert.equal(outer.computeInstance[metaSymbol].handlers.get([]).length, 1, 'outer compute only bound once');
done();
}, 50);
});
QUnit.test('compute.truthy', function(assert) {
var result = 0;
var numValue;
var num = compute(numValue = 3);
var truthy = compute.truthy(num);
var tester = compute(function () {
if (truthy()) {
return ++result;
} else {
return ++result;
}
});
tester.addEventListener('change', function (ev, newVal, oldVal) {
if (num() === 0) {
assert.equal(newVal, 2, '2 is the new val');
} else if (num() === -1) {
assert.equal(newVal, 3, '3 is the new val');
} else {
assert.ok(false, 'change should not be called');
}
});
assert.equal(tester(), 1, 'on bind, we call tester once');
num(numValue = 2);
num(numValue = 1);
num(numValue = 0);
num(numValue = -1);
});
QUnit.test('a binding compute does not double read', function(assert) {
var sourceAge = 30,
timesComputeIsCalled = 0;
var age = compute(function (newVal) {
timesComputeIsCalled++;
if (timesComputeIsCalled === 1) {
assert.ok(true, 'reading age to get value');
} else if (timesComputeIsCalled === 2) {
assert.equal(newVal, 31, 'the second time should be an update');
} else if (timesComputeIsCalled === 3) {
assert.ok(true, 'called after set to get the value');
} else {
assert.ok(false, 'You\'ve called the callback ' + timesComputeIsCalled + ' times');
}
if (arguments.length) {
sourceAge = newVal;
} else {
return sourceAge;
}
});
var info = compute(function () {
return 'I am ' + age();
});
var k = function () {};
info.bind('change', k);
assert.equal(info(), 'I am 30');
age(31);
assert.equal(info(), 'I am 31');
});
QUnit.test('cloning a setter compute (#547)', function(assert) {
var name = compute('', function (newVal) {
return this.txt + newVal;
});
var cloned = name.clone({
txt: '.'
});
cloned('-');
assert.equal(cloned(), '.-');
});
QUnit.test('compute updated method uses get and old value (#732)', function(assert) {
assert.expect(9);
var input = {
value: 1
};
var value = compute('', {
get: function () {
return input.value;
},
set: function (newVal) {
input.value = newVal;
},
on: function (update) {
input.onchange = update;
},
off: function () {
delete input.onchange;
}
});
assert.equal(value(), 1, 'original value');
assert.ok(!input.onchange, 'nothing bound');
value(2);
assert.equal(value(), 2, 'updated value');
assert.equal(input.value, 2, 'updated input.value');
function handler(ev, newVal, oldVal) {
assert.equal(newVal, 3, 'newVal');
assert.equal(oldVal, 2, 'oldVal');
value.unbind('change',handler);
}
value.bind('change', handler);
assert.ok(input.onchange, 'binding to onchange');
input.value = 3;
input.onchange({});
assert.ok(!input.onchange, 'removed binding');
assert.equal(value(), 3);
});
QUnit.test("a compute updated by source changes within a batch is part of that batch", function(assert) {
var computeA = compute("a");
var computeB = compute("b");
var combined1 = compute(function combined1(){
return computeA()+" "+computeB();
});
var combined2 = compute(function combined2(){
return computeA()+" "+computeB();
});
var combo = compute(function combo(){
return combined1()+" "+combined2();
});
var callbacks = 0;
combo.bind("change", function(){
if(callbacks === 0){
assert.ok(true, "called change once");
} else {
assert.ok(false, "called change multiple times");
}
callbacks++;
});
queues.batch.start();
computeA("A");
computeB("B");
queues.batch.stop();
});
QUnit.test("compute.async can be like a normal getter", function(assert) {
var first = compute("Justin"),
last = compute("Meyer"),
fullName = compute.async("", function(){
return first()+" "+last();
});
assert.equal(fullName(), "Justin Meyer");
});
QUnit.test("compute.async operate on single value", function(assert) {
var a = compute(1);
var b = compute(2);
var obj = compute.async({}, function( curVal ){
if(a()) {
curVal.a = a();
} else {
delete curVal.a;
}
if(b()) {
curVal.b = b();
} else {
delete curVal.b;
}
return curVal;
});
obj.bind("change", function(){});
assert.deepEqual( obj(), {a: 1, b: 2}, "object has all properties" );
a(0);
assert.deepEqual( obj(), {b: 2}, "removed a" );
b(0);
assert.deepEqual( obj(), {}, "removed b" );
});
QUnit.test("compute.async async changing value", function(assert) {
var a = compute(1);
var b = compute(2);
var done;
var async = compute.async(undefined,function( curVal, setVal ){
if(a()) {
setTimeout(function(){
setVal("a");
},10);
} else if(b()) {
setTimeout(function(){
setVal("b");
},10);
} else {
return null;
}
});
var changeArgs = [
{newVal: "a", oldVal: undefined, run: function(){ a(0); } },
{newVal: "b", oldVal: "a", run: function(){ b(0); }},
{newVal: null, oldVal: "b", run: function(){ done(); }}
],
changeNum = 0;
done = assert.async();
async.bind("change", function(ev, newVal, oldVal){
var data = changeArgs[changeNum++];
assert.equal( newVal, data.newVal, "newVal is correct" );
assert.equal( oldVal, data.oldVal, "oldVal is correct" );
setTimeout(data.run, 10);
});
});
QUnit.test("compute.async read without binding", function(assert) {
var source = compute(1);
var async = compute.async([],function( curVal, setVal ){
curVal.push(source());
return curVal;
});
assert.ok(async(), "calling async worked");
});
QUnit.test("bug with nested computes and batch ordering (#1519)", function(assert) {
var root = compute('a');
var isA = compute(function(){
return root() ==='a';
});
var isB = compute(function(){
return root() === 'b';
});
var combined = compute(function(){
var valA = isA(),
valB = isB();
return valA || valB;
});
assert.equal(combined(), true);
combined.bind('change', function(){ });
queues.batch.start();
root('b');
queues.batch.stop();
assert.equal(combined(), true);
//equal(other(), 2);
});
QUnit.test('compute change handler context is set to the function not compute', function(assert) {
var comp = compute(null);
comp.bind('change', function() {
assert.equal(typeof this, 'function');
});
comp('test');
});
QUnit.test('Calling .unbind() on un-bound compute does not throw an error', function(assert) {
var count = compute(0);
count.unbind('change');
assert.ok(true, 'No error was thrown');
});
QUnit.test("dependent computes update in the right order (2093)", function(assert) {
var root = compute('a'),
childB = compute(function() {
return root();
}),
combine = compute(function() {
return root() + childB();
});
combine.bind("change", function(ev, newVal) {
assert.equal(newVal, "bb", "concat changed");
});
root('b');
});
QUnit.test("dependent computes update in the right order with a batch (#2093)", function(assert) {
// so the problem is that `child` then `combine` happens.
// without a batch, child change fires before `combine`, firing `grandChild`, which
// then triggers `combine`.
// the goal should be for
var root = compute('a'),
child = compute(function() {
return root();
}),
child2 = compute(function(){
return root();
}),
grandChild = compute(function(){
return child();
}),
combine = compute(function() {
return child2()+grandChild();
});
/*canLog.log("root", root.computeInstance._cid,
"child", child.computeInstance._cid,
"grandChild", grandChild.computeInstance._cid,
"combine", combine.computeInstance._cid);*/
combine.bind("change", function(ev, newVal) {
assert.equal(newVal, "bb", "concat changed");
});
/*root.bind("change", function(ev, newVal){
canLog.log("root change", ev.batchNum)
});
child.bind("change", function(ev, newVal){
canLog.log("child change", ev.batchNum)
});
grandChild.bind("change", function(ev, newVal){
canLog.log("grandChild change", ev.batchNum)
});*/
queues.batch.start();
root('b');
queues.batch.stop();
});
QUnit.test("bug with nested computes and batch ordering (#1519)", function(assert) {
var root = compute('a');
var isA = compute(function(){
return root() ==='a';
});
var isB = compute(function(){
return root() === 'b';
});
var combined = compute(function(){
var valA = isA(),
valB = isB();
return valA || valB;
});
assert.equal(combined(), true);
combined.bind('change', function(){ });
queues.batch.start();
root('b');
queues.batch.stop();
assert.equal(combined(), true);
//equal(other(), 2);
});
QUnit.test("binding, unbinding, and rebinding works after a timeout (#2095)", function(assert) {
var root = compute(1),
derived = compute(function(){
return root();
});
var change = function(){};
derived.bind("change", change);
derived.unbind("change", change);
var done = assert.async();
setTimeout(function(){
derived.bind("change", function(ev, newVal, oldVal){
assert.equal(newVal, 2, "updated");
done();
});
root(2);
},10);
});
QUnit.test("ObservationRecorder.isRecording observes doesn't understand ObservationRecorder.ignore (#2099)", function(assert) {
assert.expect(0);
var c = compute(1);
c.computeInstance.bind = function() {
assert.ok(false);
};
var outer = compute(function(){
ObservationRecorder.ignore(function(){
c();
})();
});
outer.bind("change", function(){});
});
QUnit.test("handles missing update order items (#2121)",function(assert) {
var root1 = compute("root1"),
child1 = compute(function(){
return root1();
}),
root2 = compute("root2"),
child2 = compute(function(){
return root2();
}),
gc2 = compute(function(){
return child2();
}),
res = compute(function(){
return child1() + gc2();
});
res.bind("change", function(ev, newVal){
assert.equal(newVal, "ROOT1root2");
});
queues.batch.start();
root1("ROOT1");
queues.batch.stop();
});
QUnit.test("compute should not fire event when NaN is set multiple times #2128", function(assert) {
var c = compute(NaN);
compute.bind("change", function() {
assert.ok(false, "change event should not be fired");
});
assert.ok(isNaN(c()));
c(NaN);
});
QUnit.test("eventQueue.afterPreviousEvents firing too late (#2198)", function(assert) {
var compute1 = compute("a"),
compute2 = compute("b");
var derived = compute(function() {
return compute1().toUpperCase();
});
derived.bind("change", function() {
var afterPrevious = false;
compute2.bind("change", function() {
assert.ok(afterPrevious, "after previous should have fired so we would respond to this event");
});
queues.batch.start();
queues.batch.stop();
// we should get this callback before we are notified of the change
eventQueue.afterPreviousEvents(function() {
afterPrevious = true;
});
compute2("c");
});
queues.batch.start();
compute1("x");
queues.batch.stop();
});
QUnit.test("Async getter causes infinite loop (#28)", function(assert) {
var changeCount = 0;
var idCompute = compute(1);
var done = assert.async();
var comp = compute.async(undefined, function(last, resolve) {
var id = idCompute();
setTimeout(function(){
resolve(changeCount + '|' + id);
},1);
resolve(changeCount + '|' + id);
}, null);
comp.bind('change', function(ev, newVal) {
changeCount++;
comp();
});
setTimeout(function(){
idCompute(2);
}, 50);
var checkChangeCount = function(){
if(changeCount === 4) {
assert.equal(changeCount, 4);
done();
} else {
setTimeout(checkChangeCount, 10);
}
};
checkChangeCount();
});
QUnit.test("Listening to input change", function(assert) {
var input = document.createElement("input");
var comp = compute(input, "value", "input");
comp.on("change", function(){
assert.ok(true, "it changed");
});
input.value = 'foo';
domDispatch(input, "input");
});
QUnit.test("Setting an input to change", function(assert) {
var input = document.createElement("input");
var comp = compute(input, "value", "input");
comp("foo");
assert.ok(input.value === "foo");
});
QUnit.test("compute.truthy with functions (canjs/can-stache#172)", function(assert) {
var func = compute(function() {
return function() {
assert.ok(false, "should not be run");
};
});
var truthy = compute.truthy(func);
assert.equal(truthy(), true);
});
QUnit.test("works with can-reflect", function(assert) {
assert.expect(5);
var c = compute(0);
assert.equal( canReflect.getValue(c), 0, "unbound value");
var handler = function(newValue){
assert.equal(newValue, 1, "observed new value");
canReflect.offValue(c, handler);
};
assert.ok(canReflect.isValueLike(c), "isValueLike is true");
canReflect.onValue(c, handler);
assert.equal( canReflect.valueHasDependencies(c), undefined, "valueHasDependencies");
c(1);
assert.equal( canReflect.getValue(c), 1, "bound value");
c(2);
});
QUnit.test("can-reflect valueHasDependencies", function(assert) {
var a = compute("a");
var b = compute("b");
var c = compute(function(){
return a() + b();
});
c.on("change", function(){});
assert.ok( canReflect.valueHasDependencies(c), "valueHasDependencies");
});
QUnit.test("registered symbols", function(assert) {
var a = compute("a");
assert.ok(a[canSymbol.for("can.isValueLike")], "can.isValueLike");
assert.equal(a[canSymbol.for("can.getValue")](), "a", "can.getValue");
a[canSymbol.for("can.setValue")]("b");
assert.equal(a(), "b", "can.setValue");
function handler(val) {
assert.equal(val, "c", "can.onValue");
}
a[canSymbol.for("can.onValue")](handler);
a("c");
a[canSymbol.for("can.offValue")](handler);
a("d"); // doesn't trigger handler
});
QUnit.test("can-reflect setValue", function(assert) {
var a = compute("a");
canReflect.setValue(a, "A");
assert.equal(a(), "A", "compute");
});
QUnit.test("Calling .unbind() with no arguments should tear down all event handlers", function(assert) {
var count = compute(0);
count.on('change', function() {
console.log('Count changed');
});
var handlers = count.computeInstance[canSymbol.for("can.meta")].handlers;
assert.equal(handlers.get(["change"]).length, 1, "Change event added");
count.unbind();
assert.equal(handlers.get(["change"]).length, 0, "All events for compute removed");
});
QUnit.test(".off() unbinds a given handler", function(assert) {
var handler = function(){};
var c = compute('foo');
c.on('change', handler);
var handlers = c.computeInstance[canSymbol.for("can.meta")].handlers;
assert.equal(handlers.get(['change']).length, 1, 'handler added');
c.off('change', handler);
assert.equal(handlers.get(['change']).length, 0, 'hander removed');
});