server.js | |
---|---|
var express = require("express");
var connect = require("connect");
var http = require("http");
var events = require("events");
var sendfiles = require("./sendfiles").sendfiles;
var ui = require("./ui");
var visitor = require("./visitor");
var signal = require("./signal");
var pkg = require("./package"); | |
Return a random whole number as a string with | function makeId () {
return (Math.random() * 0x1000000|0) + "";
}
var emitterRegistry = {}; // by port
var cachebuster = makeId(); |
Returns a JSON string of all property-value pairs
of | function jsonize (req, keys) {
var o = {};
keys.forEach(function (k) {
var v = req.param(k);
if (v) o[k] = v;
});
return JSON.stringify(o);
}
function serveExpress (port, path, cb) { |
Create an | var tests = new events.EventEmitter;
var app = express.createServer(
connect.methodOverride(),
connect.bodyDecoder()
);
app.set("views", __dirname + "/views");
app.set("view engine", "jade"); |
Use our version of Jade. | app.register(".jade", require("jade"));
app.get("/", function (req, res) {
tests.emit("visitor", req.ua);
var json = jsonize(req, ["transport"]);
res.header("Expires", "0");
res.header("Pragma", "no-cache");
res.header("Cache-Control", "no-cache");
res.render("index", {
locals : {
bootstrap : "YETI.start(" + json + ")",
yeti_version : pkg.readPackageSync().version
}
});
});
var testIds = {};
var testResults = {};
var testQueue = {}; |
Add a new test. Called by the CLI in | app.put("/tests/add", function (req, res) {
if (!req.body.tests.length) return res.send(500);
var urls = [];
var id = makeId();
req.body.tests.forEach(function (url) {
urls.push("/project/" + id + url);
});
ui.debug("/tests/add: registered batch", id);
if (tests.listeners("add").length) {
tests.emit("add", id, urls);
} else {
testQueue[id] = urls;
}
res.send(id);
}); |
Comet middleware. Sends a response when a test comes in. | function wait (req, responseCallback) {
function ADDCB (id, urls) {
ui.debug("/tests/wait: send", urls);
responseCallback({
tests : urls
});
testIds[id] = 1;
}
function SHUTDOWNCB (cb) {
ui.debug("/tests/wait: shutdown!", port);
req.on("end", cb); |
Prevent browsers from reconnecting. | responseCallback({shutdown:true});
req.emit("end");
}
tests.on("add", ADDCB);
tests.on("shutdown", SHUTDOWNCB);
req.connection.on("close", function () { |
No longer able to write data here. | tests.removeListener("add", ADDCB);
tests.removeListener("shutdown", SHUTDOWNCB);
}); |
TODO: Delete stale tests from testQueue? | if (testQueue) {
for (
var id in testQueue
) tests.emit("add", id, testQueue[id]);
testQueue = {};
}
} |
EventSource-powered Comet, called by the browser. | app.get("/tests/wait", function (req, res) {
res.writeHead(200, {
"Content-Type" : "text/event-stream"
});
wait(req, function (data) {
res.write("data: " + JSON.stringify(data) + "\n\n");
});
}); |
XMLHttpRequest-powered Comet, called by the browser. | app.post("/tests/wait", function (req, res) {
wait(req, function (data) {
res.send(data);
});
}); |
Respond when test results for the given batch ID arrive.
Called by the CLI in | app.get("/status/:id", function (req, res) {
var id = req.params.id;
if (id in testIds) {
if (id in testResults) {
var results = testResults[id].shift();
if (results) {
return res.send(results);
} else { |
nothing in the queue | delete testResults[id]; |
fallthrough to the test listener | }
}
tests.on(id, function (results) {
res.send(results);
});
} else {
res.send("Nothing is listening to this batch. At least one browser should be pointed at the Yeti server.", 404);
}
}); |
Recieves test results from the browser. | app.post("/results", function (req, res) {
var result = JSON.parse(req.body.results);
result.ua = req.body.useragent;
var id = req.body.id;
ui.debug("/results:", id, " has results from: " + result.ua);
if (id in testIds) {
if (tests.listeners(id).length) {
tests.emit(id, result);
} else {
if ( ! (id in testResults) ) {
testResults[id] = [];
}
testResults[id].push(result);
}
} else {
ui.results(result);
} |
Advance to the next test immediately. We do this here because determining if an iframe has loaded is much harder on the client side. Takes advantage of the fact that we're on the same domain as the parent page. | res.send("<script>parent.parent.YETI.next()</script>");
}); |
File Server | var projectSend = function (res, file, appendString, nocache, prependString) {
sendfiles.call(
res,
[file],
appendString,
null, // callback
{
prependString : prependString,
cache : !nocache
}
);
};
app.get('/project/*', function (req, res) {
var nocache = false;
var splat = req.params.pop().split("/");
if (splat[0] in testIds) {
splat.shift();
nocache = true; // using a unique url
}
if (splat[0] === "") splat.shift(); // stupid leading slashes
splat = splat.join("/");
var file = "/" + decodeURIComponent(splat); |
The requested file must begin with our cwd. | if (file.indexOf(path) !== 0) { |
The file is outside of our cwd. Reject the request. | ui.log(ui.color.red("[!]")
+ " Rejected " + file
+ ", run in the directory to serve"
+ " or specify --path.");
return res.send(403);
}
if (/^.*\.html?$/.test(req.url)) { |
Inject a test reporter into the test page. | projectSend(
res, file,
"<script src=\"/dyn/" + cachebuster
+ "/inject.js\"></script><script>"
+ "$yetify({url:\"/results\"});</script>",
nocache
);
} else { |
Everything else goes untouched. | projectSend(res, file, "", nocache);
}
});
var incSend = function (res, name, nocache) {
sendfiles.call(
res,
[__dirname + "/../inc/" + name],
"", // appendString
null, // callback
{
cache : !nocache
}
);
};
app.get("/inc/*", function (req, res) {
incSend(res, req.params);
});
app.get("/dyn/:cachebuster/*", function (req, res) {
incSend(res, req.params, true);
});
app.get("/favicon.ico", function (req, res) {
incSend(res, "favicon.ico", true);
}); |
Start the server.
Workaround Express and/or Connect bugs
that strip out the | http.Server.prototype.listen.call(app, port, null, cb); |
Publish the | emitterRegistry[port] = tests;
return app;
} |
Handle the CLI server start request. Called from | function fromConfiguration (config) {
var cb = config.callback;
cb = cb || null;
var app = serveExpress(config.port, config.path, cb);
var baseUrl = "http://" + config.host + ":" + config.port;
var urls = visitor.composeURLs(
baseUrl,
"project" + config.path,
config.files
);
if (urls.length) return visitor.visit(
config.browsers,
urls
);
signal.listen();
ui.log("Yeti will only serve files inside " + config.path);
ui.log("Visit " + ui.color.bold(baseUrl) + ", then run:");
ui.log(" yeti <test document>");
ui.log("to run and report the results.");
if (config.forceVisit) {
ui.log("Running tests locally with: " + config.browsers.join(", "));
return visitor.visit(
config.browsers,
[baseUrl]
);
}
return app;
} |
Get the cachebuster for unit tests. | exports.getCachebuster = function () {
return cachebuster;
}; |
Get the | exports.getEmitterForPort = function (port) {
return emitterRegistry[port];
} |
Get the ports we've used for unit tests. | exports.getPorts = function () {
return Object.keys(emitterRegistry);
}
exports.fromConfiguration = fromConfiguration;
exports.serve = serveExpress;
|