rochdev/tinto

View on GitHub
lib/component.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict';

// TODO: support find() by Component class and instance
// TODO: handle element removed

var util = require('util');
var Q = require('q');
var queue = require('./queue');
var Entity = require('./entity');
var AssertionResult = require('./assertion-result');
var ComponentCollection = require('./component-collection');
var ElementNotFoundError = require('./errors/element-not-found-error');
var Locator = require('./utils/locator');
var extend = require('./utils/extend');
var inherits = require('./utils/inherits');
var evaluator = require('./utils/evaluator');
var bundles = require('./tinto');
var tinto = {};

/**
 * @param {tinto.Locator} locator
 * @constructor
 * @extends tinto.Entity
 * @property {tinto.Attribute} text
 */
tinto.Component = function Component(locator) {
  Entity.call(this);

  this._locator = locator;

  this.state('available', available);
  this.state('missing', missing);
  this.state('clickable', clickable); // TODO: add unclickable

  this.property('text', this.getText);

  /**
   * @function
   * @name tinto.Component#clear
   * @returns {tinto.Component}
   */
  wrap('clear', 'sendKeys');
};

inherits(tinto.Component, Entity);

/**
 * @param {Object} [protoProps]
 * @param {Object} [staticProps]
 * @returns {function(this:tinto.Component,new:tinto.Component,Promise)}
 */
tinto.Component.extend = function(protoProps, staticProps) {
  return extend.call(this, protoProps, staticProps);
};

/**
 * @param {tinto.Component} component
 * @returns {tinto.Component}
 */
tinto.Component.from = function(component) {
  return new this(component._locator);
};

/**
 * @returns {string}
 */
tinto.Component.prototype.toString = function() {
  if (this._locator.id) {
    return '[' + this.constructor.name + ':' + this._locator.id + ']';
  } else {
    return this._locator.getMessage();
  }
};

/**
 * @template T
 * @param {T} type
 * @returns {T}
 */
tinto.Component.prototype.as = function(type) {
  return tinto.Component.from.call(type, this);
};

/**
 * @param {...tinto.Component} components
 * @returns {function() : Promise.<tinto.AssertionResult>}
 */
tinto.Component.prototype.contains = function(components) {
  var self = this;

  components = Array.prototype.slice.call(arguments, 0);

  return function() {
    return Q.all(components.map(function(component) {
      return component.getElement().then(function(element) {
        return self.execute(function(element) {
          return this.contains(element);
        }, element).then(function(result) {
          return new AssertionResult(result, component.toString());
        });
      });
    }));
  };
};

/**
 * @param {tinto.Component} component
 * @returns {function() : Promise.<tinto.AssertionResult>}
 */
tinto.Component.prototype.equals = function(component) {
  var self = this;

  return function() {
    return component.getElement().then(function(element) {
      return self.execute(function(element) {
        return this === element;
      }, element).then(function(result) {
        return new AssertionResult(result, component.toString(), self.toString());
      });
    });
  };
};

/**
 * @param {string} name
 * @param {function() : Promise} [matcher]
 * @param {Array.<*>} [args]
 */
tinto.Component.prototype.state = function(name, matcher, args) {
  support.call(this, 'state', 'states', name, matcher, args, 'State "%s" does not exist');
};

/**
 * @param {string} name
 * @param {function() : Promise} [matcher]
 * @param {Array.<*>} [args]
 */
tinto.Component.prototype.property = function(name, matcher, args) {
  support.call(this, 'property', 'properties', name, matcher, args, 'Property "%s" does not exist');
};

/**
 * @returns {Promise.<webdriver.WebElement>}
 */
tinto.Component.prototype.getElement = function() {
  var self = this;

  return Q.when(evaluator.getDriver().wait(this._locator.locate.bind(this._locator), 2000))
    .catch(function() {
      throw new ElementNotFoundError(self._locator);
    });
};

/**
 * @returns {Promise.<string>}
 */
