<commit_before>
/**
  @class

  `XT.Model` is an abstract class designed to operate with `XT.DataSource`.
  It should be subclassed for any specific implentation. Subtypes should
  include a `recordType` the data source will use to retreive the record.

  To create a new model include `isNew` in the options like so:
    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = new XM.Contact({firstName: 'Randy'}, {isNew: true});

  To load an existing record include a guid in the options like so:
    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = new XM.Contact({guid: 1});
    m.fetch();

    OR

    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = XM.Contact();
    m.fetch({id: 1});

  @extends Backbone.RelationalModel
  @param {Object} attributes
  @param {Object} options
*/
XT.Model = Backbone.RelationalModel.extend(
  /** @scope XT.Model.prototype */ {

  idAttribute: "guid",

  /**
  Set to true if you want an id fetched from the server when the `isNew` option
  is passed on a new model.

  @type {Boolean}
  */
  autoFetchId: true,

  /**
  Specify the name of a data source model here.

  @type {String}
  */
  recordType: null,

  /**
  A hash structure that defines data access.

  @property
  @type {Hash}
  */
  privileges: {},

  /**
  An array of required attributes. A validate on the entire model will fail
  until all the required attributes have values.

  @type {Array}
  */
  required: [],

  /**
  An array of attribute names designating attributes that are not editable.
  Use `setReadOnly` to edit this array.

  @seealso `setReadOnly`
  @seealso `isReadOnly`
  @type {Array}
  */
  readOnly: [],

  // ..........................................................
  // METHODS
  //

  /**
    Returns whether the current record can be updated based on privilege
    settings.

    @returns {Boolean}
  */
  canUpdate: function() {
    var obj = Backbone.Relational.store.getObjectByName(this.recordType);
    return obj.canUpdate(this);
  },

  /**
    Returns whether the current record can be deleted based on privilege
    settings.

    @returns {Boolean}
  */
  canDelete: function() {
    var obj = Backbone.Relational.store.getObjectByName(this.recordType);
    return obj.canDelete(this);
  },

  /**
  Set the entire model, or a specific model attribute to readOnly. Privilege
  enforcement supercedes read-only settings.

  Examples:

  model.setReadOnly() // sets model to read only
  model.setReadOnly(false) // sets model to be editable
  model.setReadOnly('name') // sets 'name' attribute to read-only
  model.setReadOnly('name', false) // sets 'name' attribute to be editable

  @param {String|Boolean} Attribute to set, or boolean if setting the model
  @param {Boolean} boolean - default = true.
  */
  setReadOnly: function(key, value) {
    // handle attribute
    if (_.isString(key)) {
      value = _.isBoolean(value) ? value : true;
      if (value && !_.contains(this.readOnly, key)) {
        this.readOnly.push(key);
      } else if (!value && _.contains(this.readOnly, key)) {
        this.readOnly = _.without(this.readOnly, key);
      }
      return;
    }

    // handle model
    key = _.isBoolean(key) ? key : true;
    this._readOnly = key;
  },

  /**
  Return whether the model is in a read-only state. If an attribute name
  is passed, returns whether that attribute is read only.

  @param {String} attribute
  */
  isReadOnly: function(attr) {
    var canNotUpdate = !this.canUpdate();
    if (!_.isString(attr) || this._readOnly || canNotUpdate) {
      return this._readOnly || canNotUpdate;
    }
    return _.contains(this.readOnly, attr);
  },

  initialize: function() {
    var options = arguments[1];

    // set data source
    this._dataSource = XT.dataSource;

    // initialize for new record
    if (options && options.isNew) {
      this.attributes.dataState = 'create';
      if (this.autoFetchId) this._fetchId();
    }

    // set id to read only
    if (this.idAttribute && !_.contains(this.readOnly, this.idAttribute)) {
      this.readOnly.push(this.idAttribute);
    }

    // set id as required
    if (this.idAttribute && !_.contains(this.required, this.idAttribute)) {
      this.required.push(this.idAttribute);
    }

    // set up state change handler
    this.on('change', this._changed);
  },

  /**
  Reimplemented. A model is new if the dataState is `create`.
  */
  isNew: function() {
    return this.get('dataState') === 'create';
  },

  /**
  Returns true when dataState is `create` or `update`.
  */
  isDirty: function() {
    var dataState = this.get('dataState');
    return dataState === 'create' || dataState === 'update';
  },

  /**
  Reimplemented to handle state change.
  */
  fetch: function(options) {
    options = options ? _.clone(options) : {};
    var that = this;
    var success = options.success;

    // flip sync flag back on success
    options.success = function(resp, status, xhr) {
      that._sync = false;
      that._attrCache = _.clone(that.attributes);
      if (success) success(model, resp, options);
    };

    return Backbone.Model.prototype.fetch.call(this, options);
  },

  /**
  Reimplemented to handle state change.
  */
  save: function(key, value, options) {
    // Handle both `("key", value)` and `({key: value})` -style calls.
    if (_.isObject(key) || key === null || key === undefined) options = value;
    options = options ? _.clone(options) : {};

    var that = this;
    var success = options.success;

    // flip sync flag back on success
    options.success = function(resp, status, xhr) {
      that._sync = false;
      that._attrCache = _.clone(that.attributes);
      if (success) success(model, resp, options);
    };

    // Handle both `("key", value)` and `({key: value})` -style calls.
    if (_.isObject(key) || key === null || key === undefined) value = options;

    return Backbone.Model.prototype.save.call(this, key, value, options);
  },

  /**
  Reimplemented.
  */
  destroy: function(options) {
    options = options ? _.clone(options) : {};
    var that = this;
    var success = options.success;

    // clear on success
    options.success = function(resp, status, xhr) {
      that.clear({silent: true});
      that._attrCache = null;
      if (success) success(model, resp, options);
    };

    this.set('dataState', 'delete');
    return Backbone.Model.prototype.destroy.call(this, options);
  },

  /**
  Overload: sync to xtuple datasource.
  */
  sync: function(method, model, options) {
    var recordType = model.recordType;
    var id = options.id || this.id;
    var success = options.success;

    if (options === undefined) options = {};
    this._sync = true;

    // read
    if (method === 'read' && recordType && id && success) {
      return this._dataSource.retrieveRecord(recordType, id, options);

    // write
    } else if (method === 'create' || method === 'update' || method === 'delete') {
      var ret = this._dataSource.commitRecord(model, options);
      this.set('dataState', 'busy');
      return ret;
    }

    return false;
  },

  /**
  Validate:
   * State
   * All required fields present if no attributes specified.
   * Read only attributes are not being edited
  */
  validate: function(attributes, options) {
    var attrs = attributes || this.attributes;
    var i;

    // check state
    if (this.get('dataState') === 'busy' && !this._sync) {
      return "Record is busy";
    }

    // check for editing on read-only
    if (attributes) {
      if (this.isReadOnly()) {
        return "Record is in a read only state.";
      }

      for (i = 0; i < this.readOnly.length; i++) {
        var attr = this.readOnly[i];
        if (attributes[attr] !== this.previous(attr)) {
          return "Can not edit read only attribute " + attr + ".";
        }
      }

    // check required
    } else {
      for (i = 0; i < this.required.length; i++) {
        if (!this.has(this.required[i])) {
          return "'" + this.required[i] + "' is required.";
        }
      }
    }
  },

  // ..........................................................
  // PRIVATE
  //

  /** @private */
  _attrCache: null,

  /** @private */
  _dataSource: null,

  /** @private */
  _readOnly: false,

  /** @private */
  _sync: false,

  /** @private */
  _changed: function() {
    if (this.get('dataState') === 'read' && !this._sync) {
      this.set('dataState', 'update');
    }
  },

  /** @private */
  _fetchId: function() {
    var that = this;
    var options = {};

    // callback
    options.success = function(resp, status, xhr) {
      var attr = that.idAttribute;

      // set the id
      that.setReadOnly(attr, false);
      that.set(attr, resp);
      that.setReadOnly(attr, true);
    };

    // fetch id
    this._dataSource.dispatch('XT.Record', 'fetchId', this.recordType, options);
  }

});

