API Docs for: 0.2.7
Show:

File: lib/driver.js

'use strict';

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var url = require('url');

var logMethods = require('./log');
var when = require('./when');
var errors = require('./errors');
var type = require('./type');

var Connection = require('./connection');

var Browser = require('./browser');
var TimeOut = require('./timeOut');
var Utils = require('./utils');
var Chain = require('./chain');
var Compatibility = require('./compatibility');

var Session = require('./session');
var Status = require('./status');
var LogEntry = require('./logEntry');

var SessionStorage = require('./sessionStorage');

var Comparison = require('./helpers/comparison');


module.exports = Driver;

/**
 * Create a new Driver session, remember to call `.dispose()`
 * at the end to terminate the session.
 *
 * @constructor
 * @class Driver
 * @module WebDriver
 * @submodule Core
 * @param {String|Object} remote Request object or URL to selenium-server
 * @param {Object} capabilities See capabilities in {{#crossLink "Session"}}{{/crossLink}}
 * @param {Object} options
 * @param {String} options.mode Mode of web-driver requests (Driver.MODE_SYNC|Driver.MODE_ASYNC)
 * @param {String} [options.base] Base-url
 * @param {String} [options.sessionID]
 * @param {String} [options.logDir]
 * @param {Boolean} [options.logScreen=true]
 * @param {string|string[]} [options.compare='blinkDiff']
 */
function Driver (remote, capabilities, options) {
  EventEmitter.call(this);

  type('remote', remote, 'String|Object');
  type('capabilities', capabilities, 'Object');
  type('options', options, 'Object');
  type('options.mode', options.mode, 'String');

  setupDebug(this, options);

  this._browserName = capabilities.browserName || capabilities.browser;
  this._browserVersion = capabilities.browserVersion || capabilities.version;
  this._platform = capabilities.platform;
  this._deviceName = capabilities.deviceName || capabilities['device-name'];
  this._deviceOrientation = capabilities.deviceOrientation || capabilities['device-orientation'];

  options.remote = remote;
  options.capabilities = capabilities;
  this._options = options;
  this._connection = new Connection(remote, options);
  this._connection.on('request', function (req) {
    this.emit('request', req);
  }.bind(this));
  this._connection.on('response', function (res) {
    this.emit('response', res);
  }.bind(this));

  this._valueRegistry = {
    needsStitching: null,
    maxImageResolution: Number.MAX_VALUE,
    devicePixelRatio: null
  };

  if (options.sessionID) {
    this._sessionObj = new Session({ sessionId: options.sessionID, capabilities: capabilities });
  } else {
    this._sessionObj = this._createSession(capabilities);
  }

	this._comparison = new Comparison(this, options.compare || ['blinkDiff']);
}
util.inherits(Driver, EventEmitter);


////////////
// Events //
////////////

/**
 * Fired when a request is made
 *
 * @event request
 * @param {Object} request Request options
 */

/**
 * Fired when a response is received
 *
 * @event response
 * @param {Object} response Response data
 */

/**
 * Fired when a public method is called
 *
 * @event method-call
 * @param {Object} event Method event data
 */

/**
 * Fired when the session is destroyed
 *
 * @event disposed
 */


////////////////////
// Static Methods //
////////////////////

/**
 * Gets the selenium-system status
 *
 * @method getStatus
 * @param {String} remote
 * @param {String} mode
 * @return {Status}
 */
Driver.getStatus = function (remote, mode) {
  var request = new Connection(remote, mode);

  return when(when(request.request('GET', '/status'), Connection.prototype._parseResponse), function (response) {
    return new Status(response);
  });
};

/**
 * Returns a list of the currently active sessions
 *
 * Note: Appears not to be supported by the selenium-standalone-server!
 *
 * @method getSessions
 * @param {String} remote
 * @param {string} mode
 * @return {Array.<Session>}
 */
Driver.getSessions = function (remote, mode) {
  var request = new Connection(remote, mode);

  return when(when(request.request('GET', '/sessions'), Connection.prototype._parseResponse), function (sessions) {
    return sessions.map(function (session) {
      return new Session(session);
    });
  });
};


/////////////////////
// Private Methods //
/////////////////////

/**
 * Returns value according to mode
 *
 * @method _getValue
 * @protected
 * @param {*} [value]
 * @return {*}
 */
