var requirejs = require("requirejs"),
expect = require("chai").expect,
fs = require("fs"),
writeDataFlowFiles = false;
var requirejs = require("requirejs"),
expect = require("chai").expect,
fs = require("fs"),
writeDataFlowFiles = false;
Point require.js to the built model.js source.
requirejs.config({ baseUrl: "dist" });
describe("model", function() {
Load the library as an AMD module.
var ModelFromRequireJS = requirejs("../dist/model");
Load the library as a CommonJS module.
var ModelFromCommonJS = require("../dist/model");
Do some tests with the RequireJS version.
var Model = ModelFromRequireJS;
it("should create a model and listen for changes to a single property", function(done) {
Create a new model by calling Model()
.
var model = Model();
Listen for changes on x using model.when()
.
model.when("x", function (x) {
expect(x).to.equal(30);
done();
});
Set x to be 30, which triggers the callback.
model.x = 30;
Values in the model can be accessed like plain JS object properties.
expect(model.x).to.equal(30);
});
model.when()
calls the callback for existing values.
it("should call fn once to initialize", function(done) {
var model = Model();
model.x = 55;
model.when("x", function (x) {
expect(x).to.equal(55);
done();
});
});
An array of dependencies can be passed to when()
,
and the values from the model are passed to the callback.
it("should call fn with multiple dependency properties", function(done) {
var model = Model();
model.when(["x", "y", "z"], function (x, y, z) {
expect(x).to.equal(5);
expect(y).to.equal(6);
expect(z).to.equal(7);
done();
});
model.x = 5;
model.y = 6;
model.z = 7;
});
Model values are passed as arguments to the callback in the order specified in the dependencies array.
it("should call fn with multiple dependency properties in the order specified", function(done) {
var model = Model();
model.when(["y", "x", "z"], function (y, x, z) {
expect(x).to.equal(5);
expect(y).to.equal(6);
expect(z).to.equal(7);
done();
});
model.x = 5;
model.y = 6;
model.z = 7;
});
Do some tests with the CommonJS version to make sure it works.
Model = ModelFromCommonJS;
Multiple properties can be set simultaneously by
passing an object to model.set
.
it("should set values from an object", function(done) {
var model = Model();
model.when(["y", "x", "z"], function (y, x, z) {
expect(x).to.equal(5);
expect(y).to.equal(6);
expect(z).to.equal(7);
done();
});
model.set({
x: 5,
y: 6,
z: 7
});
});
The callback is not called until all dependency values are defined.
it("should call fn only when all properties are defined", function(done) {
var model = Model();
model.when(["y", "x", "z"], function (y, x, z) {
expect(x).to.equal(5);
expect(y).to.equal(6);
expect(z).to.equal(7);
done();
});
model.set({ x: 5, y: 6 });
setTimeout(function () {
model.z = 7;
}, 10);
});
Multiple changes on a property that happen in sequence
cause the when
callback to be executed only once.
it("should call fn only once for multiple updates", function(done) {
var model = Model();
model.when("x", function (x) {
expect(x).to.equal(30);
done();
});
model.x = 10;
model.x = 20;
model.x = 30;
});
it("should call fn with multiple dependency properties only once after several updates", function(done) {
var model = Model();
model.when(["x", "y", "z"], function (x, y, z) {
expect(x).to.equal(5);
expect(y).to.equal(6);
expect(z).to.equal(7);
done();
});
model.x = 5;
model.y = 6;
model.z = 5;
model.z = 6;
model.z = 7;
});
Properties can be set in the model in the body of a when()
callback.
This pattern can be used to define a data dependency graph
using a functional reactive style. The model system automatically propagates changes
through the data dependency graph. This is similar to computed properties in Ember.js.
it("should compute fullName from firstName and lastName", function(done) {
var model = Model();
model.when(["firstName", "lastName"], function (firstName, lastName) {
model.fullName = firstName + " " + lastName;
});
model.when("fullName", function (fullName) {
expect(fullName).to.equal("John Doe");
done();
});
model.firstName = "John";
model.lastName = "Doe";
});
it("should propagate changes two hops through a data dependency graph", function(done) {
var model = Model();
model.when(["x"], function (x) {
model.y = x + 1;
});
model.when(["y"], function (y) {
expect(y).to.equal(11);
model.z = y * 2;
});
model.when(["z"], function (z) {
expect(z).to.equal(22);
done();
});
model.x = 10;
});
it("should propagate changes three hops through a data dependency graph", function(done) {
var model = Model();
model.when(["w"], function (w) {
expect(w).to.equal(5);
model.x = w * 2;
});
model.when(["x"], function (x) {
expect(x).to.equal(10);
model.y = x + 1;
});
model.when(["y"], function (y) {
expect(y).to.equal(11);
model.z = y * 2;
});
model.when(["z"], function (z) {
expect(z).to.equal(22);
done();
});
model.w = 5;
});
An additional argument can be passed to when
,
which will be the value of this
in the callback function.
it("should use thisArg", function(done) {
var model = Model(),
theThing = { foo: "bar" };
model.when("x", function (x) {
expect(x).to.equal(5);
expect(this).to.equal(theThing);
expect(this.foo).to.equal("bar");
done();
}, theThing);
model.x = 5;
});
Updates through a data dependency graph propagate in a breadth-first manner.
When âaâ changes, âfâ should update once only, after the changes propagated through the following two paths simultaneously:
it("should propagate changes in breadth first iterations", function (done) {
var model = Model();
/* a -> (b, c) */
model.when("a", function (a) {
model.set({ b: a + 1, c: a + 2 });
});
/* b -> d */
model.when("b", function (b) {
model.d = b + 1;
});
/* c -> e */
model.when("c", function (c) {
model.e = c + 1;
});
/* (d, e) -> f */
model.when(["d", "e"], function (d, e) {
model.f = d + e;
});
model.when("f", function (f) {
expect(f).to.equal(15);
done();
});
model.a = 5;
});
it("should support model.on", function() {
var model = Model({ x: 4 });
model.on("x", function(newValue, oldValue){
expect(newValue).to.equal(5);
expect(oldValue).to.equal(4);
expect(this).to.equal(model);
});
model.x = 5;
});
it("should support model.on with thisArg", function() {
var model = Model(),
that = { foo: "bar" };
model.on("x", function(newValue, oldValue){
expect(this.foo).to.equal("bar");
}, that);
model.x = 5;
});
it("should support model.off", function() {
var model = Model(),
invocationCount = 0,
callback = function(newValue, oldValue){
invocationCount++;
};
model.on("x", callback);
model.x = 5;
expect(invocationCount).to.equal(1);
model.x = 6;
expect(invocationCount).to.equal(2);
model.off("x", callback);
model.x = 7;
expect(invocationCount).to.equal(2);
});
it("should cancel listeners", function(done) {
var model = Model(),
xValue,
listener = model.when("x", function (x) {
xValue = x;
});
model.x = 5;
setTimeout(function () {
expect(xValue).to.equal(5);
model.cancel(listener);
model.x = 6;
setTimeout(function () {
expect(xValue).to.equal(5);
done();
}, 0);
}, 0);
});
it("should cancel listeners for multiple properties", function(done) {
var model = Model(),
xValue,
listener = model.when(["x", "y", "z"], function (x, y, z) {
xValue = x;
});
model.x = 5;
model.y = 6;
model.z = 7;
setTimeout(function () {
expect(xValue).to.equal(5);
model.cancel(listener);
model.x = 8;
model.y = 9;
setTimeout(function () {
expect(xValue).to.equal(5);
done();
}, 0);
}, 0);
});
it("should cancel multiple listeners separately", function(done) {
var model = Model(),
xValue,
yValue,
xListener = model.when("x", function (x) { xValue = x; }),
yListener = model.when("y", function (y) { yValue = y; });
model.x = 5;
model.y = 10;
setTimeout(function () {
expect(xValue).to.equal(5);
expect(yValue).to.equal(10);
model.cancel(xListener);
model.x = 6;
model.y = 11;
setTimeout(function () {
expect(xValue).to.equal(5);
expect(yValue).to.equal(11);
model.cancel(yListener);
model.x = 7;
model.y = 12;
setTimeout(function () {
expect(xValue).to.equal(5);
expect(yValue).to.equal(11);
done();
}, 0);
}, 0);
}, 0);
});
it("should handle Model(defaults) constructor API", function() {
var model = Model({ x: 5, y: 10 });
expect(model.x).to.equal(5);
expect(model.y).to.equal(10);
});
it("Models should work with JSON stringify and parse API ", function(done) {
var model1 = Model({ x: 5, y: 10 }),
json1 = JSON.parse(JSON.stringify(model1)),
model2 = Model(json1);
model2.when(["x", "y"], function (x, y) {
expect(x).to.equal(5);
expect(y).to.equal(10);
done();
});
});
it("should support optionally specified properties with Model.None", function(done) {
var model = Model({
first: "John",
middle: Model.None,
last: "Smith"
});
model.when(["first", "middle", "last"], function (first, middle, last){
if(middle === Model.None){
model.full = first + " " + last;
} else {
model.full = first + " " + middle + " " + last;
}
});
model.when("full", function (full){
if(full === "John Smith") {
model.middle = "Clayton";
} else {
expect(full).to.equal("John Clayton Smith");
done();
}
});
});
it("should support instanceof to check model type, regardless if 'new' is used", function() {
var a = Model(),
b = new Model();
expect(a instanceof Model).to.equal(true);
expect(b instanceof Model).to.equal(true);
});
});