Code coverage report for lib/promise-webdriver.js

Statements: 83.61% (102 / 122)      Branches: 77.05% (47 / 61)      Functions: 82.76% (24 / 29)      Lines: 83.19% (99 / 119)     

All files » lib/ » promise-webdriver.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 2721                   1       1 13450 1664154           1     23099 7264     7264     7264 361024 361024 18776             7264 7264     7264 7264   7264     7264 1159662 983         983 983 8 8     983 972   972     972   972     972         972   326   326 8 318           318       972   330 330     114   216         642           7264 1059       7264             23099     1         1 496 3360 3360 3360 3360 3360 3294           3360 14         3360       1434 1434 1411   23       3360 3360 3360   3360 3220   140         2     2 524       2     2 59 59     2   2     2 340       2 104               2 2             2 8 8 8   2 2 2 2       2 951   2 152       2     2                                               2    
var __slice = Array.prototype.slice, 
    Q = require('q'), 
    _ = require('lodash'), 
    EventEmitter = require('events').EventEmitter, 
    slice = Array.prototype.slice.call.bind(Array.prototype.slice),
    utils = require('./utils');
 
// The method below returns no result, so we are able hijack the result to
// preserve the element scope.
// This alows for thing like: field.click().clear().input('hello').getValue()
var elementChainableMethods = ['clear','click','doubleClick','doubleclick',
  'flick','sendKeys','submit','type','keys','moveTo','sleep','noop'];
 
// gets the list of methods to be promisified.
function filterPromisedMethods(Obj) {
  return _(Obj).functions().filter(function(fname) {
    return  !fname.match('^toString$|^_') &&
            !EventEmitter.prototype[fname];
  }).value();
}
 
// enriches a promise with the browser + element methods.
function enrich(obj, browser) {
  // There are cases were enrich may be called on non-promise objects.
  // It is easier and safer to check within the method.
  if(utils.isPromise(obj) && !obj.__wd_promise_enriched) {
    var promise = obj;
 
    // __wd_promise_enriched is there to avoid enriching twice.
    promise.__wd_promise_enriched = true;
 
    // making sure all the sub-promises are also enriched.
    _(promise).functions().each(function(fname) {
      var _orig = promise[fname];
      promise[fname] = function() {
        return enrich(
          _orig.apply(this, __slice.call(arguments, 0))
          , browser);
      };
    });
 
    // we get the list of methods first cause we need it in the enrich method.
    var browserProto = Object.getPrototypeOf(browser);
    var Element = browserProto._Element;
 
    // we get the list of methods first cause we need it in the enrich method.
    var promisedMethods = filterPromisedMethods(browserProto);
    var elementPromisedMethods =
      Element ? filterPromisedMethods(Element.prototype) : [];
    var allPromisedMethods = _.union(promisedMethods, elementPromisedMethods);
 
    // adding browser + element methods to the current promise.
    _(allPromisedMethods).each(function(fname) {
      promise[fname] = function() {
        var args = __slice.call(arguments, 0);
        // This is a hint to figure out if we need to call a browser method or
        // an element method.
        // "<" --> browser method
        // ">" --> element method
        var scopeHint;
        if(args && args[0] && typeof args[0] === 'string' && args[0].match(/^[<>]$/)) {
          scopeHint = args[0];
          args = _.rest(args);
        }
 
        return this.then(function(res) {
          var el;
          // if the result is an element it has priority
          if(Element && res instanceof Element) { el = res; }
 
          // testing the water for the next call scope
          var isBrowserMethod =
            _.indexOf(promisedMethods, fname) >= 0;
          var isElementMethod =
            el && _.indexOf(elementPromisedMethods, fname) >= 0;
 
          Iif(!isBrowserMethod && !isElementMethod) {
            // doesn't look good
            throw new Error("Invalid method " + fname);
          }
 
          if(isBrowserMethod && isElementMethod) {
            // we need to resolve the conflict.
            Iif(scopeHint === '<') {
              isElementMethod = false;
            } else if(scopeHint === '>') {
              isBrowserMethod = false;
            } else Iif(fname.match(/element/) || (Element && args[0] instanceof Element)) {
              // method with element locators are browser scoped by default.
              // When an element is passed, we are also obviously in the global scope.
              isElementMethod = false;
            } else {
              // otherwise we stay in the element scope to allow sequential calls
              isBrowserMethod = false;
            }
          }
 
          if(isElementMethod) {
            // element method case.
            return el[fname].apply(el, args).then(function(res) {
              if(_.indexOf(elementChainableMethods, fname) >= 0) {
                // method like click, where no result is expected, we return
                // the element to make it chainable
                return el;
              } else {
                return res; // we have no choice but loosing the scope
              }
            });
          }else{
            // browser case.
            return browser[fname].apply(browser, args);
          }
        });
      };
    });
    // transfering _enrich
    promise._enrich = function(target) {
      return browser._enrich(target);
    };
 
    // adding print error helper
    promise.printError = function() {
      return promise.catch(function(err) {
        console.log(err);
        throw err;
      });
    };
  }
  return obj;
}
 
