API Docs for: 0.0.2
Show:

File: index.js

/*!
 *
 * Copyright (c) 2013 Sebastian Golasch
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

'use strict';

// ext. libs
var fs = require('fs');
var Q = require('q');

// int. libs
var WD = require('dalek-internal-webdriver');
var Browser = require('./lib/browser');

/**
 * Initializes the sauce labs driver & the remote browser instance
 *
 * @param {object} opts Initializer options
 * @constructor
 */

var Sauce = function (opts) {
  // get the browser configuration & the browser module
  var browserConf = {name: null};
  var browser = opts.browserMo;

  // prepare properties
  this._initializeProperties(opts);

  // create a new webdriver client instance
  this.webdriverClient = new WD(browser, this.events);

  // listen on browser events
  this._startBrowserEventListeners(browser);

  // assign the current browser name
  browserConf.name = this.browserName;

  // store desired capabilities of this session
  this.desiredCapabilities = browser.getDesiredCapabilities(this.browserName, this.config);
  this.browserDefaults = browser.driverDefaults;
  this.browserDefaults.status = browser.getStatusDefaults(this.desiredCapabilities);
  this.browserData = browser;

  // set auth data
  var driverConfig = this.config.get('driver.sauce');
  browser.setAuth(driverConfig.user, driverConfig.key);

  // launch the browser & when the browser launch
  // promise is fullfilled, issue the driver:ready event
  // for the particular browser
  browser
    .launch(browserConf, this.reporterEvents, this.config)
    .then(this.events.emit.bind(this.events, 'driver:ready:sauce:' + this.browserName, browser));
};

/**
 * This module is a driver plugin for [DalekJS](//github.com/dalekjs/dalek).
 * It connects Daleks testsuite with the remote testing environment of [Sauce Labs](https://saucelabs.com).
 *
 * The driver can be installed with the following command:
 *
 * ```bash
 * $ npm install dalek-driver-sauce --save-dev
 * ```
 *
 * You can use the driver by adding a config option to the your [Dalekfile](/docs/config.html)
 *
 * ```javascript
 * "driver": ["sauce"]
 * ```
 *
 * Or you can tell Dalek that it should run your tests via sauces service via the command line:
 *
 * ```bash
 * $ dalek mytest.js -d sauce
 * ```
 *
 * In order to run your tests within the Sauce Labs infrastructure, you must add your sauce username & key 
 * to your dalek configuration. Those two parameters must be set in order to get this driver up & running.
 * You can specifiy them within your [Dalekfile](/docs/config.html) like so:
 *
 * ```javascript
 * "driver.sauce": {
 *   "user": "dalekjs",
 *   "key": "aaaaaa-1234-567a-1abc-1br6d9f68689"
 * }
 * ```
 *
 * It is also possible to specify a set of other extra saucy parameters like `name` & `tags`:
 *
 * ```javascript
 * "driver.sauce": {
 *   "user": "dalekjs",
 *   "key": "aaaaaa-1234-567a-1abc-1br6d9f68689",
 *   "name": "Guineapig",
 *   "tags": ["dalek", "testproject"]
 * }
 * ```
 *
 * If you would like to have a more control over the browser/OS combinations that are available, you are able 
 * to configure you custom combinations:
 *
 * ```javascript
 * "browsers": [{
 *   "chrome": {
 *     "platform": "OS X 10.6",
 *     "actAs": "chrome",
 *     "version": 27
 *   },
 *   "chromeWin": {
 *     "platform": "Windows 7",
 *     "actAs": "chrome",
 *     "version": 27
 *   },
 *   "chromeLinux": {
 *     "platform": "Linux",
 *     "actAs": "chrome",
 *     "version": 26
 *   }
 * ```
 * 
 * You can then call your custom browsers like so:
 *
 * ```bash
 * $ dalek mytest.js -d sauce -b chrome,chromeWin,chromeLinux
 * ```
 *
 * or you can define them in your Dalekfile:
 *
 * ```javascript
 * "browser": ["chrome", "chromeWin", "chromeLinux"]
 * ```
 *
 * A list of all available browser/OS combinations, can be found [here](https://saucelabs.com/docs/platforms).
 * 
 * @module Driver
 * @class Sauce
 * @namespace Dalek
 * @part Sauce
 * @api
 */

