API Docs for: 0.0.5
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 Q = require('q');
var cp = require('child_process');
var portscanner = require('portscanner');

// int. libs
var iedriver = require('./lib/iedriver');

/**
 * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek).
 * It provides all a WebDriverServer & browser launcher for Internet Explorer.
 *
 * The browser plugin can be installed with the following command:
 *
 * ```bash
 * $ npm install dalek-browser-ie --save-dev
 * ```
 *
 * You can use the browser plugin by adding a config option to the your Dalekfile
 *
 * ```javascript
 * "browsers": ["IE"]
 * ```
 *
 * Or you can tell Dalek that it should test in this browser via the command line:
 *
 * ```bash
 * $ dalek mytest.js -b IE
 * ```
 *
 * The Webdriver Server tries to open Port 5555 by default,
 * if this port is blocked, it tries to use a port between 5555 & 5564
 * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so:
 *
 * ```javascript
 * "browsers": {
 *   "ie": {
 *     "port": 6555 
 *   }
 * }
 * ```
 *
 * It is also possible to specify a range of ports:
 *
 * ```javascript
 * "browsers": {
 *   "ie": {
 *     "portRange": [6100, 6120] 
 *   }
 * }
 * ```
 * 
 * @module DalekJS
 * @class InternetExplorer
 * @namespace Browser
 * @part InternetExplorer
 * @api
 */

