A set of modules for building scalable web applications
JavaScript
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
specs
src
tools/promise-test
.gitignore
LICENSE
README.md
package.json

README.md

##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.