UNPKG

yeti

Version:

Yeti automates browser testing.

369 lines (306 loc) 9.56 kB
/** * Yeti Hub Client. * @module client */ "use strict"; var fs = require("graceful-fs"); var path = require("path"); var util = require("util"); var shallowCopy = require("./shallow-copy"); var EventEmitter2 = require("eventemitter2").EventEmitter2; var FileMatcher = require("./file-matcher"); var Blizzard = require("./blizzard"); /** * The ClientBatch represents a batch on the Hub. * * @class ClientBatch * @constructor * @extends EventEmitter2 * @param {BlizzardSession} session Connected Blizzard session to the Hub. * @param {Object} options Options (see `Client.createBatch()`). */ function ClientBatch(session, options) { var basedir = options.basedir, tests = options.tests, istanbul; if (!basedir && options.useProxy) { throw new Error("Basedir required."); } if (basedir && (basedir !== path.resolve(basedir))) { throw new Error("Basedir is not an absolute path!"); } if (!Array.isArray(tests)) { throw new Error("Array of tests required."); } EventEmitter2.call(this); this.session = session; this.basedir = basedir; this.tests = tests; if (options.instrument) { try { istanbul = require("istanbul"); } catch (ex) { throw new Error("Coverage requested but unable to load Istanbul"); } this.instrumenter = new istanbul.Instrumenter(); this.coverageMatcher = new FileMatcher({ extension: "js", excludes: options.coverageExcludes }); } this.id = null; this.batchSession = null; // Setup our events. this.once("ack", this.onAck.bind(this)); // Remove options.basedir before sending to the server. options = shallowCopy(options); delete options.basedir; this.session.emit("rpc.batch", options, this.emit.bind(this, "ack")); } util.inherits(ClientBatch, EventEmitter2); /** * Something went wrong. * @event error * @param {Error} error The error object. */ /** * The batch is complete. Browsers may be shutting down. * @event complete */ /** * The batch has finished, browsers have shut down. * No more events will be fired. * @event end */ /** * The agent given has completed testing. * @event agentComplete * @param {String} agent Agent name. */ /** * Test result from an agent. * @event agentResult * @param {String} agent Agent name. * @param {Object} details Test result details, in YUI Test JSON format. * @param {Object} [details.coverage] Test coverage details, if YUI Test Coverage instrumented code was present. */ /** * Error while handling an agent request. * @event agentError * @param {String} agent Agent name. * @param {Object} details Exception details. * @param {String} details.message Exception message. */ /** * Pedantic error while handling an agent request. * @event agentPedanticError * @param {String} agent Agent name. * @param {Object} details Exception details. * @param {String} details.message Exception message. */ /** * Uncaught JavaScript error from an agent. * @event agentScriptError * @param {String} agent Agent name. * @param {Object} details Exception details. * @param {String} details.url Exception URL. * @param {Number} details.line Exception line number. * @param {String} details.message Exception message. */ /** * @method onAck * @protected * @param {Error} err * @param {Number} id */ ClientBatch.prototype.onAck = function (err, id) { var self = this; if (err) { self.emit("error", err); } self.id = id; self.batchSession = self.session.createNamespace("batch" + self.id); self.batchSession.incomingBridge(self, "end"); self.batchSession.incomingBridge(self, "complete"); self.batchSession.incomingBridge(self, "dispatch"); self.batchSession.incomingBridge(self, "agentComplete"); self.batchSession.incomingBridge(self, "agentResult"); self.batchSession.incomingBridge(self, "agentBeat"); self.batchSession.incomingBridge(self, "agentProgress"); self.batchSession.incomingBridge(self, "agentError"); self.batchSession.incomingBridge(self, "agentPedanticError"); self.batchSession.incomingBridge(self, "agentScriptError"); self.once("end", function () { self.batchSession.unbind(); }); self.provideTests(); }; /** * Setup BlizzardNamespace event listener that will * serve local test files under this.basedir. * * @method provideTests * @protected */ ClientBatch.prototype.provideTests = function () { var self = this; self.batchSession.on("request.clientFile", function (args, reply) { var file = args[0], completer = reply; if (!self.basedir) { reply("Not permitted."); return; } if (file[0] !== "/") { // Path is relative, resolve it to an absolute path. file = path.resolve(self.basedir, file); } // The file path must be inside the basedir. if (file.indexOf(self.basedir) !== 0) { reply("Filename provided is not in basedir!"); return; } // Instrument JS for code coverage if (self.instrumenter && self.coverageMatcher.match(file) && fs.existsSync(file)) { completer = function (err, data) { try { data = data.toString("utf8"); data = self.instrumenter.instrumentSync(data, file); } catch (err) { console.warn("[yeti] Unable to instrument file " + file + ": " + err); } return reply(null, new Buffer(data, "utf8")); }; } fs.readFile(file, completer); }); }; /** * The Client submits test batches to a Yeti Hub and tracks their progress. * * @class Client * @constructor * @extends EventEmitter2 * @param {String} url Yeti Hub HTTP URL. */ function Client(url) { EventEmitter2.call(this); this.session = null; this.url = url; this.blizzard = new Blizzard(); this.blizzard.on("error", this.handleBlizzardError.bind(this)); } util.inherits(Client, EventEmitter2); /** * Something went wrong. * @event error * @param {Error} error The error object. */ /** * An agent connected to the Hub. * @event agentConnect * @param {String} agent Agent name. */ /** * An agent requested a page (test or capture) from the connected Hub. * @event agentSeen * @param {String} agent Agent name. */ /** * An agent disconnected from the Hub. * @event agentDisconnect * @param {String} agent Agent name. */ /** * Connect to the Yeti Hub. * * @method connect * @param {Function} cb Callback function. */ Client.prototype.connect = function (cb) { var self = this; self.blizzard.connect(self.url, function (err, session) { if (err) { cb(err); return; } session.incomingBridge(self, "agentConnect"); session.incomingBridge(self, "agentSeen"); session.incomingBridge(self, "agentDisconnect"); self.session = session; cb(err); }); }; /** * Disconnect from the Yeti Hub. * * @method end */ Client.prototype.end = function () { if (!this.session) { throw new Error("Session not started."); } this.session.end(); }; /** * @method handleBlizzardError * @protected */ Client.prototype.handleBlizzardError = function (err) { if (err.code === "ECONNRESET") { this.emit("error", new Error("Server does not speak Yeti's protocol. Version mismatch?")); return; } this.emit("error", err); }; /** * Create and submit a batch of tests to the Hub. * * @method createBatch * @param {Object} config Batch information. * @param {String[]} config.tests Tests. Either relative paths to `config.basedir` or URL pathnames. * @param {String} [config.basedir] Root path for serving tests. Required if `useProxy` is true or not provided. * @param {String} [config.query] Query string additions for test URLs. * @param {Number} [config.timeout] Per-test timeout in seconds. Default is 45 seconds. * If no activity occurs before the timeout, the next test is loaded. * @param {Boolean} [config.useProxy] True if tests are filenames to proxy to the Hub. * false if they are literal URL pathnames. * If not provided, defaults to true. * @param {Boolean} [config.instrument] True if JavaScript files should be instrumented with Istanbul. * @return {ClientBatch} batch The new ClientBatch object. */ Client.prototype.createBatch = function (config) { if (!this.session) { throw new Error("Not connected. Call connect() before creating a batch."); } config = shallowCopy(config); if (undefined === config.useProxy) { // Use the proxy by default. config.useProxy = true; } return new ClientBatch(this.session, config); }; /** * Get an array of available agents on the Hub. * * @method getAgents * @param {Function} cb Callback with agent names. */ Client.prototype.getAgents = function (cb) { this.session.emit("rpc.agents", cb); }; /** * @class exports * @static */ exports.Client = Client; /** * Create a new Yeti Client instance. * * @method createClient * @param {String} url Yeti Hub HTTP URL. * @return {Client} client Yeti Client instance. */ exports.createClient = function (url) { return new Client(url); };