Driver.prototype._getValue = function (value) {

  var result;

  switch (this._options.mode) {
    case Driver.MODE_ASYNC:
      result = require('promise')(function (resolve) { resolve(value); });
      break;
    case Driver.MODE_SYNC:
      result = value;
      break;
    default:
      throw new Error('Expected options.mode to be "async" or "sync" but got ' + JSON.stringify(this._options.mode));
  }

  return result;
};


/**
 * Is driver in sync-mode?
 *
 * @method isSync
 * @return {boolean}
 */
Driver.prototype.isSync = function () {
  return (this._options.mode === Driver.MODE_SYNC);
};

/**
 * Is driver in async-mode?
 *
 * @method isAsync
 * @return {boolean}
 */
Driver.prototype.isAsync = function () {
  return (this._options.mode === Driver.MODE_ASYNC);
};


/**
 * Gets the name of the browser
 *
 * @method browserName
 * @return {string}
 */
Driver.prototype.browserName = function () {
  return this._browserName || '';
};

/**
 * Gets the version of the browser
 *
 * @method browserVersion
 * @return {string}
 */
Driver.prototype.browserVersion = function () {
  return this._browserVersion || '';
};

/**
 * Gets the name of the platform (including platform version)
 *
 * @method platform
 * @return {string}
 */
Driver.prototype.platform = function () {
  return this._platform || '';
};

/**
 * Gets the name of the device
 *
 * @method deviceName
 * @return {string}
 */
Driver.prototype.deviceName = function () {
  return this._deviceName || '';
};

/**
 * Gets the orientation of the device
 *
 * @method deviceOrientation
 * @return {string}
 */
Driver.prototype.deviceOrientation = function () {
  return this._deviceOrientation || '';
};

/**
 * Gets a complete identifier of selected browser (+ version) and platform
 *
 * @method browserId
 * @return {string}
 */
Driver.prototype.browserId = function () {

	var deviceOrientation = this.deviceOrientation(),
		browserName = this.deviceName(),
		browserVersion = this.browserVersion(),
		platform = this.platform(),
		details = [];

	browserName = browserName.length > 0 ? browserName : this.browserName();
	browserName += browserVersion.length > 0 ? browserVersion : '';

	details.push(platform);
	details.push(deviceOrientation);
	details = details.join('').length > 0 ? ' (' + details.join(' - ') + ')' : '';

	return browserName + details;
};


/**
 * Gets a value of the value registry
 *
 * @method getValue
 * @param {string} name
 * @return {*}
 */
Driver.prototype.getValue = function (name) {
  return this._valueRegistry[name];
};

/**
 * Sets the value of an entry in the registry
 *
 * @method setValue
 * @param {string} name
 * @param {boolean} value
 */
Driver.prototype.setValue = function (name, value) {
  this._valueRegistry[name] = value;
};


/**
 * Create a selenium session
 *
 * @method _createSession
 * @private
 * @param {Object} desiredCapabilities
 * @param {Object} [requiredCapabilities]
 * @return {Session}
 */
Driver.prototype._createSession = function (desiredCapabilities, requiredCapabilities) {

  var capabilities = {};

  capabilities.desiredCapabilities = desiredCapabilities;
  if (requiredCapabilities) {
    capabilities.requiredCapabilities = requiredCapabilities;
  }

  return when(this._connection.request('POST', '/session', { body: capabilities }), function (res) {
    return new Session(extractSessionData(res));
  });
};

/**
 * Trigger an event to log the method-call
 *
 * @param {object} event
 * @method _logMethodCall
 * @protected
 */
Driver.prototype._logMethodCall = function (event) {
  if (!event.target) {
    event.target = 'Driver';
  }
  when.done(this._session(), function (session) {
    event.sessionID = session.id();
    this.emit('method-call', event);
  }.bind(this));
};


/**
 * Performs a context dependent JSON request for the current session.
 * The result is parsed for errors.
 *
 * @method _requestJSON
 * @protected
 * @param {String} method
 * @param {String} path
 * @param {*} [body]
 * @param {Object} [options]
 * @return {*}
 */
Driver.prototype._requestJSON = function (method, path, body, options) {
  options = options || {};
  options.body = body;
  return this._connection.parsedRequest(this._session(), method, path, options);
};


//////////////////
// Enumerations //
//////////////////

/**
 * Sync-mode of web-driver requests
 *
 * @static
 * @property MODE_SYNC
 * @type {string}
 */
Driver.MODE_SYNC = 'sync';

/**
 * Async-mode of web-driver requests
 *
 * @static
 * @property MODE_ASYNC
 * @type {string}
 */