tinto.Component.prototype.getText = function() {
  return this.getElement().then(function(element) {
    return element.getText();
  });
};

/**
 * @param {string} name
 * @returns {Promise.<string>}
 */
tinto.Component.prototype.getAttribute = function(name) {
  return this.getElement().then(function(element) {
    return element.getAttribute(name);
  });
};

/**
 * @returns {tinto.Component}
 */
tinto.Component.prototype.click = function() {
  var self = this;
  var args = arguments;

  self.should.be.clickable;

  queue.push(function() {
    return self.getElement().then(function(element) {
      return element.click.apply(element, args);
    });
  });

  return this;
};

/**
 * @returns {tinto.Component}
 */
tinto.Component.prototype.hover = function() {
  var self = this;

  queue.push(function() {
    return self.getElement().then(function(element) {
      evaluator.getDriver().actions().mouseMove(element).perform();
    });
  });

  return this;
};

/**
 * @param {...string} keys
 * @returns {tinto.Component}
 */
tinto.Component.prototype.enter = function() {
  return this.sendKeys.apply(this, arguments);
};

/**
 * @param {...string} keys
 * @returns {tinto.Component}
 */
tinto.Component.prototype.fill = function() {
  return this.clear().sendKeys.apply(this, arguments);
};

/**
 * @param {string} selector
 * @param {boolean} cache
 * @returns {tinto.ComponentCollection.<tinto.Component>}
 */
tinto.Component.prototype.find = function(selector, cache) {
  return new ComponentCollection(tinto.Component, new Locator(selector, {
    parent: this,
    cache: cache
  }));
};

/**
 * @param {!function(this:tinto.Component)} callback
 * @param {...*} [args]
 * @returns {Promise}
 */
tinto.Component.prototype.execute = function(callback, args) {
  args = Array.prototype.slice.call(arguments, 0);

  return evaluator.execute.apply(evaluator, [this.getElement()].concat(args));
};

/**
 * @private
 * @param {string} type
 * @param {string} pluralType
 * @param {string} name
 * @param {function() : Promise} matcher
 * @param {Array.<*>} args
 * @param {string} message
 */
function support(type, pluralType, name, matcher, args, message) {
  if (matcher instanceof Array) {
    args = matcher;
    matcher = null;
  }

  var bundle = this.constructor.__bundle__;
  if (!matcher && bundle && bundles[bundle]) {
    matcher = bundles[bundle][pluralType] && bundles[bundle][pluralType][name];
  }

  if (!matcher) {
    matcher = bundles.html[pluralType] && bundles.html[pluralType][name];
  }

  if (!matcher) {
    throw new Error(util.format(message, name));
  }

  Entity.prototype[type].call(this, name, matcher, args);
}

/**
 * @private
 * @this tinto.Component
 * @returns {Promise.<boolean>}
 */
function available() {
  return this.is('missing')().then(function(result) {
    return !result.outcome;
  });
}

/**
 * @private
 * @this tinto.Component
 * @returns {Promise.<boolean>}
 */
function missing() {
  return this._locator.locate().then(function(element) {
    return element === undefined;
  });
}

/**
 * @private
 * @this tinto.Component
 * @returns {Promise.<boolean>}
 */
function clickable() {
  // TODO: validate if the element is displayed and enabled
  // TODO: handle partially visible elements

  return this.execute(function() {
    var pos = this.getBoundingClientRect();
    var x = (pos.left + pos.right) / 2;
    var y = (pos.top + pos.bottom) / 2;
    var element = document.elementFromPoint(x, y);

    return this === element || this.contains(element);
  });
}

/**
 * @privaten
 * @this tinto.Component
 * @param {...string} methods
 */
function wrap() {
  Array.prototype.slice.call(arguments, 0).forEach(function(method) {
    tinto.Component.prototype[method] = function() {
      var self = this;
      var args = arguments;

      queue.push(function() {
        return self.getElement().then(function(element) {
          return element[method].apply(element, args);
        });
      });

      return this;
    };
  });
}

module.exports = tinto.Component;