// Class Methods
enyo.mixin( /** @scope XT.Model */ XT.Model, {

  /**
    Use this function to find out whether a user can create records before instantiating one.

    @returns {Boolean}
  */
  canCreate: function() {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.getPrivileges(),
        isGranted = false;

    if (sessionPrivs && sessionPrivs.get) {
      // check global
      isGranted = privileges.all && privileges.all.create &&
                  sessionPrivs.get(privileges.all.create) === true;

      // check personal
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.create &&
                    sessionPrivs.get(privileges.personal.create) === true;
      }
    }

    return isGranted;
  },

  /**
    Use this function to find out whether a user can read this record type
    before any have been loaded.

    @returns {Boolean}
  */
  canRead: function() {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.privileges,
        isGranted = false;

    if (sessionPrivs && sessionPrivs.get) {
      // check global read privilege
      isGranted = privileges.all && privileges.all.read &&
                  sessionPrivs.get(privileges.all.read) === true;

      // check global update privilege
      if (!isGranted) {
        isGranted = privileges.all && privileges.all.update &&
                    sessionPrivs.get(privileges.all.update) === true;
      }

      // check personal view privilege
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.read &&
                    sessionPrivs.get(privileges.personal.read) === true;
      }

      // check personal update privilege
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.update &&
                    sessionPrivs.get(privileges.personal.update) === true;
      }
    }

    return isGranted;
  },

  /**
    Returns whether a user has access to update a record of this type. If a
    record is passed that involves personal privileges, it will validate
    whether that particular record is updatable.

    @returns {Boolean}
  */
  canUpdate: function(record) {
    return XT.Model._canDo.call(this, 'update', record);
  },

  /**
    Returns whether a user has access to delete a record of this type. If a
    record is passed that involves personal privileges, it will validate
    whether that particular record is deletable.

    @returns {Boolean}
  */
  canDelete: function(record) {
    return XT.Model._canDo.call(this, 'delete', record);
  },

  /** @private */
  _canDo: function(action, record) {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.privileges,
        isGrantedAll = false,
        isGrantedPersonal = false,
        userName = XT.session.details.username;

    if (sessionPrivs && sessionPrivs.get) {
      // check global privileges
      if (privileges.all && privileges.all[action]) {
        isGrantedAll = sessionPrivs.get(privileges.all[action]) === true;
      }

      // check personal privileges
      if (!isGrantedAll && privileges.personal && privileges.personal[action]) {
        isGrantedPersonal = sessionPrivs.get(privileges.personal[action]) === true;
      }
    }

    // If only personal privileges, check the original attribute cache against
    // the personal attribute list to see if we can update
    if (!isGrantedAll && isGrantedPersonal) {
      var i = 0;
      var username = XT.session.details.username;
      var props = privileges.personal && privileges.personal.properties ?
                  privileges.personal.properties : [];

      isGrantedPersonal = false;
      if (record && record._attrCache) {
        while (!isGrantedPersonal && i < props.length) {
          isGrantedPersonal = m._attrCache[props[i]].get('username') === username;
          i++;
        }
      }
    }

    return isGrantedAll || isGrantedPersonal;
  }

});

<commit_msg>make validation more extensible.<commit_after>
/**
  @class

  `XT.Model` is an abstract class designed to operate with `XT.DataSource`.
  It should be subclassed for any specific implentation. Subtypes should
  include a `recordType` the data source will use to retreive the record.

  To create a new model include `isNew` in the options like so:
    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = new XM.Contact({firstName: 'Randy'}, {isNew: true});

  To load an existing record include a guid in the options like so:
    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = new XM.Contact({guid: 1});
    m.fetch();

    OR

    XM.Contact = XT.Model.extend({recordType: 'XM.Contact'});
    m = XM.Contact();
    m.fetch({id: 1});

  @extends Backbone.RelationalModel
  @param {Object} attributes
  @param {Object} options
*/
XT.Model = Backbone.RelationalModel.extend(
  /** @scope XT.Model.prototype */ {

  idAttribute: "guid",

  /**
  Set to true if you want an id fetched from the server when the `isNew` option
  is passed on a new model.

  @type {Boolean}
  */
  autoFetchId: true,

  /**
  Specify the name of a data source model here.

  @type {String}
  */
  recordType: null,

  /**
  A hash structure that defines data access.

  @property
  @type {Hash}
  */
  privileges: {},

  /**
  An array of required attributes. A validate on the entire model will fail
  until all the required attributes have values.

  @type {Array}
  */
  required: [],

  /**
  A hash structure that defines validation functions. The `validate` function
  will iterate through each function defined on this object. Add your own custom
  validation by extending this object.
  */
  validation: {
    checkState: function(attributes, options) {
      if (this.get('dataState') === 'busy' && !this._sync) {
        return "Record is busy";
      }
    },

    checkRequired: function(attributes, options) {
      if (!attributes) {
        for (i = 0; i < this.required.length; i++) {
          if (!this.has(this.required[i])) {
            return "'" + this.required[i] + "' is required.";
          }
        }
      }
    },

    checkReadOnly: function(attributes, options) {
      var attr;

      if (attributes) {
        if (this.isReadOnly()) {
          return "Record is in a read only state.";
        } else {
          for (attr in attributes) {
            if (this.isReadOnly(attr)) {
              return "Can not edit read only attribute " + attr + ".";
            }
          }
        }
      }
    }
  },

  /**
  An array of attribute names designating attributes that are not editable.
  Use `setReadOnly` to edit this array.

  @seealso `setReadOnly`
  @seealso `isReadOnly`
  @type {Array}
  */
  readOnly: [],

  // ..........................................................
  // METHODS
  //

  /**
    Returns whether the current record can be updated based on privilege
    settings.

    @returns {Boolean}
  */
  canUpdate: function() {
    var obj = Backbone.Relational.store.getObjectByName(this.recordType);
    return obj.canUpdate(this);
  },

  /**
    Returns whether the current record can be deleted based on privilege
    settings.

    @returns {Boolean}
  */
  canDelete: function() {
    var obj = Backbone.Relational.store.getObjectByName(this.recordType);
    return obj.canDelete(this);
  },

  /**
  Set the entire model, or a specific model attribute to readOnly. Privilege
  enforcement supercedes read-only settings.

  Examples:

  model.setReadOnly() // sets model to read only
  model.setReadOnly(false) // sets model to be editable
  model.setReadOnly('name') // sets 'name' attribute to read-only
  model.setReadOnly('name', false) // sets 'name' attribute to be editable

  @param {String|Boolean} Attribute to set, or boolean if setting the model
  @param {Boolean} boolean - default = true.
  */
  setReadOnly: function(key, value) {
    // handle attribute
    if (_.isString(key)) {
      value = _.isBoolean(value) ? value : true;
      if (value && !_.contains(this.readOnly, key)) {
        this.readOnly.push(key);
      } else if (!value && _.contains(this.readOnly, key)) {
        this.readOnly = _.without(this.readOnly, key);
      }
      return;
    }

    // handle model
    key = _.isBoolean(key) ? key : true;
    this._readOnly = key;
  },

  /**
  Return whether the model is in a read-only state. If an attribute name
  is passed, returns whether that attribute is read only.

  @param {String} attribute
  */
  isReadOnly: function(attr) {
    var canNotUpdate = !this.canUpdate();
    if (!_.isString(attr) || this._readOnly || canNotUpdate) {
      return this._readOnly || canNotUpdate;
    }
    return _.contains(this.readOnly, attr);
  },

  initialize: function() {
    var options = arguments[1];

    // set data source
    this._dataSource = XT.dataSource;

    // initialize for new record
    if (options && options.isNew) {
      this.attributes.dataState = 'create';
      if (this.autoFetchId) this._fetchId();
    }

    // set id to read only
    if (this.idAttribute && !_.contains(this.readOnly, this.idAttribute)) {
      this.readOnly.push(this.idAttribute);
    }

    // set id as required
    if (this.idAttribute && !_.contains(this.required, this.idAttribute)) {
      this.required.push(this.idAttribute);
    }

    // set up state change handler
    this.on('change', this._changed);
  },

  /**
  Reimplemented. A model is new if the dataState is `create`.
  */
  isNew: function() {
    return this.get('dataState') === 'create';
  },

  /**
  Returns true when dataState is `create` or `update`.
  */
  isDirty: function() {
    var dataState = this.get('dataState');
    return dataState === 'create' || dataState === 'update';
  },

  /**
  Reimplemented to handle state change.
  */
  fetch: function(options) {
    options = options ? _.clone(options) : {};
    var that = this;
    var success = options.success;

    // flip sync flag back on success
    options.success = function(resp, status, xhr) {
      that._sync = false;
      that._attrCache = _.clone(that.attributes);
      if (success) success(model, resp, options);
    };

    return Backbone.Model.prototype.fetch.call(this, options);
  },

  /**
  Reimplemented to handle state change.
  */
  save: function(key, value, options) {
    // Handle both `("key", value)` and `({key: value})` -style calls.
    if (_.isObject(key) || key === null || key === undefined) options = value;
    options = options ? _.clone(options) : {};

    var that = this;
    var success = options.success;

    // flip sync flag back on success
    options.success = function(resp, status, xhr) {
      that._sync = false;
      that._attrCache = _.clone(that.attributes);
      if (success) success(model, resp, options);
    };

    // Handle both `("key", value)` and `({key: value})` -style calls.
    if (_.isObject(key) || key === null || key === undefined) value = options;

    return Backbone.Model.prototype.save.call(this, key, value, options);
  },

  /**
  Reimplemented.
  */
  destroy: function(options) {
    options = options ? _.clone(options) : {};
    var that = this;
    var success = options.success;

    // clear on success
    options.success = function(resp, status, xhr) {
      that.clear({silent: true});
      that._attrCache = null;
      if (success) success(model, resp, options);
    };

    this.set('dataState', 'delete');
    return Backbone.Model.prototype.destroy.call(this, options);
  },

  /**
  Overload: sync to xtuple datasource.
  */
  sync: function(method, model, options) {
    var recordType = model.recordType;
    var id = options.id || this.id;
    var success = options.success;

    if (options === undefined) options = {};
    this._sync = true;

    // read
    if (method === 'read' && recordType && id && success) {
      return this._dataSource.retrieveRecord(recordType, id, options);

    // write
    } else if (method === 'create' || method === 'update' || method === 'delete') {
      var ret = this._dataSource.commitRecord(model, options);
      this.set('dataState', 'busy');
      return ret;
    }

    return false;
  },

  /**
  Iterates through and executes each function found in the `validate`
  property.

  @seealso `validate`
  @param {Object} attributes
  @param {Object} options
  */
  validate: function(attributes, options) {
    var prop;
    var val;

    // iterate through all the properties in validation
    // and execute each function found
    for (prop in this.validation) {
      if (_.isFunction(this.validation[prop])) {
        val = this.validation[prop].call(this, attributes, options);
        if (val) return val;
      }
    }
  },

  // ..........................................................
  // PRIVATE
  //

  /** @private */
  _attrCache: null,

  /** @private */
  _dataSource: null,

  /** @private */
  _readOnly: false,

  /** @private */
  _sync: false,

  /** @private */
  _changed: function() {
    if (this.get('dataState') === 'read' && !this._sync) {
      this.set('dataState', 'update');
    }
  },

  /** @private */
  _fetchId: function() {
    var that = this;
    var options = {};

    // callback
    options.success = function(resp, status, xhr) {
      var attr = that.idAttribute;

      // set the id
      that.setReadOnly(attr, false);
      that.set(attr, resp);
      that.setReadOnly(attr, true);
    };

    // fetch id
    this._dataSource.dispatch('XT.Record', 'fetchId', this.recordType, options);
  }

});

