API Docs for: 0.2.7
Show:

File: lib/connection.js

'use strict';

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var path = require('path');
var fs = require('fs');
var mkdirp = require('mkdirp');
var JSON = require('./json');

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

var screenShotCounter = 0;

module.exports = Connection;

/**
 * Connection object
 *
 * @constructor
 * @class Connection
 * @module System
 * @param {String|Object} remote
 * @param {Object} options
 * @param {String} options.mode
 * @param {int} [options.retry=3]
 * @param {String} [options.logDir]
 * @param {Boolean} [options.logScreen=false]
 */
function Connection (remote, options) {
  EventEmitter.call(this);

  if (remote[remote.length - 1] === '/') {
    remote = remote.substr(0, remote.length - 1);
  }

  this._remote = remote;
  this._retry = options.retry || 3;
  this._options = options || {};
  this._request_internal = createInternalConnection(options.mode);
}
util.inherits(Connection, 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
 */


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

/**
 * Session request with automatic parsing for errors
 *
 * @method parsedRequest
 * @param {Session} session
 * @param {String} method
 * @param {String} path
 * @param {Object} [options]
 * @param {int} [retryCounter=0]
 * @return {*}
 */
Connection.prototype.parsedRequest = function (session, method, path, options, retryCounter) {
  return when(this.sessionRequest(session, method, path, options), function (res) {
    return this._parseResponse(res, {
		session: session,
		method: method,
		path: path,
		options: options,
		retryCounter: retryCounter || 0
	}, options);
  }.bind(this));
};

/**
 * Session request returning raw value
 *
 * @method sessionRequest
 * @param {Session} session
 * @param {String} method
 * @param {String} path
 * @param {object} [options]
 * @return {Object}
 */
Connection.prototype.sessionRequest = function (session, method, path, options) {

  var makeRequest = function (localSession) {
    var uri = '/session/' + localSession.id() + path;
    return this.request(method, uri, options);
  }.bind(this);

  return when(session, makeRequest);
};

/**
 * Plain request
 *
 * @method request
 * @param {String} method
 * @param {String} urn Unified Resource Name
 * @param {Object} options
 * @return {Buffer}
 */
Connection.prototype.request = function (method, urn, options) {
  var uri = this._remote + urn;

  if (options && typeof options.body == 'object') {
    options.body = JSON.stringify(options.body);
  }

  this.emit('request', {
    method: method,
    uri: uri,
    path: urn,
    body: (options && options.body) || '',
    headers: (options && options.headers) || []
  });

  return when(this._request_internal(method, uri, options), function (response) {
    response.body = response.body.toString('utf8');
    this.emit('response', response);
    return response;
  }.bind(this));
};

/**
 * Parse a response, throwing errors if the status suggests it
 *
 * @param {Object} requestData
 * @param {Object} res
 * @param {Object} [options]
 * @param {Boolean} [options.passThrough=false]
 * @return {*}
 */
Connection.prototype._parseResponse = function (res, requestData, options) {

  var body,
      data,
      filePath,
      hasLogFolder,
      hasRequestedScreen,
      hasScreenshot;

  options = options || {};

  if (res.statusCode >= 0 && res.statusCode < 100) {
    throw new Error('Server responded with status code (' + res.statusCode + '):\n' + res.body);

  } else if (res.statusCode >= 400 && res.statusCode < 500) { // 400s
    throw new Error('Invalid request (' + res.statusCode + '):\n' + res.body);

  } else if (res.statusCode >= 500 && res.statusCode < 600) { // 500s
    body = JSON.parse(res.body);

    hasScreenshot = body && body.value && body.value.screen;
    hasLogFolder = this._options && this._options.logDir;
    hasRequestedScreen = this._options && this._options.logScreen;

    // Create screenshot for error when available and requested
    if (hasLogFolder && hasRequestedScreen && hasScreenshot) {

      // Create folder if not exists
      mkdirp.sync(this._options.logDir);

      // Gather data
      data = new Buffer(body.value.screen, 'base64');
      filePath = path.join(this._options.logDir, getNextScreenshotId() + '_err.png');

      // Write to disk
      fs.writeFileSync(filePath, data);
    }

    // Retry if possible
    if (this._retry > requestData.retryCounter) {
		return this.parsedRequest(requestData.session, requestData.method, requestData.path, requestData.options, requestData.retryCounter + 1);
	} else {
		throw new Error("Failed command (" + res.statusCode + "):\n" + body.value.message + (body.value.class ? "\nClass: " + body.value.class : "") + (body.value.stackTrace ? "\nStack-trace:\n " + stringifyStackTrace(body.value.stackTrace) : ""));
	}

  } else if (res.statusCode >= 200 && res.statusCode < 300) {

    if (res.statusCode === 204) { // No Content - meaning: everything is ok
      return null;

    } else {
      body = JSON.parse(res.body);

      if (body.status === 0) {
        return body.value;
      } else {
        throw new Error(errors.fromBody(body));
      }
    }

  } else {
	  // Retry if possible
		if (this._retry > requestData.retryCounter) {
			return this.parsedRequest(requestData.session, requestData.method, requestData.path, requestData.options, requestData.retryCounter + 1);
		} else {
			throw new Error('Unknown status code (' + res.statusCode + '):\n' + res.body);
		}
  }
};



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

/**
 * Gets the next available screenshot id
 *
 * @return {string}
 */
function getNextScreenshotId () {
  screenShotCounter++;
  if ((screenShotCounter + '').length > 4) {
    return screenShotCounter + '';
  } else {
    return ("000" + screenShotCounter).substr(-4);
  }
}

/**
 * Turns a selenium stack-trace into a string
 *
 * @param {Array.<Object>} stackTrace
 * @return {String}
 */
function stringifyStackTrace (stackTrace) {

  var i, len, result = [];

  for (i = 0, len = stackTrace.length; i < len; i++) {
    if (stackTrace[i]) {
      result.push(stackTrace[i].methodName + "::" + stackTrace[i].className + " (" + stackTrace[i].fileName + ":" + stackTrace[i].lineNumber + ")");
    }
  }

  return result.join("\n");
}

/**
 * Create connection object according to mode-type
 *
 * @param {String} mode (async|sync)
 * @return {function}
 */
function createInternalConnection (mode) {

  var result;

  switch (mode) {
    case 'async':
      result = require('then-request');
      break;
    case 'sync':
      result = require('sync-req');
      break;
    default:
      throw new Error('Expected options.mode to be "async" or "sync" but got ' + JSON.stringify(mode));
  }

  return result;
}