##What is Emily?
- Emily is a JS library for building scalable web applications.
- It's runtime agnostic as it doesn't rely on the DOM.
- It's ready for realtime applications.
- It's only a set of AMD/commonJS modules, your module loader is the framework
- It's ready for being used with other frameworks.
- It only relies on standard features
- It eases the development of MV* applications by providing the M
##What modules does it provide?
- Observable: the all mighty observer design pattern.
- Store: the spine of your MV* application.
- Promise: a fully compliant promise/A+ implementation following promiseA+-tests
- StateMachine: don't hide your states and transitions behind if/else anymore.
- Transport: make requests to anything node.js has access to.
- Tools: these functions you always need and rewrite.
- Router: set routes with associated actions and navigate to them while keeping tack of the history
##How do I use it?
npm install emily
var emily = require("emily");
var StateMachine = emily.StateMachine;
var Observable = emily.Observable;
var Promise = emily.Promise;
var Router = emily.Router;
var StateMachine = emily.StateMachine;
var Store = emily.Store;
var Tools = emily.Tools;
var Transport = emily.Transport;
// ...
##Integration tests:
###Observable
describe("Observable implements the Observer design pattern, also called publish subscribe", function () {
it("has a notify function for publishing something on a topic", function () {
var observable = new Observable(),
scope = null,
expectedScope = {},
message;
observable.watch("topic", function listener(something) {
message = something;
scope = this;
}, expectedScope);
observable.notify("topic", "hello");
expect(message).toBe("hello");
expect(expectedScope).toBe(scope);
});
it("can listen to events on a topic only once", function () {
var observable = new Observable(),
listener = jasmine.createSpy(),
handle = null;
handle = observable.once("topic", listener, this);
expect(observable.hasObserver(handle)).toBe(true);
observable.notify("topic", 1, 2, 3);
expect(listener).toHaveBeenCalledWith(1, 2, 3);
listener.reset();
expect(observable.hasObserver(handle)).toBe(false);
observable.notify("topic", 1, 2, 3);
expect(listener).not.toHaveBeenCalled();
});
it("notifies several listeners in the order they were added", function () {
var observable = new Observable(),
order = [];
observable.watch("topic", function listener1() { order.push(1); });
observable.watch("topic", function listener2() { order.push(2); });
observable.watch("topic", function listener3() { order.push(3); });
observable.notify("topic");
expect(order[0]).toBe(1);
expect(order[1]).toBe(2);
expect(order[2]).toBe(3);
});
it("should continue publishing on all the listeners even if one of them fails", function () {
var observable = new Observable(),
order = [];
observable.watch("topic", function listener1() { order.push(1); });
observable.watch("topic", function listener2() { throw new Error("this listener fails"); });
observable.watch("topic", function listener3() { order.push(3); });
observable.notify("topic");
expect(order[0]).toBe(1);
expect(order[1]).toBe(3);
});
it("can bind the this object of a listener to a given object and pass multiple things on the topic", function () {
var observable = new Observable(),
message1,
message2,
message3,
context;
observable.watch("topic", function listener(something1, something2, something3) {
message1 = something1;
message2 = something2;
message3 = something3;
context = this;
}, this);
observable.notify("topic", "hello", "this is", "emily");
expect(message1).toBe("hello");
expect(message2).toBe("this is");
expect(message3).toBe("emily");
expect(context).toBe(this);
});
it("can remove a listener from a topic", function () {
var observable = new Observable(),
removed = true;
var handle = observable.watch("topic", function listener(something) {
removed = false;
});
// Remove the listener so it doesn't get called anymore
observable.unwatch(handle);
observable.notify("topic");
expect(removed).toBe(true);
});
it("can remove all listeners from a given topic", function () {
var observable = new Observable(),
topics = [];
observable.watch("topic1", function listener1() { topics.push("topic1"); });
observable.watch("topic1", function listener2() { topics.push("topic1"); });
observable.watch("topic2", function listener3() { topics.push("topic2"); });
observable.unwatchAll("topic1");
observable.notify("topic1");
observable.notify("topic2");
expect(topics.length).toBe(1);
expect(topics[0]).toBe("topic2");
});
it("can remove all listeners", function () {
var observable = new Observable(),
topics = [];
observable.watch("topic1", function listener1() { topics.push("topic1"); });
observable.watch("topic1", function listener2() { topics.push("topic1"); });
observable.watch("topic2", function listener3() { topics.push("topic2"); });
observable.unwatchAll();
observable.notify("topic1");
observable.notify("topic2");
expect(topics.length).toBe(0);
});
});
###Tools
describe("Tools is a set of tools commonly used in JavaScript applications", function () {
describe("Tools.getGlobal can retrieve the global object", function () {
it("returns the global object", function () {
expect(Tools.getGlobal()).toBe(__Global);
});
});
describe("Tools.mixin can add an object's properties to another object", function () {
it("takes the properties of the second object to mix them into the first one", function () {
var source = {c: 30, d: 40},
destination = {a: 10, b: 20};
Tools.mixin(source, destination);
expect(destination.a).toBe(10);
expect(destination.b).toBe(20);
expect(destination.c).toBe(30);
expect(destination.d).toBe(40);
});
it("overrides the destination's values with the source ones by default", function () {
var source = {c: 30, d: 40},
destination = {a: 10, b: 20, c: 25};
Tools.mixin(source, destination);
// The destination's c has been replaced by the source's one
expect(destination.c).toBe(30);
});
it("can prevent the desitnation's values to be replaced", function () {
var source = {c: 30, d: 40},
destination = {a: 10, b: 20, c: 25};
Tools.mixin(source, destination, true);
// The destination's c has been replaced by the source's one
expect(destination.c).toBe(25);
});
it("also returns the destination object", function () {
var source = {c: 30, d: 40},
destination = {a: 10, b: 20, c: 25};
expect(Tools.mixin(source, destination, true)).toBe(destination);
});
});
describe("Tools.count tells how many own properties an Object has", function () {
it("only counts own properties", function () {
var object = {a: 10, b: 20};
expect(Tools.count(object)).toBe(2);
});
});
describe("Tools.compareNumbers is useful for telling if a number if greater, equal or lower than another one", function () {
it("tells if a number is greater than another one", function () {
expect(Tools.compareNumbers(2.3, 2.2)).toBe(1);
});
it("tells if a number equals another one", function () {
expect(Tools.compareNumbers(2.2, 2.2)).toBe(0);
});
it("tells if a number is lower than another one", function () {
expect(Tools.compareNumbers(2.1, 2.2)).toBe(-1);
});
it("can ASC sort numbers when using Array.sort", function () {
var array = [0, 2, 9, 4, 1, 7, 3, 12, 11, 5, 6, 8, 10];
array.sort(Tools.compareNumbers);
expect(array[10]).toBe(10);
expect(array[11]).toBe(11);
});
});
describe("Tools.toArray transforms an array like object, like arguments or a nodeList to an actual array", function () {
it("transforms a list of arguments to an array", function () {
(function () {
var args = Tools.toArray(arguments);
expect(Array.isArray(args)).toBe(true);
})();
});
it("transforms a nodelist into an array", function () {
if (__Global.document) {
var all = Tools.toArray(document.querySelectorAll("*"));
expect(Array.isArray(all)).toBe(true);
}
});
});
describe("Tools.loop abstracts the difference between iterating over an object and an array", function () {
it("can iterate over an array", function () {
var array = [0, 1, 2, 3];
var _self = this;
Tools.loop(array, function (value, index, iterated) {
expect(iterated).toBe(array);
expect(array[index]).toBe(value);
// The context in which to run this function can also be given
expect(this).toBe(_self);
}, this);
});
it("can iterate over an array which length varies", function () {
var iterated = [1],
nbOfCalls = 0;
Tools.loop(iterated, function (value) {
if (nbOfCalls < 10) {
iterated.push(1);
nbOfCalls++;
}
});
expect(iterated.length).toBe(11);
});
it("can iterate over an object", function () {
var object = {a: 10, b: 20};
Tools.loop(object, function (value, key, obj) {
expect(object).toBe(obj);
expect(object[key]).toBe(value);
});
});
});
describe("Tools.objectsDiffs returns an object describing the differences between two objects", function () {
it("tells what was added in an array", function () {
var array1 = ["a", "b", "c"],
array2 = ["a", "b", "c", "d", "e"];
var diff = Tools.objectsDiffs(array1, array2);
// The third item of array2 was added
expect(diff.added[0]).toBe(3);
// The fourth item too
expect(diff.added[1]).toBe(4);
});
it("tells what was removed", function () {
var array1 = ["a", "b", "c"],
array2 = ["a", "b"];
var diff = Tools.objectsDiffs(array1, array2);
// The third item of array2 was deleted
expect(diff.deleted[0]).toBe(2);
});
it("tells what was updated", function () {
var array1 = ["a", "b", "c"],
array2 = ["a", "d", "e"];
var diff = Tools.objectsDiffs(array1, array2);
// The second item of array2 was updated
expect(diff.updated[0]).toBe(1);
// The third one too
expect(diff.updated[1]).toBe(2);
});
it("tells what remains unchanged", function () {
var array1 = ["a", "b", "c"],
array2 = ["a", "d", "e"];
var diff = Tools.objectsDiffs(array1, array2);
// The first item remains unchanged
expect(diff.unchanged[0]).toBe(0);
});
it("also works with objects", function () {
var object1 = { a: 10, b: 20, c: 30},
object2 = { b: 30, c: 30, d: 40};
var diff = Tools.objectsDiffs(object1, object2);
expect(diff.deleted[0]).toBe("a");
expect(diff.updated[0]).toBe("b");
expect(diff.unchanged[0]).toBe("c");
expect(diff.added[0]).toBe("d");
});
});
describe("Tools.setNestedProperty sets the property of an object nested in one or more objects", function () {
it("sets the property of an object deeply nested and creates the missing ones", function () {
var object = {};
Tools.setNestedProperty(object, "a.b.c.d.e.f", "emily");
expect(object.a.b.c.d.e.f).toBe("emily");
});
it("returns the value if the first parameter is not an object", function () {
expect(Tools.setNestedProperty("emily")).toBe("emily");
});
it("also works if there are arrays in the path, but it doesn't create an array", function () {
var object = {};
Tools.setNestedProperty(object, "a.b.c.0.d", "emily");
expect(object.a.b.c[0].d).toBe("emily");
expect(Array.isArray(object.a.b.c)).toBe(false);
});
});
describe("Tools.getNestedProperty gets the property of an object nested in other objects", function () {
it("gets the property of an object deeply nested in another one", function () {
var object = {b:{c:{d:{e:1}}}};
expect(Tools.getNestedProperty(object, "b.c")).toBe(object.b.c);
expect(Tools.getNestedProperty(object, "b.c.d.e")).toBe(1);
});
it("also works if an array is in the path", function () {
var object = {a: [{b: 1}]};
expect(Tools.getNestedProperty(object, "a.0.b")).toBe(1);
});
});
describe("Tools.closest finds the closest number to a base number in an array and returns its index", function () {
it("gets the closest number", function () {
expect(Tools.closest(10, [30, 5, 40, 20])).toBe(1);
expect(Tools.closest(25, [30, 5, 40, 20])).toBe(0);
expect(Tools.closest(30, [30, 5, 40, 20])).toBe(0);
expect(Tools.closest(45, [30, 5, 40, 20])).toBe(2);
});
it("gets the closest number that is greater", function () {
expect(Tools.closestGreater(10, [30, 5, 40, 20])).toBe(3);
expect(Tools.closestGreater(25, [30, 5, 40, 20])).toBe(0);
expect(Tools.closestGreater(30, [30, 5, 40, 20])).toBe(0);
expect(Tools.closestGreater(45, [30, 5, 40, 20])).toBeUndefined();
});
it("gets the closest number that is lower", function () {
expect(Tools.closestLower(10, [30, 5, 40, 20])).toBe(1);
expect(Tools.closestLower(25, [30, 5, 40, 20])).toBe(3);
expect(Tools.closestLower(30, [30, 5, 40, 20])).toBe(0);
expect(Tools.closestLower(45, [30, 5, 40, 20])).toBe(2);
});
});
});
###Store
describe("Store is an observable data structure that publishes events whenever it's updated", function () {
it("can store its data in an object", function () {
var store = new Store({});
store.set("key", "emily");
store.set("otherKey", 2);
expect(store.get("key")).toBe("emily");
expect(store.get("otherKey")).toBe(2);
expect(store.has("key")).toBe(true);
expect(store.del("key")).toBe(true);
expect(store.del("key")).toBe(false);
expect(store.has("key")).toBe(false);
});
it("can store data in an array", function () {
var store = new Store([]);
store.set(0, "emily");
store.set(1, 1);
expect(store.get(0)).toBe("emily");
expect(store.get(1)).toBe(1);
expect(store.del(0)).toBe(true);
expect(store.get(0)).toBe(1);
});
it("can be initialized with data", function () {
var store = new Store({a: 10});
expect(store.get("a")).toBe(10);
});
it("can be initialized two times with the same data but the data are not shared between them", function () {
var data = {a: 10},
store1 = new Store(data),
store2 = new Store(data);
store1.set("b", 20);
expect(store2.has("b")).toBe(false);
});
it("publishes events when a store is updated", function () {
var store = new Store([]),
itemAdded = false,
itemUpdated = false,
itemDeleted = false,
handle;
// Listening to the events uses the same API as the Observable
handle = store.watch("added", function (key) {
itemAdded = key;
}, this);
store.watch("updated", function (key) {
itemUpdated = key;
}, this);
store.watch("deleted", function (key) {
itemDeleted = key;
}, this);
store.set(0, "emily");
expect(itemAdded).toBe(0);
store.set(0, "olives");
expect(itemUpdated).toBe(0);
store.del(0);
expect(itemDeleted).toBe(0);
store.unwatch(handle);
});
it("publishes events when a value in the store is updated", function () {
var store = new Store([]),
spyNewValue,
spyOldValue,
spyEvent,
handle;
handle = store.watchValue(0, function (newValue, action, oldValue) {
spyNewValue = newValue;
spyOldValue = oldValue;
spyEvent = action;
}, this);
store.set(0, "emily");
expect(spyNewValue).toBe("emily");
expect(spyEvent).toBe("added");
store.set(0, "olives");
expect(spyNewValue).toBe("olives");
expect(spyEvent).toBe("updated");
expect(spyOldValue).toBe("emily");
store.unwatchValue(handle);
});
it("works the same with objects", function () {
var store = new Store({}),
spyNewValue,
spyOldValue,
spyEvent;
store.watchValue("key", function (newValue, action, oldValue) {
spyNewValue = newValue;
spyOldValue = oldValue;
spyEvent = action;
}, this);
store.set("key", "emily");
expect(spyNewValue).toBe("emily");
expect(spyEvent).toBe("added");
store.set("key", "olives");
expect(spyNewValue).toBe("olives");
expect(spyEvent).toBe("updated");
expect(spyOldValue).toBe("emily");
});
it("can update the property of an object nested in a store and publish an event", function () {
var store = new Store({
key: {}
}),
updatedValue = false;
store.watchValue("key", function (value) {
updatedValue = value;
}, this);
store.update("key", "a.b.c", "emily");
expect(updatedValue.a.b.c).toBe("emily");
});
it("can delete multiple items in one function call", function () {
var store = new Store(["a", "b", "c", "d", "e", "f"]);
store.delAll([0,1,2]);
expect(store.count()).toBe(3);
expect(store.get(0)).toBe("d");
expect(store.get(1)).toBe("e");
expect(store.get(2)).toBe("f");
});
it("can delete multiple properties in one function call", function () {
var store = new Store({a: 10, b: 20, c: 30});
store.delAll(["a", "b"]);
expect(store.count()).toBe(1);
expect(store.has("a")).toBe(false);
expect(store.has("b")).toBe(false);
expect(store.has("c")).toBe(true);
});
it("can compute properties from other properties", function () {
var store = new Store({a: 1000, b: 336}),
observedComputed;
store.compute("c", ["a", "b"], function () {
return this.get("a") + this.get("b");
}, store);
expect(store.get("c")).toBe(1336);
store.watchValue("c", function (value) {
observedComputed = value;
});
store.set("b", 337);
expect(store.get("c")).toBe(1337);
expect(observedComputed).toBe(1337);
});
it("can alter the inner data structure and publish changes when it's an array", function () {
var store = new Store([0, 2, 3]),
newValue;
store.watchValue(1, function (value) {
newValue = value;
});
// Splice can alter the store
store.alter("splice", 1, 0, 1); // [0,1,2,3]
expect(store.get(1)).toBe(1);
expect(newValue).toBe(1);
// Map doesn't alter it, just like calling map on any array
var newArray = store.alter("map", function (value) {
return value * 2;
});
expect(newArray[3]).toBe(6);
});
it("can also alter the inner structure and publish changes when it's an object", function () {
var store = new Store({a: 10});
expect(store.alter("hasOwnProperty", "a")).toBe(true);
});
it("can also directly call the methods of the inner structure without further publishing events", function () {
var store = new Store([0, 1, 2]);
expect(store.proxy("slice", 1, 2)).toEqual([1]);
});
it("has a function for iterating over it the same way being based on an object or an array", function () {
var store = new Store({a: 10, b: 20}),
calls = [];
store.loop(function () {
calls.push(arguments);
});
// Note that it's lucky that this test passes
// as loop doesn't guarantee the order in case of an object!
expect(calls[0][0]).toBe(10);
expect(calls[0][1]).toBe("a");
expect(calls[1][0]).toBe(20);
expect(calls[1][1]).toBe("b");
store = new Store(["a", "b"]);
calls = [];
store.loop(function () {
calls.push(arguments);
});
expect(calls[0][0]).toBe("a");
expect(calls[0][1]).toBe(0);
expect(calls[1][0]).toBe("b");
expect(calls[1][1]).toBe(1);
});
it("has a function for resetting the whole store", function () {
var store = new Store({a: 10}),
itemAdded;
// Calling reset fires the diff events
store.watch("added", function (key) {
itemAdded = key;
});
store.reset(["a"]);
expect(store.get(0)).toBe("a");
expect(itemAdded).toBe(0);
});
it("can return the jsonified version of itself", function () {
var store = new Store({a: undefined}),
jsonified;
expect(store.has("a")).toBe(true);
jsonified = store.toJSON();
expect(jsonified).toBe("{}");
});
it("can return it's internal structure", function () {
var store = new Store({a: 10}),
internal;
internal = store.dump();
expect(internal.a).toBe(10);
// The internal is not the object passed at init
expect(store).not.toBe(internal);
});
});
###StateMachine
describe("StateMachine helps you with the control flow of your apps by removing branching if/else", function () {
it("will call specific actions depending on the current state and the triggered event", function () {
var passCalled,
coinCalled,
stateMachine = new StateMachine("opened", {
// It has an 'opened' state
"opened": [
// That accepts a 'pass' event that will execute the 'pass' action
["pass", function pass(event) {
passCalled = event;
// And when done, it will transit to the 'closed' state
}, "closed"]
],
// It also has a 'closed' state
"closed": [
// That accepts a 'coin' event that will execute the 'coin' action
["coin", function coin(event) {
coinCalled = event;
// And when done, it will transit back to the 'opened' state
}, "opened"]
]
});
expect(stateMachine.getCurrent()).toBe("opened");
expect(stateMachine.event("nonExistingState")).toBe(false);
expect(stateMachine.event("pass", "hello")).toBe(true);
expect(passCalled).toBe("hello");
expect(stateMachine.getCurrent()).toBe("closed");
expect(stateMachine.event("coin", "2p")).toBe(true);
expect(coinCalled).toBe("2p");
expect(stateMachine.getCurrent()).toBe("opened");
});
it("executes the action in the given scope", function () {
var passThisObject,
coinThisObject,
scope = {},
stateMachine = new StateMachine("opened", {
"opened": [
["pass", function pass() {
passThisObject = this;
}, scope, "closed"]
],
"closed": [
["coin", function coin() {
coinThisObject = this;
}, scope, "opened"]
]
});
stateMachine.event("pass");
expect(passThisObject).toBe(scope);
stateMachine.event("coin");
expect(coinThisObject).toBe(scope);
});
it("can handle events that don't necessarily change the state", function () {
var coinCalled,
stateMachine = new StateMachine("opened", {
"opened": [
["pass", function pass() {
passThisObject = this;
}, "closed"],
["coin", function coin() {
coinCalled = true;
}]
],
"closed": [
["coin", function coin() {
coinThisbject = this;
}, "opened"]
]
});
stateMachine.event("coin");
expect(coinCalled).toBe(true);
expect(stateMachine.getCurrent()).toBe("opened");
});
it("can execute given actions upon entering or leaving a state", function () {
var onEnter,
onExit,
stateMachine = new StateMachine("opened", {
"opened": [
["pass", function pass() {
//
}, "closed"],
// Exit will be called upon leaving opened
["exit", function exit() {
onExit = true;
}]
],
"closed": [
// Whereas entry will be called upon entering the state
["entry", function entry() {
onEnter = true;
}],
["coin", function coin() {
//
}, "opened"]
]
});
stateMachine.event("pass");
expect(onExit).toBe(true);
expect(onExit).toBe(true);
expect(stateMachine.getCurrent()).toBe("closed");
});
it("can be advanced to a given state", function () {
var stateMachine = new StateMachine("opened", {
"opened": [
["pass", function pass() {
passThisObject = this;
}, "closed"]
],
"closed": [
["coin", function coin() {
coinThisObject = this;
}, "opened"]
]
});
expect(stateMachine.advance("")).toBe(false);
expect(stateMachine.advance("closed")).toBe(true);
expect(stateMachine.getCurrent()).toBe("closed");
expect(stateMachine.advance("opened")).toBe(true);
expect(stateMachine.getCurrent()).toBe("opened");
});
});
###Transport
describe("Transport hides and centralizes the logic behind requests", function () {
it("issues requests to request handlers", function () {
var onEndCalled = false;
var requestsHandlers = new Store({
// This function will handle the request specified by payload.
// It will call the onEnd request when it has received all the data
// It will call onData for each chunk of data that needs to be sent
myRequestHandler: function (payload, onEnd) {
if (payload == "whoami") {
onEnd("emily");
}
}
});
var transport = new Transport(requestsHandlers);
// Issue a request on myRequestHandler with "whoami" in the payload
transport.request("myRequestHandler", "whoami", function onEnd() {
onEndCalled = true;
});
expect(onEndCalled).toBe(true);
});
it("accepts objects as payloads", function () {
var requestsHandlers = new Store({
myRequestHandler: function (payload, onEnd) {
onEnd("Hi " + payload.firstname + " " + payload.lastname);
}
}),
transport,
response;
transport = new Transport(requestsHandlers);
transport.request("myRequestHandler", {
firstname: "olivier",
lastname: "scherrer"
}, function onEnd(data) {
response = data;
});
expect(response).toBe("Hi olivier scherrer");
});
it("can also listen to channels and receive data in several chunks", function () {
var requestsHandlers = new Store({
// When onEnd is called, no further data can be sent.
// But when the channel must no be closed, onData can be called instead
myRequestHandler: function (payload, onEnd, onData) {
onData("chunk1");
onData("chunk2");
onData("chunk3");
onEnd("chunk4");
}
}),
response = [];
var transport = new Transport(requestsHandlers);
transport.listen("myRequestHandler", {}, function onData(data) {
response.push(data);
});
expect(response.length).toBe(4);
expect(response[0]).toBe("chunk1");
expect(response[3]).toBe("chunk4");
});
it("can close a listening channel on the client end point", function () {
var aborted = false;
var requestsHandlers = new Store({
myRequestHandler: function () {
return function() {
aborted = true;
};
}
}),
transport = new Transport(requestsHandlers),
abort;
abort = transport.listen("myRequestHandler", "", function () {});
abort();
expect(aborted).toBe(true);
});
});
###Router
describe("Router determines the navigation in your application", function () {
it("can navigate to routes and pass arguments", function () {
var router = new Router();
var routeObserver1 = jasmine.createSpy(),
routeObserver2 = jasmine.createSpy(),
scope = {},
params = {};
router.set("route1", routeObserver1);
router.set("route2", routeObserver2, scope);
router.navigate("route1", params);
expect(routeObserver1.wasCalled).toBe(true);
expect(routeObserver1.mostRecentCall.args[0]).toBe(params);
expect(routeObserver2.wasCalled).toBe(false);
router.navigate("route2", params);
expect(routeObserver2.wasCalled).toBe(true);
expect(routeObserver2.mostRecentCall.args[0]).toBe(params);
expect(routeObserver2.mostRecentCall.object).toBe(scope);
});
it("publishes events when navigating to a new route", function () {
var router = new Router();
var observer = jasmine.createSpy(),
scope = {},
params = {};
router.watch(observer, scope);
router.set("route", function () {});
router.navigate("route", params);
expect(observer.wasCalled).toBe(true);
expect(observer.mostRecentCall.args[0]).toBe("route");
expect(observer.mostRecentCall.args[1]).toBe(params);
});
it("keeps track of the history while navigating", function () {
var router = new Router();
var observer = jasmine.createSpy();
router.watch(observer);
router.set("route1", function () {});
router.set("route2", function () {});
router.set("route3", function () {});
router.set("route4", function () {});
router.set("route5", function () {});
router.setMaxHistory(3);
router.navigate("route1");
router.navigate("route2");
router.back();
expect(observer.mostRecentCall.args[0]).toBe("route1");
router.forward();
expect(observer.mostRecentCall.args[0]).toBe("route2");
router.navigate("route3");
router.navigate("route4");
expect(router.go(-2)).toBe(true);
expect(observer.mostRecentCall.args[0]).toBe("route2");
expect(router.back()).toBe(false);
expect(router.forward()).toBe(true);
expect(observer.mostRecentCall.args[0]).toBe("route3");
router.navigate("route5");
expect(router.forward()).toBe(false);
router.back();
expect(observer.mostRecentCall.args[0]).toBe("route3");
});
it("can clear the history", function () {
var router = new Router();
router.set("route1");
router.set("route2");
router.navigate("route1");
router.navigate("route2");
router.clearHistory();
expect(router.back()).toBe(false);
});
it("can tell the depth of the history", function () {
var router = new Router();
router.set("route1", function () {});
router.navigate("route1");
router.navigate("route1");
router.navigate("route1");
router.navigate("route1");
router.navigate("route1");
expect(router.getHistoryCount()).toBe(5);
});
it("has a default max history of 10", function () {
var router = new Router();
expect(router.getMaxHistory()).toBe(10);
});
it("can remove a route", function () {
var router = new Router(),
handle;
handle = router.set("route1");
router.unset(handle);
expect(router.navigate("route1")).toBe(false);
});
});
###Promise
describe("Promise is a partially Promise/A+ compliant implementation", function () {
var Promise = require("emily").Promise;
it("calls the fulfillment callback within scope", function () {
var promise = new Promise(),
scope = {},
thisObj,
value;
promise.then(function (val) {
thisObj = this;
value = val;
}, scope);
promise.fulfill("emily");
expect(value).toBe("emily");
expect(thisObj).toBe(scope);
});
it("calls the rejection callback within a scope", function () {
var promise = new Promise(),
scope = {},
thisObj,
reason;
promise.then(null, function (res) {
thisObj = this;
reason = res;
}, scope);
promise.reject(false);
expect(reason).toBe(false);
expect(thisObj).toBe(scope);
});
it("can synchronise a promise with another one, or any thenable", function () {
var promise1 = new Promise(),
promise2 = new Promise(),
synched;
promise2.sync(promise1);
promise2.then(function (value) {
synched = value;
});
promise1.fulfill(true);
expect(synched).toBe(true);
});
it("can return the reason of a rejected promise", function () {
var promise = new Promise();
promise.reject("reason");
expect(promise.getReason()).toBe("reason");
});
it("can return the value of a fulfilled promise", function () {
var promise = new Promise();
promise.fulfill("emily");
expect(promise.getValue()).toBe("emily");
});
it("passes all the promise-A+ tests specs", function () {
expect('225 tests complete (6 seconds)').toBeTruthy();
});
});
- Update to shallow-diff 0.0.5
- Update to simple-loop 0.0.4
- Update to watch-notify 0.0.3
- Update to observable-store 0.0.5
####3.0.4 - 7 APR 2015
- Update to watch-notify 3.0.4
####3.0.3 - 28 MAR 2015
- Update nested-property to 0.0.6
####3.0.2 - 28 APR 2014
- Doc update
####3.0.1 - 27 APR 2014
- Remove unused docs, previous releases and browser builds. Use browserify to use Emily.js in the browser.
####3.0.0 - 27 APR 2014
- Already version 3.0.0! It doesn't change much, but every module has been extracted into its own module, and Emily just packs them together into a great library, because they work nicely together.
- It does have breaking changes though, the following, unused tools have been removed:
- Tools.jsonify which was removing unjsonifiable properties like functions and undefined properties
- Tools.compareObjects which was comparing the keys of two objects to tell if they were the same
####2.0.0 - 05 MAR 2014
- No changes since beta
####2.0.0 beta - 04 FEB 2014
- Completely removed the dependency on requirejs
- Promise.sync has been renamed to Promise.cast
####1.8.1 - 03 DEC 2013
- Add convenience method observable.once
####1.8.0 - 03 SEP 2013
- Store.reset publishes a "resetted" event when the store is resetted
- Store.reset publishes an "altered" event with the store is altered
####1.7.0 - 04 AUG 2013
- Adds router
####1.6.0 - 17 JUNE 2013
- Adds computed properties to the Store
####1.5.0 - 9 JUNE 2013
- Tools now has closest, closestGreater and closestLower for finding the number in an array that is the closest to a base number.
####1.4.0 - 13 MAY 2013
- Store.proxy now gives direct access to the data structure's methods without publishing diffs, which is much faster (useful for slice for instance)
####1.3.5 - 09 MAR 2013
- Added count alias for getNbItems in Store
- Added proxy alias for alter in Store
- Updated documentation, added integration tests
####1.3.4 - 03 MAR 2013
- Added advance to the state machine
####1.3.3 - 28 JAN 2013
- Added Store.dump
- When store publishes a change event, it publishes both the new and the previous value
####1.3.2 - 22 JAN 2013
- Fixed emily-server breaking olives
- Updated requirejs
####1.3.1 - 1 JAN 2013
- Promise has been updated to pass the promise/A+ specs according to promiseA+-tests
- Updated StateMachine so new transitions can be added on the fly
- Moved the CouchDB handler to CouchDB Emily Tools
####1.3.0 - 16 DEC 2012
- Promise has been updated to pass the promise/A specs according to promise-tests
- The build now includes the source files as you should be able to drop them into your application to decide how you want to load and optimize them
####1.2.0 - 07 OCT 2012
Removal of CouchDBStore - now part of CouchDB-Emily-Tools
Check out Olives for scalable MV* applications in the browser.