module.exports = function(WebDriver, chainable) {
 
  // wraps element + browser call in an enriched promise.
  // This is the same as in the first promise version, but enrichment +
  // event logging were added.
  function wrap(fn, fname) {
    return function() {
      var _this = this;
      var callback;
      var args = slice(arguments);
      var deferred = Q.defer();
      deferred.promise.then(function() {
        _this.emit("promise", _this, fname , args , "finished");
      });
 
 
      // Remove any undefined values from the end of the arguments array
      // as these interfere with our callback detection below
      for (var i = args.length - 1; i >= 0 && args[i] === undefined; i--) {
        args.pop();
      }
 
      // If the last argument is a function assume that it's a callback
      // (Based on the API as of 2012/12/1 this assumption is always correct)
      if(typeof args[args.length - 1] === 'function')
      {
        // Remove to replace it with our callback and then call it
        // appropriately when the promise is resolved or rejected
        callback = args.pop();
        deferred.promise.then(function(value) {
          callback(null, value);
        }, function(error) {
          callback(error);
        });
      }
 
      args.push(deferred.makeNodeResolver());
      _this.emit("promise", _this, fname , args , "calling");
      fn.apply(this, args);
 
      if(chainable) {
        return enrich(deferred.promise, this);
      } else {
        return deferred.promise;
      }
    };
  }
 
  var Element = WebDriver.prototype._Element;
 
  // promisify element shortcuts too
  var promiseElement = function() {
    return Element.apply(this, arguments);
  };
 
  // Element replacement.
  promiseElement.prototype = Object.create(Element.prototype);
 
  // WebDriver replacement.
  var promiseWebdriver = function() {
    var args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
    return WebDriver.apply(this, args);
  };
 
  promiseWebdriver.prototype = Object.create(WebDriver.prototype);
 
  promiseWebdriver.prototype._Element = promiseElement;
 
  // wrapping browser methods with promises.
  _(filterPromisedMethods(WebDriver.prototype)).each(function(fname) {
    promiseWebdriver.prototype[fname] = wrap(WebDriver.prototype[fname], fname);
  });
 
  // wrapping element methods with promises.
  _(filterPromisedMethods(Element.prototype)).each(function(fname) {
    promiseElement.prototype[fname] = wrap(Element.prototype[fname], fname);
  });
 
  /**
   * Starts the chain (promised driver only)
   * browser.chain()
   * element.chain()
   */
  promiseWebdriver.prototype.chain = promiseWebdriver.prototype.noop;
  promiseElement.prototype.chain = promiseElement.prototype.noop;
 
  /**
   * Resolves the promise (promised driver only)
   * browser.resolve(promise)
   * element.resolve(promise)
   */
  promiseWebdriver.prototype.resolve = function(promise) {
    var qPromise = new Q(promise);
    this._enrich(qPromise);
    return qPromise;
  };
  promiseElement.prototype.resolve = function(promise) {
    var qPromise = new Q(promise);
    this._enrich(qPromise, this.browser);
    return qPromise;
  };
 
  // used to by chai-as-promised and custom methods
  promiseWebdriver.prototype._enrich = function(target) {
    Eif(chainable) { enrich(target, this); }
  };
  promiseElement.prototype._enrich = function(target) {
    Eif(chainable) { enrich(target, this.browser); }
  };
 
  // used to wrap custom methods
  promiseWebdriver._wrapAsync = wrap;
 
  // helper to allow easier promise debugging.
  promiseWebdriver.prototype._debugPromise = function() {
    this.on('promise', function(context, method, args, status) {
      args = _.clone(args);
      if(context instanceof promiseWebdriver) {
        context = '';
      } else {
        context = ' [element ' + context.value + ']';
      }
      if(typeof _.last(args) === 'function') {
        args.pop();
      }
      args = ' ( ' + _(args).map(function(arg) {
        if(arg instanceof Element) {
          return arg.toString();
        } else if(typeof arg === 'object') {
          return JSON.stringify(arg);
        } else {
          return arg;
        }
      }).join(', ') + ' )';
      console.log(' --> ' + status + context + " " + method + args);
    });
  };
 
  return promiseWebdriver;
};