// Class Methods
enyo.mixin( /** @scope XT.Model */ XT.Model, {

  /**
    Use this function to find out whether a user can create records before instantiating one.

    @returns {Boolean}
  */
  canCreate: function() {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.getPrivileges(),
        isGranted = false;

    if (sessionPrivs && sessionPrivs.get) {
      // check global
      isGranted = privileges.all && privileges.all.create &&
                  sessionPrivs.get(privileges.all.create) === true;

      // check personal
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.create &&
                    sessionPrivs.get(privileges.personal.create) === true;
      }
    }

    return isGranted;
  },

  /**
    Use this function to find out whether a user can read this record type
    before any have been loaded.

    @returns {Boolean}
  */
  canRead: function() {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.privileges,
        isGranted = false;

    if (sessionPrivs && sessionPrivs.get) {
      // check global read privilege
      isGranted = privileges.all && privileges.all.read &&
                  sessionPrivs.get(privileges.all.read) === true;

      // check global update privilege
      if (!isGranted) {
        isGranted = privileges.all && privileges.all.update &&
                    sessionPrivs.get(privileges.all.update) === true;
      }

      // check personal view privilege
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.read &&
                    sessionPrivs.get(privileges.personal.read) === true;
      }

      // check personal update privilege
      if (!isGranted) {
        isGranted = privileges.personal && privileges.personal.update &&
                    sessionPrivs.get(privileges.personal.update) === true;
      }
    }

    return isGranted;
  },

  /**
    Returns whether a user has access to update a record of this type. If a
    record is passed that involves personal privileges, it will validate
    whether that particular record is updatable.

    @returns {Boolean}
  */
  canUpdate: function(record) {
    return XT.Model._canDo.call(this, 'update', record);
  },

  /**
    Returns whether a user has access to delete a record of this type. If a
    record is passed that involves personal privileges, it will validate
    whether that particular record is deletable.

    @returns {Boolean}
  */
  canDelete: function(record) {
    return XT.Model._canDo.call(this, 'delete', record);
  },

  /** @private */
  _canDo: function(action, record) {
    var privileges = this.prototype.privileges,
        sessionPrivs = XT.session.privileges,
        isGrantedAll = false,
        isGrantedPersonal = false,
        userName = XT.session.details.username;

    if (sessionPrivs && sessionPrivs.get) {
      // check global privileges
      if (privileges.all && privileges.all[action]) {
        isGrantedAll = sessionPrivs.get(privileges.all[action]) === true;
      }

      // check personal privileges
      if (!isGrantedAll && privileges.personal && privileges.personal[action]) {
        isGrantedPersonal = sessionPrivs.get(privileges.personal[action]) === true;
      }
    }

    // If only personal privileges, check the original attribute cache against
    // the personal attribute list to see if we can update
    if (!isGrantedAll && isGrantedPersonal) {
      var i = 0;
      var username = XT.session.details.username;
      var props = privileges.personal && privileges.personal.properties ?
                  privileges.personal.properties : [];

      isGrantedPersonal = false;
      if (record && record._attrCache) {
        while (!isGrantedPersonal && i < props.length) {
          isGrantedPersonal = m._attrCache[props[i]].get('username') === username;
          i++;
        }
      }
    }

    return isGrantedAll || isGrantedPersonal;
  }

});

<|endoftext|>


<commit_before>// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol

(function() {

  if (window.WebSocket) return;

  var console = window.console;
  if (!console || !console.log || !console.error) {
    console = {log: function(){ }, error: function(){ }};
  }

  // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
  if (!swfobject.getFlashPlayerVersion().major >= 10) {
    console.error("Flash Player >= 10.0.0 is required.");
    return;
  }
  if (location.protocol == "file:") {
    console.error(
      "WARNING: web-socket-js doesn't work in file:///... URL " +
      "unless you set Flash Security Settings properly. " +
      "Open the page via Web server i.e. http://...");
  }

  /**
   * This class represents a faux web socket.
   * @param {string} url
   * @param {array or string} protocols
   * @param {string} proxyHost
   * @param {int} proxyPort
   * @param {string} headers
   */
  WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
    var self = this;
    self.__id = WebSocket.__nextId++;
    WebSocket.__instances[self.__id] = self;
    self.readyState = WebSocket.CONNECTING;
    self.bufferedAmount = 0;
    self.__events = {};
    if (!protocols) {
      protocols = [];
    } else if (typeof protocols == "string") {
      protocols = [protocols];
    }
    // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
    // Otherwise, when onopen fires immediately, onopen is called before it is set.
    setTimeout(function() {
      WebSocket.__addTask(function() {
        WebSocket.__flash.create(
            self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
      });
    }, 0);
  };

  /**
   * Send data to the web socket.
   * @param {string} data  The data to send to the socket.
   * @return {boolean}  True for success, false for failure.
   */
  WebSocket.prototype.send = function(data) {
    if (this.readyState == WebSocket.CONNECTING) {
      throw "INVALID_STATE_ERR: Web Socket connection has not been established";
    }
    // We use encodeURIComponent() here, because FABridge doesn't work if
    // the argument includes some characters. We don't use escape() here
    // because of this:
    // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
    // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
    // preserve all Unicode characters either e.g. "\uffff" in Firefox.
    // Note by wtritch: Hopefully this will not be necessary using ExternalInterface.  Will require
    // additional testing.
    var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
    if (result < 0) { // success
      return true;
    } else {
      this.bufferedAmount += result;
      return false;
    }
  };

  /**
   * Close this web socket gracefully.
   */
  WebSocket.prototype.close = function() {
    if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
      return;
    }
    this.readyState = WebSocket.CLOSING;
    WebSocket.__flash.close(this.__id);
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {string} type
   * @param {function} listener
   * @param {boolean} useCapture
   * @return void
   */
  WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
    if (!(type in this.__events)) {
      this.__events[type] = [];
    }
    this.__events[type].push(listener);
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {string} type
   * @param {function} listener
   * @param {boolean} useCapture
   * @return void
   */
  WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
    if (!(type in this.__events)) return;
    var events = this.__events[type];
    for (var i = events.length - 1; i >= 0; --i) {
      if (events[i] === listener) {
        events.splice(i, 1);
        break;
      }
    }
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {Event} event
   * @return void
   */
  WebSocket.prototype.dispatchEvent = function(event) {
    var events = this.__events[event.type] || [];
    for (var i = 0; i < events.length; ++i) {
      events[i](event);
    }
    var handler = this["on" + event.type];
    if (handler) handler(event);
  };

  /**
   * Handles an event from Flash.
   * @param {Object} flashEvent
   */
  WebSocket.prototype.__handleEvent = function(flashEvent) {
    if ("readyState" in flashEvent) {
      this.readyState = flashEvent.readyState;
    }
    if ("protocol" in flashEvent) {
      this.protocol = flashEvent.protocol;
    }

    var jsEvent;
    if (flashEvent.type == "open" || flashEvent.type == "error") {
      jsEvent = this.__createSimpleEvent(flashEvent.type);
    } else if (flashEvent.type == "close") {
      // TODO implement jsEvent.wasClean
      jsEvent = this.__createSimpleEvent("close");
    } else if (flashEvent.type == "message") {
      var data = decodeURIComponent(flashEvent.message);
      jsEvent = this.__createMessageEvent("message", data);
    } else {
      throw "unknown event type: " + flashEvent.type;
    }

    this.dispatchEvent(jsEvent);
  };

  WebSocket.prototype.__createSimpleEvent = function(type) {
    if (document.createEvent && window.Event) {
      var event = document.createEvent("Event");
      event.initEvent(type, false, false);
      return event;
    } else {
      return {type: type, bubbles: false, cancelable: false};
    }
  };

  WebSocket.prototype.__createMessageEvent = function(type, data) {
    if (document.createEvent && window.MessageEvent && !window.opera) {
      var event = document.createEvent("MessageEvent");
      event.initMessageEvent("message", false, false, data, null, null, window, null);
      return event;
    } else {
      // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
      return {type: type, data: data, bubbles: false, cancelable: false};
    }
  };

  /**
   * Define the WebSocket readyState enumeration.
   */
  WebSocket.CONNECTING = 0;
  WebSocket.OPEN = 1;
  WebSocket.CLOSING = 2;
  WebSocket.CLOSED = 3;

  WebSocket.__flash = null;
  WebSocket.__instances = {};
  WebSocket.__tasks = [];
  WebSocket.__nextId = 0;

  /**
   * Load a new flash security policy file.
   * @param {string} url
   */
  WebSocket.loadFlashPolicyFile = function(url){
    WebSocket.__addTask(function() {
      WebSocket.__flash.loadManualPolicyFile(url);
    });
  };

  /**
   * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
   */
  WebSocket.__initialize = function() {
    if (WebSocket.__flash) return;

    if (WebSocket.__swfLocation) {
      // For backword compatibility.
      window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
    }
    if (!window.WEB_SOCKET_SWF_LOCATION) {
      console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
      return;
    }
    if (!WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
        WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
      var swfHost = RegExp.$1;
      if (location.host != swfHost) {
        console.error(
            "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
            "('" + location.host + "' != '" + swfHost + "'). " +
            "See also 'How to host HTML file and SWF file in different domains' section " +
            "in README.md.");
      }
    }
    var container = document.createElement("div");
    container.id = "webSocketContainer";
    // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
    // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
    // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
    // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
    // the best we can do as far as we know now.
    container.style.position = "absolute";
    if (WebSocket.__isFlashLite()) {
      container.style.left = "0px";
      container.style.top = "0px";
    } else {
      container.style.left = "-100px";
      container.style.top = "-100px";
    }
    var holder = document.createElement("div");
    holder.id = "webSocketFlash";
    container.appendChild(holder);
    document.body.appendChild(container);
    // See this article for hasPriority:
    // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
    swfobject.embedSWF(
      WEB_SOCKET_SWF_LOCATION,
      "webSocketFlash",
      "1" /* width */,
      "1" /* height */,
      "10.0.0" /* SWF version */,
      null,
      null,
      {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
      null,
      function(e) {
        if (!e.success) {
          console.error("[WebSocket] swfobject.embedSWF failed");
        }
      });
  };

  /**
   * Called by Flash to notify JS that it's fully loaded and ready
   * for communication.
   */
  WebSocket.__onFlashInitialized = function() {
    // We need to set a timeout here to avoid round-trip calls
    // to flash during the initialization process.
    setTimeout(function() {
      WebSocket.__flash = document.getElementById("webSocketFlash");
      WebSocket.__flash.setCallerUrl(location.href);
      WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
      for (var i = 0; i < WebSocket.__tasks.length; ++i) {
        WebSocket.__tasks[i]();
      }
      WebSocket.__tasks = [];
    }, 0);
  };

  /**
   * Called by Flash to notify WebSockets events are fired.
   */
  WebSocket.__onFlashEvent = function() {
    setTimeout(function() {
      try {
        // Gets events using receiveEvents() instead of getting it from event object
        // of Flash event. This is to make sure to keep message order.
        // It seems sometimes Flash events don't arrive in the same order as they are sent.
        var events = WebSocket.__flash.receiveEvents();
        for (var i = 0; i < events.length; ++i) {
          WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
        }
      } catch (e) {
        console.error(e);
      }
    }, 0);
    return true;
  };

  // Called by Flash.
  WebSocket.__log = function(message) {
    console.log(decodeURIComponent(message));
  };

  // Called by Flash.
  WebSocket.__error = function(message) {
    console.error(decodeURIComponent(message));
  };

  WebSocket.__addTask = function(task) {
    if (WebSocket.__flash) {
      task();
    } else {
      WebSocket.__tasks.push(task);
    }
  };

  /**
   * Test if the browser is running flash lite.
   * @return {boolean} True if flash lite is running, false otherwise.
   */
  WebSocket.__isFlashLite = function() {
    if (!window.navigator || !window.navigator.mimeTypes) {
      return false;
    }
    var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
    if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
      return false;
    }
    return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
  };

  if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
    if (window.addEventListener) {
      window.addEventListener("load", function(){
        WebSocket.__initialize();
      }, false);
    } else {
      window.attachEvent("onload", function(){
        WebSocket.__initialize();
      });
    }
  }

})();
<commit_msg>Add WebSocketLogger and use it in every submodule. This allow to easly manage debug switch, but also allow overwrite of logger for use in other libraries using web-socket-js.<commit_after>// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol

(function() {

  if (window.WebSocket) return;

  if (!window.WebSocketLogger || !window.WebSocketLogger.log || !window.WebSocketLogger.error) {
    var temp_console = window.console;
    if (!temp_console || !temp_console.log || !temp_console.error) {
      temp_console = {log: function(){ }, error: function(){ }};
    }
    window.WebSocketLogger = {
      log:   function(msg) { if (window.WEB_SOCKET_DEBUG) temp_console.log(msg)   },
      error: function(msg) { if (window.WEB_SOCKET_DEBUG) temp_console.error(msg) }
    }
  }

  // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
  if (!swfobject.getFlashPlayerVersion().major >= 10) {
    window.WebSocketLogger.error("Flash Player >= 10.0.0 is required.");
    return;
  }
  if (location.protocol == "file:") {
    window.WebSocketLogger.error(
      "WARNING: web-socket-js doesn't work in file:///... URL " +
      "unless you set Flash Security Settings properly. " +
      "Open the page via Web server i.e. http://...");
  }

  /**
   * This class represents a faux web socket.
   * @param {string} url
   * @param {array or string} protocols
   * @param {string} proxyHost
   * @param {int} proxyPort
   * @param {string} headers
   */
  WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
    var self = this;
    self.__id = WebSocket.__nextId++;
    WebSocket.__instances[self.__id] = self;
    self.readyState = WebSocket.CONNECTING;
    self.bufferedAmount = 0;
    self.__events = {};
    if (!protocols) {
      protocols = [];
    } else if (typeof protocols == "string") {
      protocols = [protocols];
    }
    // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
    // Otherwise, when onopen fires immediately, onopen is called before it is set.
    setTimeout(function() {
      WebSocket.__addTask(function() {
        WebSocket.__flash.create(
            self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
      });
    }, 0);
  };

  /**
   * Send data to the web socket.
   * @param {string} data  The data to send to the socket.
   * @return {boolean}  True for success, false for failure.
   */
  WebSocket.prototype.send = function(data) {
    if (this.readyState == WebSocket.CONNECTING) {
      throw "INVALID_STATE_ERR: Web Socket connection has not been established";
    }
    // We use encodeURIComponent() here, because FABridge doesn't work if
    // the argument includes some characters. We don't use escape() here
    // because of this:
    // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
    // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
    // preserve all Unicode characters either e.g. "\uffff" in Firefox.
    // Note by wtritch: Hopefully this will not be necessary using ExternalInterface.  Will require
    // additional testing.
    var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
    if (result < 0) { // success
      return true;
    } else {
      this.bufferedAmount += result;
      return false;
    }
  };

  /**
   * Close this web socket gracefully.
   */
  WebSocket.prototype.close = function() {
    if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
      return;
    }
    this.readyState = WebSocket.CLOSING;
    WebSocket.__flash.close(this.__id);
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {string} type
   * @param {function} listener
   * @param {boolean} useCapture
   * @return void
   */
  WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
    if (!(type in this.__events)) {
      this.__events[type] = [];
    }
    this.__events[type].push(listener);
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {string} type
   * @param {function} listener
   * @param {boolean} useCapture
   * @return void
   */
  WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
    if (!(type in this.__events)) return;
    var events = this.__events[type];
    for (var i = events.length - 1; i >= 0; --i) {
      if (events[i] === listener) {
        events.splice(i, 1);
        break;
      }
    }
  };

  /**
   * Implementation of {@link DOM 2 EventTarget Interface}
   *
   * @param {Event} event
   * @return void
   */
  WebSocket.prototype.dispatchEvent = function(event) {
    var events = this.__events[event.type] || [];
    for (var i = 0; i < events.length; ++i) {
      events[i](event);
    }
    var handler = this["on" + event.type];
    if (handler) handler(event);
  };

  /**
   * Handles an event from Flash.
   * @param {Object} flashEvent
   */
  WebSocket.prototype.__handleEvent = function(flashEvent) {
    if ("readyState" in flashEvent) {
      this.readyState = flashEvent.readyState;
    }
    if ("protocol" in flashEvent) {
      this.protocol = flashEvent.protocol;
    }

    var jsEvent;
    if (flashEvent.type == "open" || flashEvent.type == "error") {
      jsEvent = this.__createSimpleEvent(flashEvent.type);
    } else if (flashEvent.type == "close") {
      // TODO implement jsEvent.wasClean
      jsEvent = this.__createSimpleEvent("close");
    } else if (flashEvent.type == "message") {
      var data = decodeURIComponent(flashEvent.message);
      jsEvent = this.__createMessageEvent("message", data);
    } else {
      throw "unknown event type: " + flashEvent.type;
    }

    this.dispatchEvent(jsEvent);
  };

  WebSocket.prototype.__createSimpleEvent = function(type) {
    if (document.createEvent && window.Event) {
      var event = document.createEvent("Event");
      event.initEvent(type, false, false);
      return event;
    } else {
      return {type: type, bubbles: false, cancelable: false};
    }
  };

  WebSocket.prototype.__createMessageEvent = function(type, data) {
    if (document.createEvent && window.MessageEvent && !window.opera) {
      var event = document.createEvent("MessageEvent");
      event.initMessageEvent("message", false, false, data, null, null, window, null);
      return event;
    } else {
      // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
      return {type: type, data: data, bubbles: false, cancelable: false};
    }
  };

  /**
   * Define the WebSocket readyState enumeration.
   */
  WebSocket.CONNECTING = 0;
  WebSocket.OPEN = 1;
  WebSocket.CLOSING = 2;
  WebSocket.CLOSED = 3;

  WebSocket.__flash = null;
  WebSocket.__instances = {};
  WebSocket.__tasks = [];
  WebSocket.__nextId = 0;

  /**
   * Load a new flash security policy file.
   * @param {string} url
   */
  WebSocket.loadFlashPolicyFile = function(url){
    WebSocket.__addTask(function() {
      WebSocket.__flash.loadManualPolicyFile(url);
    });
  };

  /**
   * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
   */
  WebSocket.__initialize = function() {
    if (WebSocket.__flash) return;

    if (WebSocket.__swfLocation) {
      // For backword compatibility.
      window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
    }
    if (!window.WEB_SOCKET_SWF_LOCATION) {
      window.WebSocketLogger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
      return;
    }
    if (!WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
        WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
      var swfHost = RegExp.$1;
      if (location.host != swfHost) {
        window.WebSocketLogger.error(
            "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
            "('" + location.host + "' != '" + swfHost + "'). " +
            "See also 'How to host HTML file and SWF file in different domains' section " +
            "in README.md.");
      }
    }
    var container = document.createElement("div");
    container.id = "webSocketContainer";
    // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
    // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
    // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
    // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
    // the best we can do as far as we know now.
    container.style.position = "absolute";
    if (WebSocket.__isFlashLite()) {
      container.style.left = "0px";
      container.style.top = "0px";
    } else {
      container.style.left = "-100px";
      container.style.top = "-100px";
    }
    var holder = document.createElement("div");
    holder.id = "webSocketFlash";
    container.appendChild(holder);
    document.body.appendChild(container);
    // See this article for hasPriority:
    // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
    swfobject.embedSWF(
      WEB_SOCKET_SWF_LOCATION,
      "webSocketFlash",
      "1" /* width */,
      "1" /* height */,
      "10.0.0" /* SWF version */,
      null,
      null,
      {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
      null,
      function(e) {
        if (!e.success) {
          window.WebSocketLogger.error("[WebSocket] swfobject.embedSWF failed");
        }
      });
  };

  /**
   * Called by Flash to notify JS that it's fully loaded and ready
   * for communication.
   */
  WebSocket.__onFlashInitialized = function() {
    // We need to set a timeout here to avoid round-trip calls
    // to flash during the initialization process.
    setTimeout(function() {
      WebSocket.__flash = document.getElementById("webSocketFlash");
      WebSocket.__flash.setCallerUrl(location.href);
      WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
      for (var i = 0; i < WebSocket.__tasks.length; ++i) {
        WebSocket.__tasks[i]();
      }
      WebSocket.__tasks = [];
    }, 0);
  };

  /**
   * Called by Flash to notify WebSockets events are fired.
   */
  WebSocket.__onFlashEvent = function() {
    setTimeout(function() {
      try {
        // Gets events using receiveEvents() instead of getting it from event object
        // of Flash event. This is to make sure to keep message order.
        // It seems sometimes Flash events don't arrive in the same order as they are sent.
        var events = WebSocket.__flash.receiveEvents();
        for (var i = 0; i < events.length; ++i) {
          WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
        }
      } catch (e) {
        window.WebSocketLogger.error(e);
      }
    }, 0);
    return true;
  };

  // Called by Flash.
  WebSocket.__log = function(message) {
    window.WebSocketLogger.log(decodeURIComponent(message));
  };

  // Called by Flash.
  WebSocket.__error = function(message) {
    window.WebSocketLogger.error(decodeURIComponent(message));
  };

  WebSocket.__addTask = function(task) {
    if (WebSocket.__flash) {
      task();
    } else {
      WebSocket.__tasks.push(task);
    }
  };

  /**
   * Test if the browser is running flash lite.
   * @return {boolean} True if flash lite is running, false otherwise.
   */
  WebSocket.__isFlashLite = function() {
    if (!window.navigator || !window.navigator.mimeTypes) {
      return false;
    }
    var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
    if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
      return false;
    }
    return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
  };

  if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
    if (window.addEventListener) {
      window.addEventListener("load", function(){
        WebSocket.__initialize();
      }, false);
    } else {
      window.attachEvent("onload", function(){
        WebSocket.__initialize();
      });
    }
  }

})();
<|endoftext|>