Driver.MODE_ASYNC = 'async';


/**
 * Status of the HTML5 application cache - Uncached
 *
 * @static
 * @property APPLICATION_CACHE_UNCACHED
 * @type {int}
 */
Driver.APPLICATION_CACHE_UNCACHED = 0;

/**
 * Status of the HTML5 application cache - Idle
 *
 * @static
 * @property APPLICATION_CACHE_IDLE
 * @type {int}
 */
Driver.APPLICATION_CACHE_IDLE = 1;

/**
 * Status of the HTML5 application cache - Checking
 *
 * @static
 * @property APPLICATION_CACHE_CHECKING
 * @type {int}
 */
Driver.APPLICATION_CACHE_CHECKING = 2;

/**
 * Status of the HTML5 application cache - Downloading
 *
 * @static
 * @property APPLICATION_CACHE_DOWNLOADING
 * @type {int}
 */
Driver.APPLICATION_CACHE_DOWNLOADING = 3;

/**
 * Status of the HTML5 application cache - Update-ready
 *
 * @static
 * @property APPLICATION_CACHE_UPDATE_READY
 * @type {int}
 */
Driver.APPLICATION_CACHE_UPDATE_READY = 4;

/**
 * Status of the HTML5 application cache - Obsolete
 *
 * @static
 * @property APPLICATION_CACHE_OBSOLETE
 * @type {int}
 */
Driver.APPLICATION_CACHE_OBSOLETE = 5;


////////////////////
// Public Methods //
////////////////////

/**
 * Get the session object of current session
 *
 * Note: This function call is not logged!
 *
 * @method _session
 * @private
 * @return {Session}
 */
Driver.prototype._session = function () {
  return this._sessionObj;
};

/**
 * Get the session object of current session
 *
 * @method session
 * @return {Session}
 */
Driver.prototype.session = Driver.prototype._session;


/**
 * Adjusts the configurations according to the browsers behavior.
 *
 * Note:
 * This is needed especially when screenshots should be taken since
 * browsers behave here very different from each other.
 *
 * Needs to be called only once per taxi instance.
 *
 * @method adjust
 * @param {object} [options] Preparation options
 * @param {int} [options.url='http://www.example.org'] Default website for adjustment for browser
 * @param {int} [options.horizontalPadding=0] Padding of the document
 * @since 0.2.6
 */
Driver.prototype.adjust = function (options) {

	options = options || {};

	var url = options.url || 'http://www.example.org';

	// Navigate to adjustment website
	return when(this.browser().activeWindow().navigator().navigateTo(url), function () {

		var Stitching = require('./helpers/stitching'),
			stitching = new Stitching(this);

		// Will do needsStitching and devicePixelRatio if not done yet
		return stitching.doesNeedStitching(options);

	}.bind(this));
};


/**
 * Gets the browser object.
 * Direct-access. No need to wait.
 *
 * @method browser
 * @return {Browser}
 */
Driver.prototype.browser = function () {
  return new Browser(this);
};


/**
 * Get the time-out object.
 * Direct-access. No need to wait.
 *
 * @method timeOut
 * @return {TimeOut}
 */
Driver.prototype.timeOut = function () {
  return new TimeOut(this);
};


/**
 * Get the helpers object.
 * Direct-access. No need to wait.
 *
 * @method helpers
 * @return {Utils}
 */
Driver.prototype.utils = function () {
  return new Utils(this);
};


/**
 * Get the chain object.
 * Direct-access. No need to wait.
 *
 * @method chain
 * @return {Chain}
 */
Driver.prototype.chain = function () {
  return new Chain(this);
};

/**
 * Get the compatibility object.
 * Direct-access. No need to wait.
 *
 * @method compatibility
 * @return {Compatibility}
 */
Driver.prototype.compatibility = function () {
  return new Compatibility(this);
};


/**
 * Get the status of the html5 application cache
 *
 * @method getApplicationCacheStatus
 * @return {Number}
 */
Driver.prototype.getApplicationCacheStatus = function () {
  return this._requestJSON('GET', '/application_cache/status');
};


/**
 * Get the log for a given log type. Log buffer is reset after each request.
 *
 * @method getLogs
 * @param {String} logType
 * @return {Array.<LogEntry>}
 */
Driver.prototype.getLogs = function (logType) {
  type('logType', logType, 'String');

  return when(this._requestJSON('POST', '/log', {
    type: logType
  }), function (logs) {
    return logs.map(function (logEntry) {
      return new LogEntry(logEntry);
    }.bind(this));
  }.bind(this));
};

