API Docs for: 0.1.0

lib/plumbing/ServerEnvironment.js

"use strict";

var root = "../..";
var parts = require(root + "/deps/parts");
var ilk = require(root + "/deps/ilk");
var Gabarito = require(root + "/lib/gabarito");

var Environment = require("./Environment");
var url = require("url");
var path = require("path");

module.exports = ilk.tokens(function (
    results,
    server,
    complete,
    http,
    fs,

    startServer,
    dispatchBrowser,
    ditchBrowser,
    routeRequest,
    closeServer,
    deliverBrowserRunner,
    deliverAssets,
    deliverFile,
    receiveEvent,
    tellReporters,
    concatFiles,
    whenFinished
) {

    /**
     * The server environment provides a simple http server to serve the test
     * files and run the tests themselves within a browser.
     *
     * An implementation must implement a way of dispatching a browser to
     * navigate to "http://localhost:1432" and it may implement a method to
     * close the browser when the gabarito has finished.
     *
     * @class gabarito.plumbing.ServerEnvironment
     * @extends gabarito.plumbing.Environment
     * @constructor
     *
     * @param {HTTP} [http] The node http library
     * @param {FileSystem} [fs] The node file system library
     */
    return Environment.descend(function (pHttp, pFs) {
        server.mark(this);
        results.mark(this);
        complete.mark(this, null);

        http.mark(this, pHttp || require("http"));
        fs.mark(this, pFs || require("fs"));
    }).

    proto(whenFinished, function (done) {
        this[complete] = function () {
            this[complete] = null;
            this[ditchBrowser](done);
        };
    }).

    /**
     * This method may be implemented in order to clean up any browser
     * reference.
     *
     * It must call the done function afterwards.
     *
     * @method ditchBrowser
     * @for gabarito.plumbing.ServerEnvironment
     * @protected
     *
     * @param {function} done
     */
    proto(ditchBrowser, function (done) {
        done();
    }).

    proto(startServer, parts.that(function (that, files, reporters, done) {
        this[server] = this[http].createServer(function (req, res) {
            that[routeRequest](req, res, files, reporters);
        });

        this[server].listen(1432, "0.0.0.0", function () { done(); });
    })).

    proto(routeRequest, function (req, res, files, reporters) {
        switch (url.parse(req.url).pathname) {
        case "/"          : return this[deliverBrowserRunner](req, res, files);
        case "/assets.js" : return this[deliverAssets](req, res);
        case "/event"     : return this[receiveEvent](req, res, reporters);
        default           : return this[deliverFile](req, res, files);
        }
    }).

    proto(deliverBrowserRunner, function (req, res, files) {
        var hooks =
            "var queue = [];" +
            "var sending = false;" +
            "var parts = gabarito.parts;" +

            "var normalize = function (v) {" +
                "if (v instanceof Error) {" +
                    "return {" +
                        "name: v.name," +
                        "toString: v.toString()," +
                        "stack: v.stack," +
                        "message: v.message" +
                    "};" +
                "} else if (parts.isArray(v)) {" +
                    "return parts.map(v, normalize);" +
                "} else if (parts.isObject(v)) {" +
                    "var o = {};" +
                    "parts.forEach(v, function (v, p) {" +
                        "o[p] = normalize(v);" +
                    "});" +
                    "return o;" +
                "}" +
                "return v;" +
            "};" +

            "var emit = parts.args(function (args) {" +
                "queue.push(args);" +
                "if (!sending) {" +
                    "sending = true;" +
                    "send();" +
                "}" +
            "});" +

            "var send = function () {" +
                "var args = queue[0];" +
                "var xhr = new XMLHttpRequest();" +
                "xhr.onreadystatechange = function () {" +
                    "if (xhr.readyState === 4) {" +
                        "xhr = null;" +
                        "queue.shift();" +
                        "if (queue.length !== 0) {" +
                            "send();" +
                        "} else { " +
                            "sending = false;" +
                        "}" +
                    "}" +
                "};" +
                "xhr.open(\"POST\", \"/event\", true);" +
                "xhr.send(JSON.stringify(normalize(args)));" +
            "};" +

            "var events = " +
                    JSON.stringify(Gabarito.constant("EVENTS")) + ";" +

            "parts.forEach(events, function (e) {" +
                "gabarito.on(e, parts.args(function (args) {" +
                    "emit.apply(null, [e].concat(args)); " +
                "}));" +
            "});" +

            "gabarito.verify();";

        var html =
            "<!DOCTYPE HTML PUBLIC \"-" +
                    "//W3C//DTD HTML 4.01 Transitional//EN\">" +

            "<html>" +
                "<head>" +
                    "<title>gabarito runner</title>" +
                    "<meta charset=\"UTF-8\">" +
                    "<script src=\"/assets.js\"></script>" +
                        files.map(function (f) {
                            return "<script src=\"" +
                                    path.join("f", f) + "\"></script>";
                        }).join("") +
                        "<script>(function () {" + hooks + "}());</script>" +
                    "</head>" +
                "<body>" +
                    "<p>Running...</p>" +
                "</body>" +
            "</html>";

        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(html);
    }).

    proto(concatFiles, parts.that(function (that, files) {
        return files.map(function (f) { return that[fs].readFileSync(f); }).
                join("\n");
    })).

    proto(deliverAssets, function (req, res) {
        var data = this[concatFiles]([
            require.resolve(root + "/deps/json2.js"),
            require.resolve(root + "/deps/parts"),
            require.resolve(root + "/deps/ilk"),
            require.resolve("../gabarito")
        ]);

        res.writeHead(200, { "Content-Type": "text/javascript" });
        res.end(data);
    }).

    proto(deliverFile, function (req, res, files) {
        var location = url.parse(req.url);
        var n = location.pathname;
        var f = (/^\/r\//).test(n)?
                path.join(process.cwd(), n.replace(/^\/r\/(.+)/, "$1")):
                n.replace(/^\/f(.+)/, "$1");

        this[fs].readFile(f, function (err, file) {
            if (err) {
                res.statusCode = 404;
                res.end();
            } else {
                res.writeHead(200, { "Content-Type": "text/javascript" });
                res.end(file);
            }
        });
    }).

    proto(receiveEvent, parts.that(function (that, req, res, reporters) {
        var post = [];
        req.on("data", function (chunk) { post.push(chunk); });
        req.on("end", function () {
            var data = post.join("");
            var args = JSON.parse(data);
            var event = args.shift();

            that[tellReporters](reporters, event, args);

            if (event === "complete") {
                that[results] = args[0];
                parts.work(function () { that[complete](); });
            }
        });

        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end();
    })).

    proto(tellReporters, parts.that(function (that, reporters, event, args) {
        reporters.forEach(function (r) {
            r[event].apply(r, [that].concat(args));
        });
    })).

    proto(closeServer, function (done) {
        this[server].close(function () { done(); });
        this[server] = undefined;
    }).

    /**
     * This method must be implemented in order to dispatch the browser to
     * navigate to "http://localhost:1432".
     *
     * The done function must be called afterwards.
     *
     * @method dispatchBrowser
     * @for gabarito.plumbing.ServerEnvironment
     * @protected
     *
     * @param {function} done
     */
    proto(dispatchBrowser, function (done) {
        throw new Error("Unimplemented method.");
    }).

    proto({

        dispatch: parts.that(function (that, files, reporters, done) {
            this[startServer](files, reporters, function () {
                that[dispatchBrowser](function () {
                    that[whenFinished](function () {
                        that[closeServer](function () {
                            done(that[results]);
                        });
                    });
                });
            });

        })
    }).

    shared({
        dispatchBrowser: dispatchBrowser,
        ditchBrowser: ditchBrowser
    });
});