<commit_before>/*
 * Copyright 2012 Amadeus s.a.s.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var util = require("util");
var events = require("events");
var dns = require("dns");

var detectBrowser = require("../util/browser-detection.js").detectBrowser;

var allowedTestUpdateEvents = {
    "testStarted": 1,
    "testFinished": 1,
    "error": 1,
    "coverage": 1
};

var wrapInTryCatch = function (scope, fct) {
    return function () {
        try {
            return fct.apply(scope, arguments);
        } catch (e) {
            console.log("Error when called from " + scope + ": ", e.stack || e);
        }
    };
};

var checkRestart = function (scope, task) {
    var restarts = task.restarts || 0;
    restarts++;
    if (restarts > scope.config.maxTaskRestarts) {
        return false;
    }
    task.restarts = restarts;
    return true;
};

var campaignTaskFinished = function (scope, restart) {
    var task = scope.currentTask;
    var campaign = scope.currentCampaign;
    scope.currentTask = null;
    scope.currentCampaign = null;
    if (scope.taskTimeoutId != null) {
        clearTimeout(scope.taskTimeoutId);
        scope.taskTimeoutId = null;
    }
    var event = {
        event: "taskFinished"
    };
    task.browser.onTaskFinished();
    feedEventWithTaskData(event, task);
    event.restartPlanned = restart && checkRestart(scope, task);
    if (event.restartPlanned) {
        task.browser.addTask(task);
    }
    campaign.addResult(event);
    if (scope.socket) {
        scope.socket.emit('slave-stop');
        scope.emitAvailable();
    }
};

var emitTaskError = function (scope, message, restart) {
    var task = scope.currentTask;
    if (task) {
        scope.currentCampaign.addResult({
            event: "error",
            taskId: task.taskId,
            taskName: task.test.name,
            error: {
                message: message
            },
            name: task.test.name
        });
        campaignTaskFinished(scope, restart);
    }
};

var feedEventWithTaskData = function (event, task) {
    event.taskId = task.taskId;
    event.taskName = task.test.name;
    event.browserName = task.browser.name;
    event.browserRemainingTasks = task.browser.pendingTasks;
    return event;
};

var Slave = function (socket, data, config) {
    this.config = config;
    this.socket = socket;
    this.userAgent = data.userAgent;
    this.browserInfo = detectBrowser(data);
    this.displayName = this.browserInfo.displayName;
    var connection = socket.request.connection;
    this.address = connection.remoteAddress;
    this.port = connection.remotePort;
    this.addressName = null; // set by onReceiveAddressName
    this.currentTask = null;
    this.currentCampaign = null;
    this.paused = !! data.paused;
    this.taskTimeoutId = null;
    dns.reverse(this.address, this.onReceiveAddressName.bind(this));
    socket.on('disconnect', wrapInTryCatch(this, this.onSocketDisconnected));
    socket.on('test-update', wrapInTryCatch(this, this.onTestUpdate));
    socket.on('task-finished', wrapInTryCatch(this, this.onTaskFinished));
    socket.on('pause-changed', wrapInTryCatch(this, this.onPauseChanged));
};

util.inherits(Slave, events.EventEmitter);

Slave.prototype.toString = function () {
    var os = this.browserInfo.os.name;
    return (this.addressName || this.address) + ":" + this.port + " (" + this.displayName + "; " + os + ")";
};

Slave.prototype.onReceiveAddressName = function (err, names) {
    if (!err && names.length > 0) {
        this.addressName = names[0];
    }
};

Slave.prototype.assignTask = function (campaign, task) {
    this.currentTask = task;
    this.currentCampaign = campaign;
    this.emit('unavailable');
    task.slave = this;
    var test = task.test;
    this.socket.emit('slave-execute', {
        url: test.url,
        name: test.name,
        taskId: task.taskId,
        campaignId: campaign.id,
        stats: {
            remainingTasks: campaign.remainingTasks,
            browserRemainingTasks: task.browser.pendingTasks
        }
    });
    this.currentCampaign.addResult({
        event: "taskStarted",
        taskId: task.taskId,
        taskName: test.name,
        slave: {
            userAgent: this.userAgent,
            address: this.address,
            addressName: this.addressName,
            port: this.port
        }
    });
    this.taskTimeoutId = setTimeout(this.taskTimeout.bind(this), this.config.taskTimeout);
};

Slave.prototype.onTestUpdate = function (event) {
    var eventName = event.event;
    if (allowedTestUpdateEvents.hasOwnProperty(eventName)) {
        feedEventWithTaskData(event, this.currentTask);
        this.currentCampaign.addResult(event);
    }
};

Slave.prototype.onPauseChanged = function (paused) {
    paused = !! paused; // makes sure it is a boolean
    if (this.paused !== paused) {
        this.paused = paused;
        if (!this.currentTask) {
            this.emit(paused ? 'unavailable' : 'available');
        }
    }
};

Slave.prototype.onSocketDisconnected = function () {
    if (this.socket) {
        this.socket = null;
        emitTaskError(this, "Browser was disconnected: " + this.toString(), true);
        this.emit('disconnect');
    }
};

Slave.prototype.onTaskFinished = function () {
    campaignTaskFinished(this);
};

Slave.prototype.disconnect = function () {
    if (this.socket) {
        this.socket.disconnect();
    }
};

Slave.prototype.isAvailable = function () {
    return this.socket && !(this.paused || this.currentTask);
};

Slave.prototype.emitAvailable = function () {
    if (this.isAvailable()) {
        this.emit('available');
    }
};

Slave.prototype.getStatus = function () {
    if (this.currentTask) {
        return this.currentTask.test.name;
    } else if (this.paused) {
        return "(paused)";
    } else {
        return "(idle)";
    }
};

Slave.prototype.taskTimeout = function () {
    this.taskTimeoutId = null;
    emitTaskError(this, "Task timeout.", false);
};

Slave.prototype.dispose = function (callback) {
    this.socket.emit('dispose');
    // Give the slave some time to die
    setTimeout(function () {
        this.disconnect();
        callback();
    }.bind(this), 100);
};

module.exports = Slave;<commit_msg>Fixing issue with missing remote address on slave connection.<commit_after>/*
 * Copyright 2012 Amadeus s.a.s.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var util = require("util");
var events = require("events");
var dns = require("dns");

var detectBrowser = require("../util/browser-detection.js").detectBrowser;

var allowedTestUpdateEvents = {
    "testStarted": 1,
    "testFinished": 1,
    "error": 1,
    "coverage": 1
};

var wrapInTryCatch = function (scope, fct) {
    return function () {
        try {
            return fct.apply(scope, arguments);
        } catch (e) {
            console.log("Error when called from " + scope + ": ", e.stack || e);
        }
    };
};

var checkRestart = function (scope, task) {
    var restarts = task.restarts || 0;
    restarts++;
    if (restarts > scope.config.maxTaskRestarts) {
        return false;
    }
    task.restarts = restarts;
    return true;
};

var campaignTaskFinished = function (scope, restart) {
    var task = scope.currentTask;
    var campaign = scope.currentCampaign;
    scope.currentTask = null;
    scope.currentCampaign = null;
    if (scope.taskTimeoutId != null) {
        clearTimeout(scope.taskTimeoutId);
        scope.taskTimeoutId = null;
    }
    var event = {
        event: "taskFinished"
    };
    task.browser.onTaskFinished();
    feedEventWithTaskData(event, task);
    event.restartPlanned = restart && checkRestart(scope, task);
    if (event.restartPlanned) {
        task.browser.addTask(task);
    }
    campaign.addResult(event);
    if (scope.socket) {
        scope.socket.emit('slave-stop');
        scope.emitAvailable();
    }
};

var emitTaskError = function (scope, message, restart) {
    var task = scope.currentTask;
    if (task) {
        scope.currentCampaign.addResult({
            event: "error",
            taskId: task.taskId,
            taskName: task.test.name,
            error: {
                message: message
            },
            name: task.test.name
        });
        campaignTaskFinished(scope, restart);
    }
};

var feedEventWithTaskData = function (event, task) {
    event.taskId = task.taskId;
    event.taskName = task.test.name;
    event.browserName = task.browser.name;
    event.browserRemainingTasks = task.browser.pendingTasks;
    return event;
};

var Slave = function (socket, data, config) {
    this.config = config;
    this.socket = socket;
    this.userAgent = data.userAgent;
    this.browserInfo = detectBrowser(data);
    this.displayName = this.browserInfo.displayName;
    this.address = socket.conn.remoteAddress;
    this.port = socket.request.connection.remotePort;
    this.addressName = null; // set by onReceiveAddressName
    this.currentTask = null;
    this.currentCampaign = null;
    this.paused = !! data.paused;
    this.taskTimeoutId = null;
    dns.reverse(this.address, this.onReceiveAddressName.bind(this));
    socket.on('disconnect', wrapInTryCatch(this, this.onSocketDisconnected));
    socket.on('test-update', wrapInTryCatch(this, this.onTestUpdate));
    socket.on('task-finished', wrapInTryCatch(this, this.onTaskFinished));
    socket.on('pause-changed', wrapInTryCatch(this, this.onPauseChanged));
};

util.inherits(Slave, events.EventEmitter);

Slave.prototype.toString = function () {
    var os = this.browserInfo.os.name;
    return (this.addressName || this.address) + ":" + this.port + " (" + this.displayName + "; " + os + ")";
};

Slave.prototype.onReceiveAddressName = function (err, names) {
    if (!err && names.length > 0) {
        this.addressName = names[0];
    }
};

Slave.prototype.assignTask = function (campaign, task) {
    this.currentTask = task;
    this.currentCampaign = campaign;
    this.emit('unavailable');
    task.slave = this;
    var test = task.test;
    this.socket.emit('slave-execute', {
        url: test.url,
        name: test.name,
        taskId: task.taskId,
        campaignId: campaign.id,
        stats: {
            remainingTasks: campaign.remainingTasks,
            browserRemainingTasks: task.browser.pendingTasks
        }
    });
    this.currentCampaign.addResult({
        event: "taskStarted",
        taskId: task.taskId,
        taskName: test.name,
        slave: {
            userAgent: this.userAgent,
            address: this.address,
            addressName: this.addressName,
            port: this.port
        }
    });
    this.taskTimeoutId = setTimeout(this.taskTimeout.bind(this), this.config.taskTimeout);
};

Slave.prototype.onTestUpdate = function (event) {
    var eventName = event.event;
    if (allowedTestUpdateEvents.hasOwnProperty(eventName)) {
        feedEventWithTaskData(event, this.currentTask);
        this.currentCampaign.addResult(event);
    }
};

Slave.prototype.onPauseChanged = function (paused) {
    paused = !! paused; // makes sure it is a boolean
    if (this.paused !== paused) {
        this.paused = paused;
        if (!this.currentTask) {
            this.emit(paused ? 'unavailable' : 'available');
        }
    }
};

Slave.prototype.onSocketDisconnected = function () {
    if (this.socket) {
        this.socket = null;
        emitTaskError(this, "Browser was disconnected: " + this.toString(), true);
        this.emit('disconnect');
    }
};

Slave.prototype.onTaskFinished = function () {
    campaignTaskFinished(this);
};

Slave.prototype.disconnect = function () {
    if (this.socket) {
        this.socket.disconnect();
    }
};

Slave.prototype.isAvailable = function () {
    return this.socket && !(this.paused || this.currentTask);
};

Slave.prototype.emitAvailable = function () {
    if (this.isAvailable()) {
        this.emit('available');
    }
};

Slave.prototype.getStatus = function () {
    if (this.currentTask) {
        return this.currentTask.test.name;
    } else if (this.paused) {
        return "(paused)";
    } else {
        return "(idle)";
    }
};

Slave.prototype.taskTimeout = function () {
    this.taskTimeoutId = null;
    emitTaskError(this, "Task timeout.", false);
};

Slave.prototype.dispose = function (callback) {
    this.socket.emit('dispose');
    // Give the slave some time to die
    setTimeout(function () {
        this.disconnect();
        callback();
    }.bind(this), 100);
};

module.exports = Slave;<|endoftext|>


<commit_before>'use strict';

(function() {
  describe('Menus', function() {
    //Initialize global variables
    var scope,
      Menus;

    // Load the main application module
    beforeEach(module(ApplicationConfiguration.applicationModuleName));

    beforeEach(inject(function(_Menus_) {
      Menus = _Menus_;
    }));

    it('should have topbar added', function() {
      expect(Menus.menus.topbar).toBeDefined();
    });

    it('should have default roles to user and admin', function() {
      expect(Menus.defaultRoles).toEqual(['user', 'admin']);
    });

    describe('addMenu', function() {
      describe('with no options', function() {
        var menuId = 'menu1',
          menu;
        beforeEach(function() {
          menu = Menus.addMenu(menuId);
        });

        it('should return menu object', function() {
          expect(menu).toBeDefined();
        });

        it('should default roles', function() {
          expect(menu.roles).toEqual(Menus.defaultRoles);
        });

        it('should have empty items', function() {
          expect(menu.items).toEqual([]);
        });

        it('should set shouldRender to shouldRender function handle', function() {
          expect(menu.shouldRender()).toBeFalsy();
        });
      });

      describe('with options', function() {
        var menu,
          options = {
            roles: ['a', 'b', 'c'],
            items: ['d', 'e', 'f']
          };
        beforeEach(function() {
          menu = Menus.addMenu('menu1', options);
        });

        it('should set items to options.items list', function() {
          expect(menu.items).toBe(options.items);
        });

        it('should set roles to options.roles list', function() {
          expect(menu.roles).toBe(options.roles);
        });
      });
    });

    describe('shouldRender', function() {
      var menuOptions = {
          roles: ['*', 'menurole']
        },
        menu;
      beforeEach(function() {
        menu = Menus.addMenu('menu1', menuOptions);
      });

      describe('when logged out', function() {
        it('should render if menu is public', function() {
          expect(menu.shouldRender()).toBeTruthy();
        });

        it('should not render if menu is private', function() {
          menu = Menus.addMenu('menu1', {
            isPublic: false
          });
          expect(menu.shouldRender()).toBeFalsy();
        });
      });

      describe('when logged in', function() {
        var user = {
          roles: ['1', 'menurole', '2']
        };
        describe('menu with * role', function() {
          it('should render', function() {
            expect(menu.shouldRender(user)).toBeTruthy();
          });
        });

        describe('menu without * role', function() {
          beforeEach(function() {
            menu = Menus.addMenu('menu1', {
              roles: ['b', 'menurole', 'c']
            });
          });

          it('should render if user has same role as menu', function() {
            expect(menu.shouldRender(user)).toBeTruthy();
          });

          it('should not render if user has different roles', function() {
            user = {
              roles: ['1', '2', '3']
            };
            expect(menu.shouldRender(user)).toBeFalsy();
          });
        });
      });
    });

    describe('validateMenuExistance', function() {
      describe('when menuId not provided', function() {
        it('should throw menuId error', function() {
          expect(Menus.validateMenuExistance).toThrowError('MenuId was not provided');
        });
      });

      describe('when menu does not exist', function() {
        it('should throw no menu error', function() {
          var target = function() {
            Menus.validateMenuExistance('noMenuId');
          };
          expect(target).toThrowError('Menu does not exist');
        });
      });

      describe('when menu exists', function() {
        var menuId = 'menuId';
        beforeEach(function() {
          Menus.menus[menuId] = {};
        });

        it('should return truthy', function() {
          expect(Menus.validateMenuExistance(menuId)).toBeTruthy();
        });
      });
    });

    describe('removeMenu', function() {
      var menu = {
        id: 'menuId'
      };
      beforeEach(function() {
        Menus.menus[menu.id] = menu;
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.removeMenu(menu.id);
      });

      it('should remove existing menu from menus', function() {
        expect(Menus.menus).not.toContain(menu.id);
      });

      it('validates menu existance before removing', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menu.id);
      });
    });

    describe('addMenuItem', function() {
      var menuId = 'menu1',
        subMenuItem1 = {
          title: 'sub1'
        },
        subMenuItem2 = {
          title: 'sub2'
        },
        menuItemOptions = {
          title: 'title',
          state: 'state',
          type: 'type',
          class: 'class',
          isPublic: false,
          roles: ['a', 'b'],
          position: 2,
          items: [subMenuItem1, subMenuItem2]
        },
        menu,
        menuItem;

      beforeEach(function() {
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.addSubMenuItem = jasmine.createSpy();
        Menus.addMenu(menuId, {
          roles: ['a', 'b']
        });
        menu = Menus.addMenuItem(menuId, menuItemOptions);
        menuItem = menu.items[0];
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should return the menu', function() {
        expect(menu).toBeDefined();
      });

      it('should set menu item shouldRender function', function() {
        expect(menuItem.shouldRender).toBeDefined();
      });

      describe('with options set', function() {
        it('should add menu item to menu', function() {
          expect(menu.items.length).toBe(1);
        });

        it('should set menu item title to options title', function() {
          expect(menuItem.title).toBe(menuItemOptions.title);
        });

        it('should set menu item state to options state', function() {
          expect(menuItem.state).toBe(menuItemOptions.state);
        });

        it('should set menu item type to options type', function() {
          expect(menuItem.type).toBe(menuItemOptions.type);
        });

        it('should set menu item class to options class', function() {
          expect(menuItem.class).toBe(menuItemOptions.class);
        });

        it('should set menu item position to options position', function() {
          expect(menuItem.position).toBe(menuItemOptions.position);
        });

        it('should call addSubMenuItem for each item in options', function() {
          expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.state, subMenuItem1);
          expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.state, subMenuItem2);
        });
      });

      describe('without options set', function() {
        beforeEach(function() {
          menu = Menus.addMenuItem(menuId);
          menuItem = menu.items[1];
        });

        it('should set menu item type to item', function() {
          expect(menuItem.type).toBe('item');
        });

        it('should set menu item title to empty', function() {
          expect(menuItem.title).toBe('');
        });

        it('should set menu item isPublic to false', function() {
          expect(menuItem.isPublic).toBeFalsy();
        });

        it('should set menu item roles to default roles', function() {
          expect(menuItem.roles).toEqual(Menus.defaultRoles);
        });

        it('should set menu item position to 0', function() {
          expect(menuItem.position).toBe(0);
        });
      });
    });

    describe('removeMenuItem', function() {
      var menuId = 'menuId',
        menuItemState = 'menu.state1',
        menuItemState2 = 'menu.state2',
        menu;

      beforeEach(function() {
        Menus.addMenu(menuId);
        Menus.addMenuItem(menuId, { state: menuItemState });
        Menus.addMenuItem(menuId, { state: menuItemState2 });
        Menus.validateMenuExistance = jasmine.createSpy();
        menu = Menus.removeMenuItem(menuId, menuItemState);
      });

      it('should return menu object', function() {
        expect(menu).not.toBeNull();
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should remove sub menu items with same state', function() {
        expect(menu.items.length).toBe(1);
        expect(menu.items[0].state).toBe(menuItemState2);
      });
    });

    describe('addSubMenuItem', function() {
      var subItemOptions = {
        title: 'title',
        state: 'sub.state',
        isPublic: false,
        roles: ['a', 'b'],
        position: 4
      };
      var menuId = 'menu1',
        menuItem1Options = {
          state: 'item1.state',
          items: [],
          isPublic: false
        },
        menuItem2Options = {
          state: 'item2.state2',
          items: [],
          isPublic: true,
          roles: ['a']
        },
        menuItem1,
        menuItem2,
        menuItem3,
        subItem1,
        subItem2,
        menu;

      beforeEach(function() {
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.addMenu(menuId);
        Menus.addMenuItem(menuId, menuItem1Options);
        Menus.addMenuItem(menuId, menuItem2Options);
        Menus.addMenuItem(menuId, { state: 'something.else' });
        Menus.addSubMenuItem(menuId, menuItem1Options.state, subItemOptions);
        menu = Menus.addSubMenuItem(menuId, menuItem1Options.state);
        menuItem1 = menu.items[0];
        menuItem2 = menu.items[1];
        menuItem3 = menu.items[2];
        subItem1 = menuItem1.items[0];
        subItem2 = menuItem1.items[1];
      });

      afterEach(function() {
        Menus.removeMenu(menuId);
      });

      it('should return menu object', function() {
        expect(menu).not.toBeNull();
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should not add sub menu item to menu item of different state', function() {
        expect(menuItem3.items.length).toBe(0);
      });

      it('should set shouldRender', function() {
        expect(subItem1.shouldRender).toBeDefined();
      });

      describe('with options set', function() {
        it('should add sub menu item to menu item', function() {
          expect(subItem1).toBeDefined();
        });

        it('should set title to options title', function() {
          expect(subItem1.title).toBe(subItemOptions.title);
        });

        it('should set state to options state', function() {
          expect(subItem1.state).toBe(subItemOptions.state);
        });

        it('should set roles to options roles', function() {
          expect(subItem1.roles).toEqual(subItemOptions.roles);
        });

        it('should set position to options position', function() {
          expect(subItem1.position).toEqual(subItemOptions.position);
        });
      });

      describe('without optoins set', function() {
        it('should add sub menu item to menu item', function() {
          expect(subItem2).toBeDefined();
        });

        it('should set isPublic to parent isPublic', function() {
          expect(subItem2.isPublic).toBe(menuItem1.isPublic);
        });

        it('should set title to blank', function() {
          expect(subItem2.title).toBe('');
        });

        it('should set state to blank', function() {
          expect(subItem2.state).toBe('');
        });

        it('should set roles to parent roles', function() {
          expect(subItem2.roles).toEqual(menuItem1.roles);
        });

        it('should set position to 0', function() {
          expect(subItem2.position).toBe(0);
        });
      });

      describe('then removeSubMenuItem', function() {
        beforeEach(function() {
          Menus.validateMenuExistance = jasmine.createSpy();
          menu = Menus.removeSubMenuItem(menuId, subItem1.state);
        });

        it('should validate menu existance', function() {
          expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
        });

        it('should return menu object', function() {
          expect(menu).toBeDefined();
        });

        it('should remove sub menu item', function() {
          expect(menuItem1.items.length).toBe(1);
          expect(menuItem1.items[0].state).toEqual(subItem2.state);
        });
      });
    });
  });
})();
<commit_msg>Client Fixes<commit_after>'use strict';

(function() {
  describe('Menus', function() {
    //Initialize global variables
    var scope,
      Menus;

    // Load the main application module
    beforeEach(module(ApplicationConfiguration.applicationModuleName));

    beforeEach(inject(function(_Menus_) {
      Menus = _Menus_;
    }));

    it('should have topbar added', function() {
      expect(Menus.menus.topbar).toBeDefined();
    });

    it('should have default roles to user and admin', function() {
      expect(Menus.defaultRoles).toEqual(['user', 'admin']);
    });

    describe('addMenu', function() {
      describe('with no options', function() {
        var menuId = 'menu1',
          menu;
        beforeEach(function() {
          menu = Menus.addMenu(menuId);
        });

        it('should return menu object', function() {
          expect(menu).toBeDefined();
        });

        it('should default roles', function() {
          expect(menu.roles).toEqual(Menus.defaultRoles);
        });

        it('should have empty items', function() {
          expect(menu.items).toEqual([]);
        });

        it('should set shouldRender to shouldRender function handle', function() {
          expect(menu.shouldRender()).toBeFalsy();
        });
      });

      describe('with options', function() {
        var menu,
          options = {
            roles: ['a', 'b', 'c'],
            items: ['d', 'e', 'f']
          };
        beforeEach(function() {
          menu = Menus.addMenu('menu1', options);
        });

        it('should set items to options.items list', function() {
          expect(menu.items).toBe(options.items);
        });

        it('should set roles to options.roles list', function() {
          expect(menu.roles).toBe(options.roles);
        });
      });
    });

    describe('shouldRender', function() {
      var menuOptions = {
          roles: ['*', 'menurole']
        },
        menu;
      beforeEach(function() {
        menu = Menus.addMenu('menu1', menuOptions);
      });

      describe('when logged out', function() {
        it('should render if menu is public', function() {
          expect(menu.shouldRender()).toBeTruthy();
        });

        it('should not render if menu is private', function() {
          menu = Menus.addMenu('menu1', {
            isPublic: false
          });
          expect(menu.shouldRender()).toBeFalsy();
        });
      });

      describe('when logged in', function() {
        var user = {
          roles: ['1', 'menurole', '2']
        };
        describe('menu with * role', function() {
          it('should render', function() {
            expect(menu.shouldRender(user)).toBeTruthy();
          });
        });

        describe('menu without * role', function() {
          beforeEach(function() {
            menu = Menus.addMenu('menu1', {
              roles: ['b', 'menurole', 'c']
            });
          });

          it('should render if user has same role as menu', function() {
            expect(menu.shouldRender(user)).toBeTruthy();
          });

          it('should not render if user has different roles', function() {
            user = {
              roles: ['1', '2', '3']
            };
            expect(menu.shouldRender(user)).toBeFalsy();
          });
        });
      });
    });

    describe('validateMenuExistance', function() {
      describe('when menuId not provided', function() {
        it('should throw menuId error', function() {
          expect(Menus.validateMenuExistance).toThrowError('MenuId was not provided');
        });
      });

      describe('when menu does not exist', function() {
        it('should throw no menu error', function() {
          var target = function() {
            Menus.validateMenuExistance('noMenuId');
          };
          expect(target).toThrowError('Menu does not exist');
        });
      });

      describe('when menu exists', function() {
        var menuId = 'menuId';
        beforeEach(function() {
          Menus.menus[menuId] = {};
        });

        it('should return truthy', function() {
          expect(Menus.validateMenuExistance(menuId)).toBeTruthy();
        });
      });
    });

    describe('removeMenu', function() {
      var menu = {
        id: 'menuId'
      };
      beforeEach(function() {
        Menus.menus[menu.id] = menu;
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.removeMenu(menu.id);
      });

      it('should remove existing menu from menus', function() {
        expect(Menus.menus).not.toContain(menu.id);
      });

      it('validates menu existance before removing', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menu.id);
      });
    });

    describe('addMenuItem', function() {
      var menuId = 'menu1',
        subMenuItem1 = {
          title: 'sub1'
        },
        subMenuItem2 = {
          title: 'sub2'
        },
        menuItemOptions = {
          title: 'title',
          state: 'state',
          icon: 'icon',
          type: 'type',
          class: 'class',
          isPublic: false,
          roles: ['a', 'b'],
          position: 2,
          items: [subMenuItem1, subMenuItem2]
        },
        menu,
        menuItem;

      beforeEach(function() {
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.addSubMenuItem = jasmine.createSpy();
        Menus.addMenu(menuId, {
          roles: ['a', 'b']
        });
        menu = Menus.addMenuItem(menuId, menuItemOptions);
        menuItem = menu.items[0];
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should return the menu', function() {
        expect(menu).toBeDefined();
      });

      it('should set menu item shouldRender function', function() {
        expect(menuItem.shouldRender).toBeDefined();
      });

      describe('with options set', function() {
        it('should add menu item to menu', function() {
          expect(menu.items.length).toBe(1);
        });

        it('should set menu item title to options title', function() {
          expect(menuItem.title).toBe(menuItemOptions.title);
        });

        it('should set menu item state to options state', function() {
          expect(menuItem.state).toBe(menuItemOptions.state);
        });

        it('should set menu item type to options type', function() {
          expect(menuItem.type).toBe(menuItemOptions.type);
        });

        it('should set menu item class to options class', function() {
          expect(menuItem.class).toBe(menuItemOptions.class);
        });

        it('should set menu item position to options position', function() {
          expect(menuItem.position).toBe(menuItemOptions.position);
        });

        it('should call addSubMenuItem for each item in options', function() {
          expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.state, menuItemOptions.icon, subMenuItem1);
          expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.state, menuItemOptions.icon, subMenuItem2);
        });
      });

      describe('without options set', function() {
        beforeEach(function() {
          menu = Menus.addMenuItem(menuId);
          menuItem = menu.items[1];
        });

        it('should set menu item type to item', function() {
          expect(menuItem.type).toBe('item');
        });

        it('should set menu item title to empty', function() {
          expect(menuItem.title).toBe('');
        });

        it('should set menu item isPublic to false', function() {
          expect(menuItem.isPublic).toBeFalsy();
        });

        it('should set menu item roles to default roles', function() {
          expect(menuItem.roles).toEqual(Menus.defaultRoles);
        });

        it('should set menu item position to 0', function() {
          expect(menuItem.position).toBe(0);
        });
      });
    });

    describe('removeMenuItem', function() {
      var menuId = 'menuId',
        menuItemState = 'menu.state1',
        menuItemState2 = 'menu.state2',
        menu;

      beforeEach(function() {
        Menus.addMenu(menuId);
        Menus.addMenuItem(menuId, { state: menuItemState });
        Menus.addMenuItem(menuId, { state: menuItemState2 });
        Menus.validateMenuExistance = jasmine.createSpy();
        menu = Menus.removeMenuItem(menuId, menuItemState);
      });

      it('should return menu object', function() {
        expect(menu).not.toBeNull();
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should remove sub menu items with same state', function() {
        expect(menu.items.length).toBe(1);
        expect(menu.items[0].state).toBe(menuItemState2);
      });
    });

    describe('addSubMenuItem', function() {
      var subItemOptions = {
        title: 'title',
        state: 'sub.state',
        isPublic: false,
        roles: ['a', 'b'],
        position: 4
      };
      var menuId = 'menu1',
        menuItem1Options = {
          state: 'item1.state',
          items: [],
          isPublic: false
        },
        menuItem2Options = {
          state: 'item2.state2',
          items: [],
          isPublic: true,
          roles: ['a']
        },
        menuItem1,
        menuItem2,
        menuItem3,
        subItem1,
        subItem2,
        menu;

      beforeEach(function() {
        Menus.validateMenuExistance = jasmine.createSpy();
        Menus.addMenu(menuId);
        Menus.addMenuItem(menuId, menuItem1Options);
        Menus.addMenuItem(menuId, menuItem2Options);
        Menus.addMenuItem(menuId, { state: 'something.else' });
        Menus.addSubMenuItem(menuId, menuItem1Options.state, subItemOptions);
        menu = Menus.addSubMenuItem(menuId, menuItem1Options.state);
        menuItem1 = menu.items[0];
        menuItem2 = menu.items[1];
        menuItem3 = menu.items[2];
        subItem1 = menuItem1.items[0];
        subItem2 = menuItem1.items[1];
      });

      afterEach(function() {
        Menus.removeMenu(menuId);
      });

      it('should return menu object', function() {
        expect(menu).not.toBeNull();
      });

      it('should validate menu existance', function() {
        expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
      });

      it('should not add sub menu item to menu item of different state', function() {
        expect(menuItem3.items.length).toBe(0);
      });

      it('should set shouldRender', function() {
        expect(subItem1.shouldRender).toBeDefined();
      });

      describe('with options set', function() {
        it('should add sub menu item to menu item', function() {
          expect(subItem1).toBeDefined();
        });

        it('should set title to options title', function() {
          expect(subItem1.title).toBe(subItemOptions.title);
        });

        it('should set state to options state', function() {
          expect(subItem1.state).toBe(subItemOptions.state);
        });

        it('should set roles to options roles', function() {
          expect(subItem1.roles).toEqual(subItemOptions.roles);
        });

        it('should set position to options position', function() {
          expect(subItem1.position).toEqual(subItemOptions.position);
        });
      });

      describe('without optoins set', function() {
        it('should add sub menu item to menu item', function() {
          expect(subItem2).toBeDefined();
        });

        it('should set isPublic to parent isPublic', function() {
          expect(subItem2.isPublic).toBe(menuItem1.isPublic);
        });

        it('should set title to blank', function() {
          expect(subItem2.title).toBe('');
        });

        it('should set state to blank', function() {
          expect(subItem2.state).toBe('');
        });

        it('should set roles to parent roles', function() {
          expect(subItem2.roles).toEqual(menuItem1.roles);
        });

        it('should set position to 0', function() {
          expect(subItem2.position).toBe(0);
        });
      });

      describe('then removeSubMenuItem', function() {
        beforeEach(function() {
          Menus.validateMenuExistance = jasmine.createSpy();
          menu = Menus.removeSubMenuItem(menuId, subItem1.state);
        });

        it('should validate menu existance', function() {
          expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId);
        });

        it('should return menu object', function() {
          expect(menu).toBeDefined();
        });

        it('should remove sub menu item', function() {
          expect(menuItem1.items.length).toBe(1);
          expect(menuItem1.items[0].state).toEqual(subItem2.state);
        });
      });
    });
  });
})();
<|endoftext|>