/**
 * Get available log types
 *
 * @method getLogTypes
 * @return {Array.<String>}
 */
Driver.prototype.getLogTypes = function () {
  return this._requestJSON('GET', '/log/types');
};


/**
 * End this Driver session
 *
 * @method dispose
 * @param {Object} [status]
 */
Driver.prototype.dispose = function (status) {
  var doDispose = function () {
    return when(this._requestJSON('DELETE', ''), function (res) {

		this.emit('disposed');

		// Make sure that dispose won't be called again
		this.dispose = function () {};

		return res;

    }.bind(this));
  }.bind(this);

	return when(this._comparison.tearDown(), function () {
		if (status && (status.passed === true || status.passed === false)) {
			return when(this.sauceJobUpdate(status), doDispose);
		} else {
			return doDispose();
		}
	}.bind(this));
};

/**
 * End this Driver session
 *
 * Alias for `dispose`
 * @method quit
 */
Driver.prototype.quit = Driver.prototype.dispose;


/**
 * Get the Session-Storage object.
 * Direct-access. No need to wait.
 *
 * @method sessionStorage
 * @return {SessionStorage}
 */
Driver.prototype.sessionStorage = function () {
  return new SessionStorage(this);
};


/**
 * Sauce Labs Methods
 *
 * @method sauceJobUpdate
 * @param {Object} body
 * @return {Boolean}
 */
Driver.prototype.sauceJobUpdate = function (body) {
  var remote = this._connection._remote;
  var request = this._connection._request_internal.bind(this);

  function makeRequest (session) {
    if (typeof remote !== 'string') {
      remote = remote.base;
    }
    if (remote.indexOf('saucelabs') === -1) {
      return false;
    }
    if (body === undefined) {
      return true;
    }
    var auth = url.parse(remote).auth;

    return when(request({
      method: 'PUT',
      uri: 'http://' + auth + '@saucelabs.com/rest/v1/' + auth.split(':')[0] + '/jobs/' + session.id(),
      headers: {
        'Content-Type': 'text/json'
      },
      body: JSON.stringify(body)
    }), function () {
      return true;
    });
  }

  return when(this._session(), makeRequest);
};

logMethods(Driver.prototype);


///////////////
// Utilities //
///////////////

/**
 * Extract a session ID and the accepted capabilities from a server response
 *
 * @param {Object} res
 * @return {Object} `{sessionId: String, capabilities: Object}`
 */
function extractSessionData (res) {
  var body;

  if (res.statusCode !== 200) {
    console.dir(res.headers);
    throw new Error('Failed to start a Selenium session. Server responded with status code ' + res.statusCode + ':\n' + res.body);
  }

  body = JSON.parse(res.body);
  if (body.status === 0) {
    return { sessionId: body.sessionId, capabilities: body.value };
  } else {
    throw new Error(errors.fromBody(body));
  }
}

/**
 * Setup debug output
 *
 * @param {Driver} driver
 * @param {object} options
 */
function setupDebug (driver, options) {
  var indentation = 0;

  function stringFill (filler, length) {
    var buffer = new Buffer(length);
    buffer.fill(filler);
    return buffer.toString();
  }

  function getIndentation (add) {
    return stringFill(' ', (indentation + add) * 2);
  }

  if (options.debug) {
    if (options.httpDebug) {
      driver.on('request', function (req) {
        console.log(getIndentation(1) + "Request:  ", JSON.stringify(req).substr(0, 5000));
      });
      driver.on('response', function (res) {
        console.log(getIndentation(1) + "Response: ", JSON.stringify(res).substr(0, 5000));
      });
    }
    driver.on('method-call', function (event) {
      var msg = event.target;
      indentation = event.indentation;
      if (event.selector) {
        msg += '(' + util.inspect(event.selector, {colors: true}) + ')';
      }
      msg += '.' + event.name;
      msg += '(' + event.args.map(function (a) {
        return util.inspect(a, {colors: true});
      }).join(', ') + ')';
      if (event.result && typeof event.result !== 'object') {
        msg += ' => ' + util.inspect(event.result, {colors: true});
      }
      console.log(getIndentation(0) + '[' + (event.state + stringFill(' ', 5)).substr(0, 5) + '] ' + msg);
      if (event.state.toLowerCase() !== 'start') {
        console.log(getIndentation(0) + stringFill('-', 50));
      }
    });
  }
}