UNPKG

yeti

Version:

Yeti automates browser testing.

369 lines (294 loc) 10.4 kB
"use strict"; var assert = require("assert"); var util = require("util"); var EventEmitter2 = require("../event-emitter"); var EventYoshi = require("eventyoshi"); var TestSpecification = require("./test-specification"); var TestServer = require("./http/test-server"); var WebDriverCollection = require("./webdriver-collection"); /** * A Batch represents a collection of tests on the Hub. * * @class Batch * @constructor * @param {Object} options Options * @param {AllBatches} options.allBatches * @param {Number} options.id Batch ID. * @param {BlizzardSession} options.session Hub session. * @param {Object} options.spec Test specification, see constructor for TestSpecification. */ function Batch(options) { this.allBatches = options.allBatches; this.id = options.id; this.session = options.session; var mountpoint = this.allBatches.hub.mountpoint; if (mountpoint === "/") { mountpoint = ""; } options.spec.mountpoint = mountpoint; options.spec.batchId = options.id; this.spec = new TestSpecification(options.spec); this.testServer = new TestServer( '<script src="' + mountpoint + '/agent/public/inject.js"></script>' ); EventEmitter2.call(this); this.batchSession = this.session.createNamespace("batch" + this.id); this.targets = {}; this.runningTargets = {}; this.targetEmitter = new EventYoshi(); this.setupAgentEmitter(); this.setupCleanupEvents(); this.requestedCapabilities = options.requestedCapabilities; this.agentIdWhitelist = {}; } util.inherits(Batch, EventEmitter2); /** * Prepare to handle the end of our Yeti Client session. * * @method setupCleanupEvents * @private */ Batch.prototype.setupCleanupEvents = function () { var self = this; // Our Yeti Client, who has our test data, has died. self.session.on("end", function handleDeadClient() { // TODO: Place into an abort() method. self.destroy(); }); }; Batch.prototype.report = function (event, target, data) { this.batchSession.emit("rpc." + event, target.getName(), data); }; Batch.prototype.destroy = function () { var self = this; if (self.destroyed) { return; } function completer() { self.emit("end"); self.batchSession.emit("rpc.end"); self.batchSession.unbind(); self.allBatches = null; self.batchSession = null; self.runningTargets = null; self.targets = null; self.spec = null; self.session = null; self.testServer = null; } self.destroyed = true; // Return all browsers to the capture page. // Important: remove targetEmitter's targets Object.keys(self.runningTargets).forEach(function unbind(id) { var target = self.targets[id]; self.targetEmitter.remove(target); target.dispatch(self.id, TestSpecification.empty()); }); if (self.managedBrowsers) { self.managedBrowsers.quit(completer); delete self.managedBrowsers; } else { completer(); } }; Batch.prototype.completeTarget = function (target) { var id = target.getId(); delete this.runningTargets[id]; this.targetEmitter.remove(target); if (Object.keys(this.runningTargets).length === 0) { this.emit("complete"); this.report("complete", target); this.destroy(); } }; Batch.prototype.proxyEvent = function (yoshiEvent, selfEvent) { var self = this; if (!selfEvent) { selfEvent = yoshiEvent; } assert(self.targetEmitter.listeners(yoshiEvent).length === 0, "This event was already proxied. EventYoshi cannot handle more than 1 listener."); self.targetEmitter.on(yoshiEvent, function (data) { // Send the event over the wire first. // Then handle it ourselves. If we don't, // complete could be sent before targetComplete. self.report(selfEvent, this.child, data); self.emit(selfEvent, this.child, data); }); }; Batch.prototype.setupAgentEmitter = function () { var self = this; // Proxy these events from Targets // to our BlizzardSession. // TODO: Update Client to use these new events // Update docs to use these new events // Remove legacy events after 0.2.x /* * XXX Unable to use these because of a bug in EventYoshi, see below. this.proxyEvent("complete", "targetComplete"); this.proxyEvent("results", "targetResult"); this.proxyEvent("beat", "targetBeat"); this.proxyEvent("progress", "targetProgress"); this.proxyEvent("agentError", "targetError"); // FIXME: Change target to emit targetError? this.proxyEvent("disconnect", "targetDisconnect"); this.proxyEvent("scriptError", "targetScriptError"); */ // LEGACY EVENT HANDLERS // Targets used to be called AgentGroups, // which used to be not groupable at all and // were simply Agents. Provide these events // for 0.2.x API compatibility. this.proxyEvent("complete", "agentComplete"); this.proxyEvent("results", "agentResult"); this.proxyEvent("beat", "agentBeat"); this.proxyEvent("progress", "agentProgress"); this.proxyEvent("agentError", "agentError"); this.proxyEvent("disconnect", "agentDisconnect"); this.proxyEvent("scriptError", "agentScriptError"); // Attaching multiple .on listeners to the targetEmitter // can trigger a bug in eventyoshi@0.1.2, so here we // listen to our re-emitted agentComplete event setup // earlier by proxyEvent. self.on("agentComplete", function (target) { self.completeTarget(target); }); // This event means one browser disconnected, // perhaps not the entire target. self.on("agentDisconnect", function (target, agent) { if (self.managedBrowsers) { self.managedBrowsers.restart(agent.id, function (err) { if (err) { console.warn("[Yeti] Unable to restart browser", agent.ua, err); } }); } }); }; Batch.prototype.getId = function () { return this.id; }; Batch.prototype.allowAgentId = function (id) { this.agentIdWhitelist[id] = 1; }; Batch.prototype.disallowAgentId = function (id) { delete this.agentIdWhitelist[id]; }; Batch.prototype.getAgentWhitelist = function () { return Object.keys(this.agentIdWhitelist); }; Batch.prototype.launchAndDispatch = function (reply) { var self = this, address, remote, queue; self.managedBrowsers = new WebDriverCollection({ batch: self, hub: self.allBatches.hub, browsers: self.spec.launchBrowsers, webdriver: self.spec.webdriver }); self.managedBrowsers.launch(function (err) { reply(err, self.id); // TODO destroy self on error self.dispatch(); }); }; Batch.prototype.dispatch = function () { if (this.destroyed) { return false; } // Freeze the current available targets. // TODO: Only select targets asked for. var targets, agents, allAgents = this.allBatches.allAgents, targetNames = [], mountpoint = this.allBatches.hub.mountpoint, self = this; if (self.managedBrowsers) { agents = allAgents.getAllById(self.managedBrowsers.getAllAgentIds()); } else { agents = allAgents.getAvailableAgents(); } targets = allAgents.createBatchTargetsFromAgents(self, agents); targetNames = targets.map(function (target) { return target.getName(); }); self.batchSession.emit("rpc.dispatch", targetNames); self.debug("dispatch targets:", targets); targets.forEach(function (target) { var id = target.getId(); if (self.destroyed) { // We were destroyed on the same tick. return; } self.targets[id] = target; self.runningTargets[id] = true; self.targetEmitter.add(target); allAgents.bindTarget(target); target.dispatch(self.id, self.spec); }); }; Batch.prototype.getFile = function (filename, cb) { this.batchSession.emit("rpc.clientFile", filename, cb); }; Batch.prototype.getAgent = function (agentId) { var targetId, agent; for (targetId in this.targets) { agent = this.targets[targetId].getAgent(agentId); if (agent) { break; } } return agent; }; Batch.prototype.handleFileRequest = function (server, agentId, filename) { var agent, fileInBatch, batch = this; if (agentId) { agent = batch.getAgent(agentId); } fileInBatch = batch.spec.tests.some(function (test) { return test === filename; }); this.getFile(filename, function (err, buffer) { var test; if (err) { if (agent) { // If this file is in the current test batch, // redirect to the next test. // Otherwise, send a 404. // Note: calling nextURL has side effects // and it may fire the complete event. if (fileInBatch) { batch.report("agentError", agent, { message: "Unable to serve the test: " + filename }); if (agent.target) { test = agent.target.tests.getByUrl(filename); test.setResults(true); test.setExecuting(false); } server.res.writeHead(302, { "Location": agent.nextURL() }); server.res.end(); } else { batch.report("agentPedanticError", agent, { message: "Unable to serve the file: " + filename + ", ignoring" }); server.res.message(404); } } else { server.res.message(500, "Unable to locate the requested agent."); } return; } if (agent) { batch.debug("Recording ping for agentId =", agentId, "for file =", filename); agent.ping(); } batch.testServer.serve(server, filename, fileInBatch, buffer); }); }; module.exports = Batch;