var InternetExplorer = {

  /**
   * Verbose version of the browser name
   *
   * @property longName
   * @type string
   * @default Internet Explorer
   * @api
   */

  longName: 'Internet Explorer',

  /**
   * Default port of the IEDriverServer
   * The port may change, cause the port conflict resultion
   * tool might pick another one, if the default one is blocked
   *
   * @property port
   * @type integer
   * @default 5555
   */

  port: 5555,

  /**
   * Default maximum port of the IEDriverServer
   * The port is the highest port in the range that can be allocated
   * by the IEDriverServer
   *
   * @property maxPort
   * @type integer
   * @default 5654
   */

  maxPort: 5654,

  /**
   * Default host of the IEDriverServer
   * The host may be overridden with
   * a user configured value
   *
   * @property host
   * @type string
   * @default localhost
   */

  host: 'localhost',

  /**
   * Default desired capabilities that should be
   * transferred when the browser session gets requested
   *
   * @property desiredCapabilities
   * @type object
   */

  desiredCapabilities: {
    browserName: 'InternetExplorer',
    initialBrowserUrl: ''
  },

  /**
   * Driver defaults, what should the driver be able to access.
   *
   * @property driverDefaults
   * @type object
   */

  driverDefaults: {
    viewport: true,
    status: true,
    sessionInfo: true
  },

  /**
   * Path to the IEDriverServer.exe file
   *
   * @property path
   * @type string
   * @default /
   */

  path: '/',

  /**
   * Child process instance of the IEDriverServer
   *
   * @property spawned
   * @type null|Object
   */

  spawned: null,

  /**
   * IE processes that are running on startup,
   * and therefor shouldn`t be closed
   *
   * @property openProcesses
   * @type array
   * @default []   
   */

  openProcesses: [],

  /**
   * Resolves the driver port
   *
   * @method getPort
   * @return {integer} port WebDriver server port
   */

  getPort: function () {
    return this.port;
  },

  /**
   * Returns the driver host
   *
   * @method getHost
   * @return {string} host WebDriver server hostname
   */

  getHost: function () {
    return this.host;
  },

  /**
   * Resolves the maximum range for the driver port
   *
   * @method getMaxPort
   * @return {integer} port Max WebDriver server port range
   */

  getMaxPort: function () {
    return this.maxPort;
  },

  /**
   * Launches the driver
   * (the driver takes care of launching the browser)
   *
   * @method launch
   * @return {object} promise Browser promise
   */

  launch: function (configuration, events, config) {
    var deferred = Q.defer();

    // store injected configuration/log event handlers
    this.reporterEvents = events;
    this.configuration = configuration;
    this.config = config;

    // check for a user set port
    var browsers = this.config.get('browsers');
    if (browsers && Array.isArray(browsers)) {
      browsers.forEach(this._checkUserDefinedPorts.bind(this));
    }

    // check if the current port is in use, if so, scan for free ports
    portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred));
    return deferred.promise;
  },

  /**
   * Kills the driver & browser processes
   *
   * @method kill
   * @chainable
   */

  kill: function () {
    // get a list of all running processes
    this._list(function(svc){
      // filter out the browser process
      svc.forEach(function (item, idx) {
        Object.keys(item).forEach(function (key) {
          if(svc[idx][key] === 'iexplore.exe') {
            // kill the browser process
            this._kill(svc[idx].PID, true);
          }
        }.bind(this));
      }.bind(this));
    }.bind(this),true);

    // kill the driver process
    this.spawned.kill('SIGTERM');
    return this;
  },

  _checkPorts: function (deferred, err, port) {
    // check if the port was blocked & if we need to switch to another port
    if (this.port !== port) {
      this.reporterEvents.emit('report:log:system', 'dalek-browser-ie: Switching to port: ' + port);
      this.port = port;
    }

    // invoke the ie driver afterwards
    this._startIEdriver(deferred);
    return this;
  },

  _startIEdriver: function (deferred) {
    var stream = '';
    this.spawned = cp.spawn(iedriver.path, ['--port=' + this.getPort()]);

    this.spawned.stdout.on('data', function (data) {
      var dataStr = data + '';
      stream += dataStr;
      if (stream.search('Listening on port') !== -1) {
        deferred.resolve();
      }
    });
    return this;
  },

  /**
   * Process user defined ports
   *
   * @method _checkUserDefinedPorts
   * @param {object} browser Browser configuration
   * @chainable
   * @private
   */

  _checkUserDefinedPorts: function (browser) {
    // check for a single defined port
    if (browser.ie && browser.ie.port) {
      this.port = parseInt(browser.ie.port, 10);
      this.maxPort = this.port + 90;
      this.reporterEvents.emit('report:log:system', 'dalek-browser-ie: Switching to user defined port: ' + this.port);
    }

    // check for a port range
    if (browser.ie && browser.ie.portRange && browser.ie.portRange.length === 2) {
      this.port = parseInt(browser.ie.portRange[0], 10);
      this.maxPort = parseInt(browser.ie.portRange[1], 10);
      this.reporterEvents.emit('report:log:system', 'dalek-browser-ie: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort);
    }

    return this;
  },

  /**
   * Lists all running processes (win only)
   *
   * @method _list
   * @param {Function} callback Receives the process object as the only callback argument
   * @param {Boolean} [verbose=false] Verbose output
   * @chainable
   * @private
   */

  _list: function(callback, verbose) {
    verbose = typeof verbose === 'boolean' ? verbose : false;
    cp.exec('tasklist /FO CSV' + (verbose === true ? ' /V' : ''), function (err, stdout) {
      var pi = stdout.split('\r\n');
      var p = [];

      pi.forEach(function (line) {
        if (line.trim().length !== 0) {
          p.push(line);
        }
      });

      var proc = [];
      var head = null;
      while (p.length > 1) {
        var rec = p.shift();
        rec = rec.replace(/\"\,/gi,'";').replace(/\"|\'/gi,'').split(';');
        if (head === null){
          head = rec;
          for (var i=0;i<head.length;i++){
            head[i] = head[i].replace(/ /gi,'');
          }

          if (head.indexOf('PID')<0){
            head[1] = 'PID';
          }
        } else {
          var tmp = {};
          for (var j=0;j<rec.length;j++){
            tmp[head[j]] = rec[j].replace(/\"|\'/gi,'');
          }
          proc.push(tmp);
        }
      }
      callback(proc);
    });

    return this;
  },

  /**
   * Kill a specific process (win only)
   *
   * @method _kill
   * @param {Number} PID Process ID
   * @param {Boolean} [force=false] Force close the process.
   * @param {Function} [callback] Callback after process has been killed
   * @chainable
   * @private
   */

  _kill: function(pid, force, callback) {
    if (!pid || isNaN(parseInt(pid))){
      throw new Error('PID is required for the kill operation.');
    }
    callback = callback || function(){};
    if (typeof force === 'function'){
      callback = force;
      force = false;
    }
    cp.exec('taskkill /PID ' + pid + (force === true ? ' /f' : ''),callback);
    return this;
  }

};

// expose the module
module.exports = InternetExplorer;