Sauce.prototype = {

  /**
   * Initializes the driver properties
   *
   * @method _initializeProperties
   * @param {object} opts Options needed to kick off the driver
   * @chainable
   * @private
   */

  _initializeProperties: function (opts) {
    // prepare properties
    this.actionQueue = [];
    this.config = opts.config;
    this.lastCalledUrl = null;
    this.driverStatus = {};
    this.sessionStatus = {};
    // store injcted options in object properties
    this.events = opts.events;
    this.reporterEvents = opts.reporter;
    this.browserName = opts.browser;
    return this;
  },

  /**
   * Binds listeners on browser events
   *
   * @method _initializeProperties
   * @param {object} browser Browser module
   * @chainable
   * @private
   */

  _startBrowserEventListeners: function (browser) {
    // issue the kill command to the browser, when all tests are completed
    this.events.on('tests:complete:sauce:' + this.browserName, browser.kill.bind(browser));
    // clear the webdriver session, when all tests are completed
    this.events.on('tests:complete:sauce:' + this.browserName, this.webdriverClient.closeSession.bind(this.webdriverClient));
    return this;
  },

  /**
   * Checks if a webdriver session has already been established,
   * if not, create a new one
   *
   * @method start
   * @return {object} promise Driver promise
   */

  start: function () {
    var deferred = Q.defer();

    // store desired capabilities of this session
    this.desiredCapabilities = this.browserData.getDesiredCapabilities(this.browserName, this.config);
    this.browserDefaults = this.browserData.driverDefaults;
    this.browserDefaults.status = this.browserData.getStatusDefaults(this.desiredCapabilities);

    // start a browser session
    this._startBrowserSession(deferred, this.desiredCapabilities, this.browserDefaults);

    return deferred.promise;
  },

  /**
   * Creates a new webdriver session
   * Gets the driver status
   * Gets the session status
   * Resolves the promise (e.g. let them tests run)
   *
   * @method _startBrowserSession
   * @param {object} deferred Browser session deferred
   * @chainable
   * @private
   */

  _startBrowserSession: function (deferred, desiredCapabilities, defaults) {
    var viewport = this.config.get('viewport');

    // start a session, transmit the desired capabilities
    var promise = this.webdriverClient.createSession({desiredCapabilities: desiredCapabilities});
    
    // set the default viewport if supported by the browser
    if (defaults.viewport) {
      promise = promise.then(this.webdriverClient.setWindowSize.bind(this.webdriverClient, viewport.width, viewport.height));
    }

    // get the driver status if supported by the browser
    if (defaults.status === true) {
      promise = promise
        .then(this.webdriverClient.status.bind(this.webdriverClient))
        .then(this._driverStatus.bind(this));
    } else {
      promise = promise.then(this._driverStatus.bind(this, JSON.stringify({value: defaults.status})));
    }

    // get the session info if supported by the browser
    if (defaults.sessionInfo === true) {
      promise = promise
        .then(this.webdriverClient.sessionInfo.bind(this.webdriverClient))
        .then(this._sessionStatus.bind(this));
    } else {
      promise = promise.then(this._driverStatus.bind(this, JSON.stringify({value: defaults.sessionInfo})));
    }

    // finally resolve the deferred
    promise.then(deferred.resolve);
    return this;
  },

  /**
   * Starts to execution of a batch of tests
   *
   * @method end
   * @chainable
   */

  end: function () {
    var result = Q.resolve();

    // loop through all promises created by the remote methods
    // this is synchronous, so it waits if a method is finished before
    // the next one will be executed
    this.actionQueue.forEach(function (f) {
      result = result.then(f);
    });

    // flush the queue & fire an event
    // when the queue finished its executions
    result.then(this.flushQueue.bind(this));
    return this;
  },

  /**
   * Flushes the action queue (e.g. commands that should be send to the wbdriver server)
   *
   * @method flushQueue
   * @chainable
   */

  flushQueue: function () {
    // clear the action queue
    this.actionQueue = [];
    
    // kill the session
    var promise = this.webdriverClient.deleteSession();

    // emit the run.complete event
    promise.then(function () {
      this.events.emit('driver:message', {key: 'run.complete', value: null});
    }.bind(this));

    return this;
  },

  /**
   * Loads the browser session status
   *
   * @method _sessionStatus
   * @param {object} sessionInfo Session information
   * @return {object} promise Browser session promise
   * @private
   */

  _sessionStatus: function (sessionInfo) {
    var defer = Q.defer();
    this.sessionStatus = JSON.parse(sessionInfo).value;
    this.events.emit('driver:sessionStatus:sauce:' + this.browserName, this.sessionStatus);
    defer.resolve();
    return defer.promise;
  },

  /**
   * Loads the browser driver status
   *
   * @method _driverStatus
   * @param {object} statusInfo Driver status information
   * @return {object} promise Driver status promise
   * @private
   */

  _driverStatus: function (statusInfo) {
    var defer = Q.defer();
    this.driverStatus = JSON.parse(statusInfo).value;
    this.events.emit('driver:status:sauce:' + this.browserName, this.driverStatus);
    defer.resolve();
    return defer.promise;
  },

  /**
   * Creates an anonymus function that calls a webdriver
   * method that has no return value, emits an empty result event
   * if the function has been run
   * TODO: Name is weird, should be saner
   *
   * @method _createNonReturnee
   * @param {string} fnName Name of the webdriver function that should be called
   * @return {function} fn
   * @private
   */

  _createNonReturnee: function (fnName) {
    return this._actionQueueNonReturneeTemplate.bind(this, fnName);
  },

  /**
   * Generates a chain of webdriver calls for webdriver
   * methods that don't have a return value
   * TODO: Name is weird, should be saner
   *
   * @method _actionQueueNonReturneeTemplate
   * @param {string} fnName Name of the webdriver function that should be called
   * @param {string} hash Unique action hash
   * @param {string} uuid Unique action hash
   * @chainable
   * @private
   */

  _actionQueueNonReturneeTemplate:function (fnName, hash, uuid) {
    this.actionQueue.push(this.webdriverClient[fnName].bind(this.webdriverClient));
    this.actionQueue.push(this._generateDummyDriverMessageFn.bind(this, fnName, hash, uuid));
    return this;
  },

  /**
   * Creates a driver notification with an empty value
   * TODO: Name is weird, should be saner
   *
   * @method _generateDummyDriverMessageFn
   * @param {string} fnName Name of the webdriver function that should be called
   * @param {string} hash Unique action hash
   * @param {string} uuid Unique action hash
   * @return {object} promise Driver message promise
   * @private
   */

  _generateDummyDriverMessageFn: function (fnName, hash, uuid) {
    var deferred = Q.defer();
    this.events.emit('driver:message', {key: fnName, value: null, uuid: uuid, hash: hash});
    deferred.resolve();
    return deferred.promise;
  }
};

