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(); });});
Changelog
3.0.7 - 28 AUG 2015
- Update to shallow-diff 0.0.5
- Update to simple-loop 0.0.4
3.0.6 - 12 APR 2015
- Update to watch-notify 0.0.3
3.0.5 - 7 APR 2015
- 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
Going further
Check out Olives for scalable MV* applications in the browser.