/**
 * Determines if the driver is a "multi" browser driver,
 * e.g. can handle more than one browser
 *
 * @method isMultiBrowser
 * @return {bool} isMultiBrowser Driver can handle more than one browser
 */

module.exports.isMultiBrowser = function () {
  return true;
};

/**
 * Verifies a browser request
 * TODO: Still a noop, need to add "verify the browser" logic
 *
 * @method verifyBrowser
 * @return {bool} isVerifiedBrowser Driver can handle this browser
 */

module.exports.verifyBrowser = function () {
  return true;
};

/**
 * Determines if the driver comes with its own browsers bundled
 *
 * @method dummyBrowser
 * @return {bool} isDummyBrowser Driver does not rely on Dalek browser modules
 */

module.exports.dummyBrowser = function () {
  return true;
};

/**
 * Returnes the browser that comes bundled with the driver
 *
 * @method getBrowser
 * @param {object} driver Driver instance
 * @return {object} Browser module
 */

module.exports.getBrowser = function (driver) {
  return {configuration: null, module: new Browser(driver)};
};

/**
 * Creates a new driver instance
 *
 * @method create
 * @param {object} opts Options needed to kick off the driver
 * @return {Sauce} driver
 */

module.exports.create = function (opts) {
  // load the remote command helper methods
  var dir = __dirname + '/lib/commands/';
  fs.readdirSync(dir).forEach(function (file) {
    require(dir + file)(Sauce);
  });

  return new Sauce(opts);
};