<commit_before>/**
 * ProArray
 */

'use strict';

require('natural-compare-lite');

function natcompCaseInsensitive(a, b) {
  return String.naturalCompare(a.toLowerCase(), b.toLowerCase());
}

function numericalCompare(a, b) {
  return a - b;
}

function numericalCompareReverse(a, b) {
  return b - a;
}

function retval(v) {
  return v;
}

/**
 * @class Array
 * @classdesc The native Array object.
 * @see {@link http://goo.gl/Ijd3ks|MDN JavaScript Array Reference}
 */

var properties = {
  /**
   * Creates an array of elements split into groups the length of `size`. If the array
   * can't be split evenly, the final chunk will be the remaining elements.
   *
   * @function Array#chunk
   * @param {number} [size=1] - The length of each chunk.
   * @returns {Array} An array containing the chunks.
   *
   * @example
   * [1, 2, 3, 4].chunk(2);
   * // -> [[1, 2], [3, 4]]
   *
   * [1, 2, 3, 4].chunk(3);
   * // -> [[1, 2, 3], [4]]
   */
  chunk: function(size) {
    size = size || 1;

    var numChunks = Math.ceil(this.length / size);
    var res = new Array(numChunks);

    for (var i = 0, index = 0; i < numChunks; i++) {
      res[i] = this.slice(index, index += size);
    }

    return res;
  },

  /**
   * Removes all elements from the array.
   *
   * @function Array#clear
   *
   * @example
   * var array = [1, 2, 3];
   * array.clear();
   * console.log(array);
   * // -> []
   */
  clear: function() {
    this.length = 0;
  },

  /**
   * Creates a shallow copy of the array.
   *
   * @function Array#clone
   * @returns {Array} A clone of the array.
   *
   * @example
   * var a = [1, 2, 3];
   * var b = a.clone();
   * console.log(b, b === a);
   * // -> [1, 2, 3] false
   */
  clone: function() {
    var len = this.length;
    var array = new Array(len);

    for (var i = 0; i < len; i++) {
      array[i] = this[i];
    }

    return array;
  },

  /**
   * Returns a new array with all falsey values removed. Falsey values
   * are `false`, `0`, `""`, `null`, `undefined`, and `NaN`.
   *
   * @function Array#compact
   * @returns {Array} A new array containing only the truthy values of the original array.
   *
   * @example
   * [0, 1, false, 2, '', 3].compact();
   * // -> [1, 2, 3]
   */
  compact: function() {
    return this.filter(retval);
  },

  /**
   * Returns a new array with all of the values of this array that are not in
   * any of the input arrays (performs a set difference).
   *
   * @function Array#difference
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array}
   *
   * @example
   * [1, 2, 3, 4, 5].difference([5, 2, 10]);
   * // -> [1, 3, 4]
   */
  difference: function() {
    var difference = [];

    next: for (var i = 0; i < this.length; i++) {
      var item = this[i];

      for (var j = 0; j < arguments.length; j++) {
        if (arguments[j].indexOf(item) >= 0) {
          continue next;
        }
      }

      // The item was not part of any of the input arrays so it can be added to the difference array
      difference.push(item);
    }

    return difference;
  },

  /**
   * @callback Array#each~eachCallback
   * @param {*} value - The current element being processed.
   * @param {number} index - The index of the current element being processed.
   * @param {Array} array - The array {@link Array#each|`.each()`} was called on.
   */
  /**
   * Invokes a callback function on each element in the array.
   *
   * A generic iterator method similar to [`.forEach()`](http://goo.gl/n6z5Jz) but with the following differences:
   *
   * 1. `this` always refers to the current element in the iteration (the `value` argument to the callback).
   * 2. Returning `false` in the callback will cancel the iteration (similar to a `break` statement).
   * 3. The array is returned to allow for function chaining.
   * 4. The callback __is__ invoked for indexes that have been deleted or elided unless `safeIteration` is `true`.
   *
   * @function Array#each
   * @param {Array#each~eachCallback} callback - A function to be executed on each element in the array.
   * @param {boolean} [safeIteration=false] - When `true`, the callback will not be invoked
   *     for indexes that have been deleted or elided (are undefined).
   * @returns {Array} `this`
   *
   * @example
   * ['a', 'b', 'c'].each(console.log.bind(console));
   * // -> 'a' 0 ['a', 'b', 'c']
   * // -> 'b' 1 ['a', 'b', 'c']
   * // -> 'c' 2 ['a', 'b', 'c']
   * // -> ['a', 'b', 'c']
   *
   * ['a', 'b', 'c'].each(function(value, index) {
   *   console.log(value);
   *   if (index === 1) return false;
   * });
   * // -> 'a'
   * // -> 'b'
   * // -> ['a', 'b', 'c']
   *
   * [[1, 2], [3, 4, 5]].each(Array.prototype.pop);
   * // -> [[1], [3, 4]]
   *
   * new Array(1).each(console.log.bind(console));
   * // -> undefined 0 ['a', 'b', 'c']
   * // -> [undefined]
   *
   * new Array(1).each(console.log.bind(console), true);
   * // -> [undefined]
   */
  each: function(callback, safeIteration) {
    var i = 0;

    if (safeIteration)
      while (i < this.length && (!(i in this) || callback.call(this[i], this[i], i, this) !== false)) i++;
    else
      while (i < this.length && callback.call(this[i], this[i], i++, this) !== false);

    return this;
  },

  /**
   * Determines if the arrays are equal by doing a shallow comparison of their elements using strict equality.
   *
   * __Note:__ The order of elements in the arrays __does__ matter. The elements must be found in the same order
   * for the arrays to be considered equal.
   *
   * @function Array#equals
   * @param {Array} array - An array to compare for equality.
   * @returns {boolean} `true` if the arrays are equal, `false` otherwise.
   *
   * @example
   * var array = [1, 2, 3];
   *
   * array.equals(array);
   * // -> true
   *
   * array.equals([1, 2, 3]);
   * // -> true
   *
   * array.equals([3, 2, 1]);
   * // -> false
   */
  equals: function(array) {
    if (array === this) {
      return true;
    }

    if (!array || array.length !== this.length) {
      return false;
    }

    for (var i = 0; i < array.length; i++) {
      if (array[i] !== this[i]) {
        return false;
      }
    }

    return true;
  },

  /**
   * Retrieve an element in the array.
   *
   * @function Array#get
   * @param {number} index - A zero-based integer indicating which element to retrieve.
   * @returns {*} The element at the specified index.
   *
   * @example
   * var array = [1, 2, 3];
   *
   * array.get(0);
   * // -> 1
   *
   * array.get(1);
   * // -> 2
   *
   * array.get(-1);
   * // -> 3
   *
   * array.get(-2);
   * // -> 2
   *
   * array.get(5);
   * // -> undefined
   */
  get: function(index) {
    return this[index < 0 ? index + this.length : index];
  },

  /**
   * Performs a set intersection on this array and the input array(s).
   *
   * @function Array#intersect
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array} An array that is the intersection of this array and the input array(s).
   *
   * @example
   * [1, 2, 3].intersect([2, 3, 4]);
   * // -> [2, 3]
   *
   * [1, 2, 3].intersect([101, 2, 50, 1], [2, 1]);
   * // -> [1, 2]
   */
  intersect: function() {
    var intersection = [];

    next: for (var i = 0; i < this.length; i++) {
      var item = this[i];

      // The current item can only be added if it is not already in the intersection
      if (intersection.indexOf(item) < 0) {
        // If the item is not in every input array, continue to the next item
        for (var j = 0; j < arguments.length; j++) {
          if (arguments[j].indexOf(item) < 0) {
            continue next;
          }
        }

        intersection.push(item);
      }
    }

    return intersection;
  },

  /**
   * Sorts an array in place using a natural string comparison algorithm and returns the array.
   *
   * @function Array#natsort
   * @param {boolean} [caseInsensitive=false] - Set this to `true` to ignore letter casing when sorting.
   * @returns {Array} The array.
   *
   * @example
   * var files = ['a.txt', 'a10.txt', 'a2.txt', 'a1.txt'];
   * files.natsort();
   * console.log(files);
   * // -> ['a.txt', 'a1.txt', 'a2.txt', 'a10.txt']
   */
  natsort: function(caseInsensitive) {
    return this.sort(caseInsensitive ? natcompCaseInsensitive : String.naturalCompare);
  },

  /**
   * Sorts an array in place using a numerical comparison algorithm
   * (sorts numbers from lowest to highest) and returns the array.
   *
   * @function Array#numsort
   * @returns {Array} The array.
   *
   * @example
   * var files = [10, 0, 2, 1];
   * files.numsort();
   * console.log(files);
   * // -> [0, 1, 2, 3]
   */
  numsort: function() {
    return this.sort(numericalCompare);
  },

  /**
   * Sorts an array in place using a reverse numerical comparison algorithm
   * (sorts numbers from highest to lowest) and returns the array.
   *
   * @function Array#rnumsort
   * @returns {Array} The array.
   *
   * @example
   * var files = [10, 0, 2, 1];
   * files.rnumsort();
   * console.log(files);
   * // -> [3, 2, 1, 0]
   */
  rnumsort: function() {
    return this.sort(numericalCompareReverse);
  },

  /**
   * Removes all occurrences of the passed in items from the array if they exist in the array.
   *
   * @function Array#remove
   * @param {...*} *items - Items to remove from the array.
   * @returns {Array} A reference to the array (so it's chainable).
   *
   * @example
   * var array = [1, 2, 3, 3, 4, 3, 5];
   *
   * array.remove(1);
   * // -> [2, 3, 3, 4, 3, 5]
   *
   * array.remove(3);
   * // -> [2, 4, 5]
   *
   * array.remove(2, 5);
   * // -> [4]
   */
  remove: function() {
    for (var i = 0; i < arguments.length; i++) {
      for (;;) {
        var remIndex = this.indexOf(arguments[i]);
        if (remIndex < 0) break;
        this.splice(remIndex, 1);
      }
    }

    return this;
  },

  /**
   * Returns an array containing every distinct element that is in either this array or the input array(s).
   *
   * @function Array#union
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array} An array that is the union of this array and the input array(s).
   *
   * @example
   * [1, 2, 3].union([2, 3, 4, 5]);
   * // -> [1, 2, 3, 4, 5]
   */
  union: function() {
    var union = this.unique();

    for (var i = 0; i < arguments.length; i++) {
      var array = arguments[i];
      for (var j = 0; j < array.length; j++) {
        if (union.indexOf(array[j]) < 0) {
          union.push(array[j]);
        }
      }
    }

    return union;
  },

  /**
   * Returns a duplicate-free clone of the array.
   *
   * @function Array#unique
   * @param {boolean} [isSorted=false] - If the input array's contents are sorted and this is set to `true`,
   *     a faster algorithm will be used to create the unique array.
   * @returns {Array}
   *
   * @example
   * // Unsorted
   * [4, 2, 3, 2, 1, 4].unique();
   * // -> [4, 2, 3, 1]
   *
   * // Sorted
   * [1, 2, 2, 3, 4, 4].unique();
   * // -> [1, 2, 3, 4]
   *
   * [1, 2, 2, 3, 4, 4].unique(true);
   * // -> [1, 2, 3, 4] (but faster than the previous example)
   */
  unique: function(isSorted) {
    var res = [];
    var len = this.length;

    if (!len) {
      return res;
    }

    res[0] = this[0];
    var i = 1;

    if (isSorted) {
      for (; i < len; i++) {
        if (this[i] !== this[i - 1]) {
          res.push(this[i]);
        }
      }
    } else {
      for (; i < len; i++) {
        if (res.indexOf(this[i]) < 0) {
          res.push(this[i]);
        }
      }
    }

    return res;
  },

  /**
   * Returns a copy of the current array without any elements from the input parameters.
   *
   * @function Array#without
   * @param {...*} *items - Items to leave out of the returned array.
   * @returns {Array}
   *
   * @example
   * [1, 2, 3, 4].without(2, 4);
   * // -> [1, 3]
   */
  without: function() {
    var array = [];

    next: for (var i = 0; i < this.length; i++) {
      for (var j = 0; j < arguments.length; j++) {
        if (arguments[j] === this[i]) {
          continue next;
        }
      }
      array.push(this[i]);
    }

    return array;
  }
};

// Format the properties to be defined with Object.defineProperties()
for (var propName in properties) {
  properties[propName] = {
    value: properties[propName],
    configurable: true,
    writable: true
  };
}

// Set aliases

/**
 * Alias of {@link Array#difference}.
 *
 * @function Array#diff
 * @see {@link Array#difference}
 */
properties.diff = properties.difference;

/**
 * Alias of {@link Array#unique}.
 *
 * @function Array#uniq
 * @see {@link Array#unique}
 */
properties.uniq = properties.unique;

// Define the properties on Array.prototype
Object.defineProperties(Array.prototype, properties);
<commit_msg>Optimize #compact()<commit_after>/**
 * ProArray
 */

'use strict';

require('natural-compare-lite');

function natcompCaseInsensitive(a, b) {
  return String.naturalCompare(a.toLowerCase(), b.toLowerCase());
}

function numericalCompare(a, b) {
  return a - b;
}

function numericalCompareReverse(a, b) {
  return b - a;
}

/**
 * @class Array
 * @classdesc The native Array object.
 * @see {@link http://goo.gl/Ijd3ks|MDN JavaScript Array Reference}
 */

var properties = {
  /**
   * Creates an array of elements split into groups the length of `size`. If the array
   * can't be split evenly, the final chunk will be the remaining elements.
   *
   * @function Array#chunk
   * @param {number} [size=1] - The length of each chunk.
   * @returns {Array} An array containing the chunks.
   *
   * @example
   * [1, 2, 3, 4].chunk(2);
   * // -> [[1, 2], [3, 4]]
   *
   * [1, 2, 3, 4].chunk(3);
   * // -> [[1, 2, 3], [4]]
   */
  chunk: function(size) {
    size = size || 1;

    var numChunks = Math.ceil(this.length / size);
    var res = new Array(numChunks);

    for (var i = 0, index = 0; i < numChunks; i++) {
      res[i] = this.slice(index, index += size);
    }

    return res;
  },

  /**
   * Removes all elements from the array.
   *
   * @function Array#clear
   *
   * @example
   * var array = [1, 2, 3];
   * array.clear();
   * console.log(array);
   * // -> []
   */
  clear: function() {
    this.length = 0;
  },

  /**
   * Creates a shallow copy of the array.
   *
   * @function Array#clone
   * @returns {Array} A clone of the array.
   *
   * @example
   * var a = [1, 2, 3];
   * var b = a.clone();
   * console.log(b, b === a);
   * // -> [1, 2, 3] false
   */
  clone: function() {
    var len = this.length;
    var array = new Array(len);

    for (var i = 0; i < len; i++) {
      array[i] = this[i];
    }

    return array;
  },

  /**
   * Returns a new array with all falsey values removed. Falsey values
   * are `false`, `0`, `""`, `null`, `undefined`, and `NaN`.
   *
   * @function Array#compact
   * @returns {Array} A new array containing only the truthy values of the original array.
   *
   * @example
   * [0, 1, false, 2, '', 3].compact();
   * // -> [1, 2, 3]
   */
  compact: function() {
    var res = [];

    for (var i = 0; i < this.length; i++) {
      if (this[i]) {
        res.push(this[i]);
      }
    }

    return res;
  },

  /**
   * Returns a new array with all of the values of this array that are not in
   * any of the input arrays (performs a set difference).
   *
   * @function Array#difference
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array}
   *
   * @example
   * [1, 2, 3, 4, 5].difference([5, 2, 10]);
   * // -> [1, 3, 4]
   */
  difference: function() {
    var difference = [];

    next: for (var i = 0; i < this.length; i++) {
      var item = this[i];

      for (var j = 0; j < arguments.length; j++) {
        if (arguments[j].indexOf(item) >= 0) {
          continue next;
        }
      }

      // The item was not part of any of the input arrays so it can be added to the difference array
      difference.push(item);
    }

    return difference;
  },

  /**
   * @callback Array#each~eachCallback
   * @param {*} value - The current element being processed.
   * @param {number} index - The index of the current element being processed.
   * @param {Array} array - The array {@link Array#each|`.each()`} was called on.
   */
  /**
   * Invokes a callback function on each element in the array.
   *
   * A generic iterator method similar to [`.forEach()`](http://goo.gl/n6z5Jz) but with the following differences:
   *
   * 1. `this` always refers to the current element in the iteration (the `value` argument to the callback).
   * 2. Returning `false` in the callback will cancel the iteration (similar to a `break` statement).
   * 3. The array is returned to allow for function chaining.
   * 4. The callback __is__ invoked for indexes that have been deleted or elided unless `safeIteration` is `true`.
   *
   * @function Array#each
   * @param {Array#each~eachCallback} callback - A function to be executed on each element in the array.
   * @param {boolean} [safeIteration=false] - When `true`, the callback will not be invoked
   *     for indexes that have been deleted or elided (are undefined).
   * @returns {Array} `this`
   *
   * @example
   * ['a', 'b', 'c'].each(console.log.bind(console));
   * // -> 'a' 0 ['a', 'b', 'c']
   * // -> 'b' 1 ['a', 'b', 'c']
   * // -> 'c' 2 ['a', 'b', 'c']
   * // -> ['a', 'b', 'c']
   *
   * ['a', 'b', 'c'].each(function(value, index) {
   *   console.log(value);
   *   if (index === 1) return false;
   * });
   * // -> 'a'
   * // -> 'b'
   * // -> ['a', 'b', 'c']
   *
   * [[1, 2], [3, 4, 5]].each(Array.prototype.pop);
   * // -> [[1], [3, 4]]
   *
   * new Array(1).each(console.log.bind(console));
   * // -> undefined 0 ['a', 'b', 'c']
   * // -> [undefined]
   *
   * new Array(1).each(console.log.bind(console), true);
   * // -> [undefined]
   */
  each: function(callback, safeIteration) {
    var i = 0;

    if (safeIteration)
      while (i < this.length && (!(i in this) || callback.call(this[i], this[i], i, this) !== false)) i++;
    else
      while (i < this.length && callback.call(this[i], this[i], i++, this) !== false);

    return this;
  },

  /**
   * Determines if the arrays are equal by doing a shallow comparison of their elements using strict equality.
   *
   * __Note:__ The order of elements in the arrays __does__ matter. The elements must be found in the same order
   * for the arrays to be considered equal.
   *
   * @function Array#equals
   * @param {Array} array - An array to compare for equality.
   * @returns {boolean} `true` if the arrays are equal, `false` otherwise.
   *
   * @example
   * var array = [1, 2, 3];
   *
   * array.equals(array);
   * // -> true
   *
   * array.equals([1, 2, 3]);
   * // -> true
   *
   * array.equals([3, 2, 1]);
   * // -> false
   */
  equals: function(array) {
    if (array === this) {
      return true;
    }

    if (!array || array.length !== this.length) {
      return false;
    }

    for (var i = 0; i < array.length; i++) {
      if (array[i] !== this[i]) {
        return false;
      }
    }

    return true;
  },

  /**
   * Retrieve an element in the array.
   *
   * @function Array#get
   * @param {number} index - A zero-based integer indicating which element to retrieve.
   * @returns {*} The element at the specified index.
   *
   * @example
   * var array = [1, 2, 3];
   *
   * array.get(0);
   * // -> 1
   *
   * array.get(1);
   * // -> 2
   *
   * array.get(-1);
   * // -> 3
   *
   * array.get(-2);
   * // -> 2
   *
   * array.get(5);
   * // -> undefined
   */
  get: function(index) {
    return this[index < 0 ? index + this.length : index];
  },

  /**
   * Performs a set intersection on this array and the input array(s).
   *
   * @function Array#intersect
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array} An array that is the intersection of this array and the input array(s).
   *
   * @example
   * [1, 2, 3].intersect([2, 3, 4]);
   * // -> [2, 3]
   *
   * [1, 2, 3].intersect([101, 2, 50, 1], [2, 1]);
   * // -> [1, 2]
   */
  intersect: function() {
    var intersection = [];

    next: for (var i = 0; i < this.length; i++) {
      var item = this[i];

      // The current item can only be added if it is not already in the intersection
      if (intersection.indexOf(item) < 0) {
        // If the item is not in every input array, continue to the next item
        for (var j = 0; j < arguments.length; j++) {
          if (arguments[j].indexOf(item) < 0) {
            continue next;
          }
        }

        intersection.push(item);
      }
    }

    return intersection;
  },

  /**
   * Sorts an array in place using a natural string comparison algorithm and returns the array.
   *
   * @function Array#natsort
   * @param {boolean} [caseInsensitive=false] - Set this to `true` to ignore letter casing when sorting.
   * @returns {Array} The array.
   *
   * @example
   * var files = ['a.txt', 'a10.txt', 'a2.txt', 'a1.txt'];
   * files.natsort();
   * console.log(files);
   * // -> ['a.txt', 'a1.txt', 'a2.txt', 'a10.txt']
   */
  natsort: function(caseInsensitive) {
    return this.sort(caseInsensitive ? natcompCaseInsensitive : String.naturalCompare);
  },

  /**
   * Sorts an array in place using a numerical comparison algorithm
   * (sorts numbers from lowest to highest) and returns the array.
   *
   * @function Array#numsort
   * @returns {Array} The array.
   *
   * @example
   * var files = [10, 0, 2, 1];
   * files.numsort();
   * console.log(files);
   * // -> [0, 1, 2, 3]
   */
  numsort: function() {
    return this.sort(numericalCompare);
  },

  /**
   * Sorts an array in place using a reverse numerical comparison algorithm
   * (sorts numbers from highest to lowest) and returns the array.
   *
   * @function Array#rnumsort
   * @returns {Array} The array.
   *
   * @example
   * var files = [10, 0, 2, 1];
   * files.rnumsort();
   * console.log(files);
   * // -> [3, 2, 1, 0]
   */
  rnumsort: function() {
    return this.sort(numericalCompareReverse);
  },

  /**
   * Removes all occurrences of the passed in items from the array if they exist in the array.
   *
   * @function Array#remove
   * @param {...*} *items - Items to remove from the array.
   * @returns {Array} A reference to the array (so it's chainable).
   *
   * @example
   * var array = [1, 2, 3, 3, 4, 3, 5];
   *
   * array.remove(1);
   * // -> [2, 3, 3, 4, 3, 5]
   *
   * array.remove(3);
   * // -> [2, 4, 5]
   *
   * array.remove(2, 5);
   * // -> [4]
   */
  remove: function() {
    for (var i = 0; i < arguments.length; i++) {
      for (;;) {
        var remIndex = this.indexOf(arguments[i]);
        if (remIndex < 0) break;
        this.splice(remIndex, 1);
      }
    }

    return this;
  },

  /**
   * Returns an array containing every distinct element that is in either this array or the input array(s).
   *
   * @function Array#union
   * @param {...Array} *arrays - A variable number of arrays.
   * @returns {Array} An array that is the union of this array and the input array(s).
   *
   * @example
   * [1, 2, 3].union([2, 3, 4, 5]);
   * // -> [1, 2, 3, 4, 5]
   */
  union: function() {
    var union = this.unique();

    for (var i = 0; i < arguments.length; i++) {
      var array = arguments[i];
      for (var j = 0; j < array.length; j++) {
        if (union.indexOf(array[j]) < 0) {
          union.push(array[j]);
        }
      }
    }

    return union;
  },

  /**
   * Returns a duplicate-free clone of the array.
   *
   * @function Array#unique
   * @param {boolean} [isSorted=false] - If the input array's contents are sorted and this is set to `true`,
   *     a faster algorithm will be used to create the unique array.
   * @returns {Array}
   *
   * @example
   * // Unsorted
   * [4, 2, 3, 2, 1, 4].unique();
   * // -> [4, 2, 3, 1]
   *
   * // Sorted
   * [1, 2, 2, 3, 4, 4].unique();
   * // -> [1, 2, 3, 4]
   *
   * [1, 2, 2, 3, 4, 4].unique(true);
   * // -> [1, 2, 3, 4] (but faster than the previous example)
   */
  unique: function(isSorted) {
    var res = [];
    var len = this.length;

    if (!len) {
      return res;
    }

    res[0] = this[0];
    var i = 1;

    if (isSorted) {
      for (; i < len; i++) {
        if (this[i] !== this[i - 1]) {
          res.push(this[i]);
        }
      }
    } else {
      for (; i < len; i++) {
        if (res.indexOf(this[i]) < 0) {
          res.push(this[i]);
        }
      }
    }

    return res;
  },

  /**
   * Returns a copy of the current array without any elements from the input parameters.
   *
   * @function Array#without
   * @param {...*} *items - Items to leave out of the returned array.
   * @returns {Array}
   *
   * @example
   * [1, 2, 3, 4].without(2, 4);
   * // -> [1, 3]
   */
  without: function() {
    var array = [];

    next: for (var i = 0; i < this.length; i++) {
      for (var j = 0; j < arguments.length; j++) {
        if (arguments[j] === this[i]) {
          continue next;
        }
      }
      array.push(this[i]);
    }

    return array;
  }
};

// Format the properties to be defined with Object.defineProperties()
for (var propName in properties) {
  properties[propName] = {
    value: properties[propName],
    configurable: true,
    writable: true
  };
}

// Set aliases

/**
 * Alias of {@link Array#difference}.
 *
 * @function Array#diff
 * @see {@link Array#difference}
 */
properties.diff = properties.difference;

/**
 * Alias of {@link Array#unique}.
 *
 * @function Array#uniq
 * @see {@link Array#unique}
 */
properties.uniq = properties.unique;

// Define the properties on Array.prototype
Object.defineProperties(Array.prototype, properties);
<|endoftext|>


<commit_before>describe('validators.datetime', function() {
  var datetime = validate.validators.datetime.bind(validate.validators.datetime)
    , XDate = validate.XDate
    , moment = validate.moment;

  afterEach(function() {
    delete validate.validators.datetime.notValid;
    delete validate.validators.datetime.tooEarly;
    delete validate.validators.datetime.tooLate;
    delete validate.validators.datetime.options;
    validate.XDate = XDate;
    validate.moment = moment;
  });

  it("allows empty values", function() {
    expect(datetime(null, {})).not.toBeDefined();
    expect(datetime(undefined, {})).not.toBeDefined();
    expect(datetime("", {})).not.toBeDefined();
    expect(datetime("  ", {})).not.toBeDefined();
  });

  describe("parse", function() {
    var parse = validate.validators.datetime.parse;

    beforeEach(function() {
      delete validate.XDate;
      delete validate.moment;
    });

    it("throws an error if neither XDate or moment.js is found", function() {
      expect(function() {
        parse("2014-09-02");
      }).toThrow();
    });

    function runParseTestsForValidStrings() {
        // 2013-10-25 00:00:00 UTC
        expect(parse("2013-10-26", {})).toEqual(1382745600000);

        // 1000-01-01 00:00:00 UTC
        expect(parse("1000-01-01", {})).toEqual(-30610224000000);

        // UTC
        expect(parse("2013-10-26T10:35:24", {})).toEqual(1382783724000);
        // PDT
        expect(parse("2013-10-26T10:35:24-0700", {})).toEqual(1382808924000);
    }

    function runNaNTests() {
      expect(parse("foobar", {})).toBeNaN();
    }

    describe("with XDate", function() {
      beforeEach(function() {
        validate.XDate = XDate;
      });

      it("returns the millis since epoch for valid strings", function() {
        runParseTestsForValidStrings();
      });

      it("returns NaN for invalid dates", function() {
        runNaNTests();
      });
    });

    describe("with moment.js", function() {
      beforeEach(function() {
        validate.moment = moment;
        spyOn(moment, "utc").and.callThrough();
      });

      it("returns the millis since epoch for valid strings", function() {
        runParseTestsForValidStrings();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("returns NaN for invalid dates", function() {
        runNaNTests();
        expect(moment.utc).toHaveBeenCalled();
      });
    });
  });

  describe("format", function() {
    var format = validate.validators.datetime.format;

    beforeEach(function() {
      delete validate.XDate;
      delete validate.moment;
    });

    it("throws and error if neither XDate or moment.js is found", function() {
      expect(function() {
        format(1382808924000, {});
      }).toThrow();
    });

    function runDatetimeTest() {
      var expected = "2013-10-26 17:35:24";
      expect(format(1382808924000, {})).toBe(expected);
    }

    function runDateTest() {
      var expected = "2013-10-26";
      expect(format(1382745600000, {dateOnly: true})).toBe(expected);
    }

    function runOverrideTest(dateFormat) {
      var expected = "10/26/13";
      expect(format(1382808924000, {dateFormat: dateFormat})).toBe(expected);
    }

    describe("with XDate", function() {
      beforeEach(function() {
        validate.XDate = XDate;
      });

      it("formats as ISO8601 in errors", function() {
        runDatetimeTest();
      });

      it("only includes the date part it dateOnly is set", function() {
        runDateTest();
      });

      it("allows you to override the format string", function() {
        runOverrideTest("MM/dd/yy");
      });
    });

    describe("with moment.js", function() {
      beforeEach(function() {
        validate.moment = moment;
        spyOn(moment, "utc").and.callThrough();
      });

      it("formats as ISO8601 in errors", function() {
        runDatetimeTest();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("only includes the date part it dateOnly is set", function() {
        runDateTest();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("allows you to override the format string", function() {
        runOverrideTest("MM/DD/YY");
        expect(moment.utc).toHaveBeenCalled();
      });
    });
  });

  it("allows valid dates", function() {
    expect(datetime("2013-10-26 13:47:00", {})).not.toBeDefined();
  });

  it("doesn't allow invalid dates", function() {
    var expected = "must be a valid date";
    expect(datetime("foobar", {})).toEqual(expected);
  });

  it("uses the parse function to validate dates", function() {
    var spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(NaN)
      , options = {foo: "bar"};
    expect(datetime("2013-06-27", options)).toBeDefined();
    expect(spy).toHaveBeenCalledWith("2013-06-27", options);
  });

  it("doesn't allow h, m or s when dateOnly is true", function() {
    var expected = "must be a valid date"
      , opts = {dateOnly: true};
    expect(datetime("2013-10-26 13:47:00", opts)).toEqual(expected);
    expect(datetime("2013-10-26", opts)).not.toBeDefined();
  });

  it("returns the message if specified for invalid dates", function() {
    validate.validators.datetime.notValid = "notValid";
    expect(datetime("foobar", {})).toEqual("notValid");

    var opts = {message: "my other message"};
    expect(datetime("foobar", opts)).toEqual("my other message");
  });

  describe("earliest", function() {
    it("doesn't allow earlier dates", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 23:59:59"
        , expected = ["must be no earlier than 2013-10-26 00:00:00"];

      expect(datetime(value, options)).toEqual(expected);
    });

    it("allows earlier or equal dates", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
      value = "2013-10-27 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
    });

    it("formats the error using the format function", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 00:00:00"
        , spy = spyOn(validate.validators.datetime, 'format').and.returnValue("foobar")
        , expected = ["must be no earlier than foobar"];
      expect(datetime(value, options)).toEqual(expected);
    });

    it("parses the earliest value using the parse function", function() {
      var options = {earliest: 'foobar'}
        , value = XDate.today()
        , spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(value);
      datetime(value, options);
      expect(spy).toHaveBeenCalledWith('foobar', options);
    });

    it("uses the default message if available", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 23:59:59";

      validate.validators.datetime.tooEarly = "default message";
      expect(datetime(value, options)).toEqual(["default message"]);

      options.message = "overridden";
      expect(datetime(value, options)).toEqual("overridden");
    });
  });

  describe("latest", function() {
    it("doesn't allow later dates", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:01"
        , expected = ["must be no later than 2013-10-26 00:00:00"];

      expect(datetime(value, options)).toEqual(expected);
    });

    it("allows later or equal dates", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
      value = "2013-10-25 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
    });

    it("formats the error using the format function", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-27 00:00:00"
        , spy = spyOn(validate.validators.datetime, 'format').and.returnValue("foobar")
        , expected = ["must be no later than foobar"];
      expect(datetime(value, options)).toEqual(expected);
    });

    it("parses the latest value using the parse function", function() {
      var options = {latest: 'foobar'}
        , value = XDate.today()
        , spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(value);
      datetime(value, options);
      expect(spy).toHaveBeenCalledWith('foobar', options);
    });

    it("uses the default message if available", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:01";

      validate.validators.datetime.tooLate = "default message";
      expect(datetime(value, options)).toEqual(["default message"]);

      options.message = "overridden";
      expect(datetime(value, options)).toEqual("overridden");
    });
  });

  it("can return multiple errors", function() {
    var options = {
          earliest: '2013-10-26 00:00:00',
          latest: '2013-10-24 00:00:00'
        }
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toHaveLength(2);
  });

  it("returns the user defined message only once", function() {
    var options = {
          earliest: '2013-10-26 00:00:00',
          latest: '2013-10-24 00:00:00',
          message: 'foobar'
        }
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toEqual('foobar');
  });

  it("supports default options", function() {
    validate.validators.datetime.options =
      {message: "barfoo", earliest: "2013-10-26 00:00:00"};
    var options = {message: 'foobar'}
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toEqual('foobar');
    expect(datetime(value, {})).toEqual('barfoo');
    expect(validate.validators.datetime.options)
      .toEqual({message: "barfoo", earliest: "2013-10-26 00:00:00"});
    expect(options).toEqual({message: "foobar"});
  });
});

describe('validators.date', function() {
  it("calls the datetime validator with dateOnly set to true", function() {
    var errors = ["my error"]
      , value = "my value"
      , options = {foo: "bar"}
      , spy = spyOn(validate.validators, 'datetime').and.returnValue(errors);
    expect(validate.validators.date(value, options)).toBe(errors);
    expect(spy).toHaveBeenCalledWith(value, {foo: "bar", dateOnly: true});
  });

  it("doesn't modify the options argument", function() {
    var options = {}
      , spy = spyOn(validate.validators, 'datetime');
    validate.validators.date("value", options);
    expect(options).toEqual({});
  });

  it("calls the datetime validator with the right context", function() {
    spyOn(validate.validators, 'datetime');
    validate.validators.date("foo", {});
    expect(validate.validators.datetime).toHaveBeenCalledWithContext(validate.validators.datetime);
  });
});
<commit_msg>Ensure JS dates works with the datetime validator<commit_after>describe('validators.datetime', function() {
  var datetime = validate.validators.datetime.bind(validate.validators.datetime)
    , XDate = validate.XDate
    , moment = validate.moment;

  afterEach(function() {
    delete validate.validators.datetime.notValid;
    delete validate.validators.datetime.tooEarly;
    delete validate.validators.datetime.tooLate;
    delete validate.validators.datetime.options;
    validate.XDate = XDate;
    validate.moment = moment;
  });

  it("allows empty values", function() {
    expect(datetime(null, {})).not.toBeDefined();
    expect(datetime(undefined, {})).not.toBeDefined();
    expect(datetime("", {})).not.toBeDefined();
    expect(datetime("  ", {})).not.toBeDefined();
  });

  describe("parse", function() {
    var parse = validate.validators.datetime.parse;

    beforeEach(function() {
      delete validate.XDate;
      delete validate.moment;
    });

    it("throws an error if neither XDate or moment.js is found", function() {
      expect(function() {
        parse("2014-09-02");
      }).toThrow();
    });

    function runParseTestsForValidDates() {
        // 2013-10-25 00:00:00 UTC
        expect(parse("2013-10-26", {})).toEqual(1382745600000);

        // 1000-01-01 00:00:00 UTC
        expect(parse("1000-01-01", {})).toEqual(-30610224000000);

        // UTC
        expect(parse("2013-10-26T10:35:24", {})).toEqual(1382783724000);
        // PDT
        expect(parse("2013-10-26T10:35:24-0700", {})).toEqual(1382808924000);

        // PDT
        var date = new Date("2013-10-26T10:35:24-0700");
        expect(parse(date, {})).toEqual(date.getTime());
    }

    function runNaNTests() {
      expect(parse("foobar", {})).toBeNaN();
    }

    describe("with XDate", function() {
      beforeEach(function() {
        validate.XDate = XDate;
      });

      it("returns the millis since epoch for valid strings", function() {
        runParseTestsForValidDates();
      });

      it("returns NaN for invalid dates", function() {
        runNaNTests();
      });
    });

    describe("with moment.js", function() {
      beforeEach(function() {
        validate.moment = moment;
        spyOn(moment, "utc").and.callThrough();
      });

      it("returns the millis since epoch for valid strings", function() {
        runParseTestsForValidDates();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("returns NaN for invalid dates", function() {
        runNaNTests();
        expect(moment.utc).toHaveBeenCalled();
      });
    });
  });

  describe("format", function() {
    var format = validate.validators.datetime.format;

    beforeEach(function() {
      delete validate.XDate;
      delete validate.moment;
    });

    it("throws and error if neither XDate or moment.js is found", function() {
      expect(function() {
        format(1382808924000, {});
      }).toThrow();
    });

    function runDatetimeTest() {
      var expected = "2013-10-26 17:35:24";
      expect(format(1382808924000, {})).toBe(expected);
    }

    function runDateTest() {
      var expected = "2013-10-26";
      expect(format(1382745600000, {dateOnly: true})).toBe(expected);
    }

    function runOverrideTest(dateFormat) {
      var expected = "10/26/13";
      expect(format(1382808924000, {dateFormat: dateFormat})).toBe(expected);
    }

    describe("with XDate", function() {
      beforeEach(function() {
        validate.XDate = XDate;
      });

      it("formats as ISO8601 in errors", function() {
        runDatetimeTest();
      });

      it("only includes the date part it dateOnly is set", function() {
        runDateTest();
      });

      it("allows you to override the format string", function() {
        runOverrideTest("MM/dd/yy");
      });
    });

    describe("with moment.js", function() {
      beforeEach(function() {
        validate.moment = moment;
        spyOn(moment, "utc").and.callThrough();
      });

      it("formats as ISO8601 in errors", function() {
        runDatetimeTest();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("only includes the date part it dateOnly is set", function() {
        runDateTest();
        expect(moment.utc).toHaveBeenCalled();
      });

      it("allows you to override the format string", function() {
        runOverrideTest("MM/DD/YY");
        expect(moment.utc).toHaveBeenCalled();
      });
    });
  });

  it("allows valid dates", function() {
    expect(datetime("2013-10-26 13:47:00", {})).not.toBeDefined();
  });

  it("allows date objects", function() {
    expect(datetime(new Date(), {})).not.toBeDefined();
  });

  it("doesn't allow invalid dates", function() {
    var expected = "must be a valid date";
    expect(datetime("foobar", {})).toEqual(expected);
  });

  it("uses the parse function to validate dates", function() {
    var spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(NaN)
      , options = {foo: "bar"};
    expect(datetime("2013-06-27", options)).toBeDefined();
    expect(spy).toHaveBeenCalledWith("2013-06-27", options);
  });

  it("doesn't allow h, m or s when dateOnly is true", function() {
    var expected = "must be a valid date"
      , opts = {dateOnly: true};
    expect(datetime("2013-10-26 13:47:00", opts)).toEqual(expected);
    expect(datetime("2013-10-26", opts)).not.toBeDefined();
  });

  it("returns the message if specified for invalid dates", function() {
    validate.validators.datetime.notValid = "notValid";
    expect(datetime("foobar", {})).toEqual("notValid");

    var opts = {message: "my other message"};
    expect(datetime("foobar", opts)).toEqual("my other message");
  });

  describe("earliest", function() {
    it("doesn't allow earlier dates", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 23:59:59"
        , expected = ["must be no earlier than 2013-10-26 00:00:00"];

      expect(datetime(value, options)).toEqual(expected);
    });

    it("allows earlier or equal dates", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
      value = "2013-10-27 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
    });

    it("formats the error using the format function", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 00:00:00"
        , spy = spyOn(validate.validators.datetime, 'format').and.returnValue("foobar")
        , expected = ["must be no earlier than foobar"];
      expect(datetime(value, options)).toEqual(expected);
    });

    it("parses the earliest value using the parse function", function() {
      var options = {earliest: 'foobar'}
        , value = XDate.today()
        , spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(value);
      datetime(value, options);
      expect(spy).toHaveBeenCalledWith('foobar', options);
    });

    it("uses the default message if available", function() {
      var options = {earliest: '2013-10-26 00:00:00'}
        , value = "2013-10-25 23:59:59";

      validate.validators.datetime.tooEarly = "default message";
      expect(datetime(value, options)).toEqual(["default message"]);

      options.message = "overridden";
      expect(datetime(value, options)).toEqual("overridden");
    });
  });

  describe("latest", function() {
    it("doesn't allow later dates", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:01"
        , expected = ["must be no later than 2013-10-26 00:00:00"];

      expect(datetime(value, options)).toEqual(expected);
    });

    it("allows later or equal dates", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
      value = "2013-10-25 00:00:00";
      expect(datetime(value, options)).not.toBeDefined();
    });

    it("formats the error using the format function", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-27 00:00:00"
        , spy = spyOn(validate.validators.datetime, 'format').and.returnValue("foobar")
        , expected = ["must be no later than foobar"];
      expect(datetime(value, options)).toEqual(expected);
    });

    it("parses the latest value using the parse function", function() {
      var options = {latest: 'foobar'}
        , value = XDate.today()
        , spy = spyOn(validate.validators.datetime, 'parse').and.returnValue(value);
      datetime(value, options);
      expect(spy).toHaveBeenCalledWith('foobar', options);
    });

    it("uses the default message if available", function() {
      var options = {latest: '2013-10-26 00:00:00'}
        , value = "2013-10-26 00:00:01";

      validate.validators.datetime.tooLate = "default message";
      expect(datetime(value, options)).toEqual(["default message"]);

      options.message = "overridden";
      expect(datetime(value, options)).toEqual("overridden");
    });
  });

  it("can return multiple errors", function() {
    var options = {
          earliest: '2013-10-26 00:00:00',
          latest: '2013-10-24 00:00:00'
        }
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toHaveLength(2);
  });

  it("returns the user defined message only once", function() {
    var options = {
          earliest: '2013-10-26 00:00:00',
          latest: '2013-10-24 00:00:00',
          message: 'foobar'
        }
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toEqual('foobar');
  });

  it("supports default options", function() {
    validate.validators.datetime.options =
      {message: "barfoo", earliest: "2013-10-26 00:00:00"};
    var options = {message: 'foobar'}
      , value = "2013-10-25 00:00:00";
    expect(datetime(value, options)).toEqual('foobar');
    expect(datetime(value, {})).toEqual('barfoo');
    expect(validate.validators.datetime.options)
      .toEqual({message: "barfoo", earliest: "2013-10-26 00:00:00"});
    expect(options).toEqual({message: "foobar"});
  });
});

describe('validators.date', function() {
  it("calls the datetime validator with dateOnly set to true", function() {
    var errors = ["my error"]
      , value = "my value"
      , options = {foo: "bar"}
      , spy = spyOn(validate.validators, 'datetime').and.returnValue(errors);
    expect(validate.validators.date(value, options)).toBe(errors);
    expect(spy).toHaveBeenCalledWith(value, {foo: "bar", dateOnly: true});
  });

  it("doesn't modify the options argument", function() {
    var options = {}
      , spy = spyOn(validate.validators, 'datetime');
    validate.validators.date("value", options);
    expect(options).toEqual({});
  });

  it("calls the datetime validator with the right context", function() {
    spyOn(validate.validators, 'datetime');
    validate.validators.date("foo", {});
    expect(validate.validators.datetime).toHaveBeenCalledWithContext(validate.validators.datetime);
  });
});
<|endoftext|>


<commit_before>var Backbone = require('backbone');
var ConfigModel = require('builder/data/config-model');
var AnalysisDefinitionNodesCollection = require('builder/data/analysis-definition-nodes-collection');
var TablesCollection = require('builder/data/tables-collection');
var TableModel = require('builder/data/table-model');
var AnalysisSourceOptionsModel = require('builder/editor/layers/layer-content-views/analyses/analysis-source-options-model');

describe('builder/editor/layers/layer-content-views/analyses/analysis-source-options-model', function () {
  beforeEach(function () {
    var configModel = new ConfigModel({
      base_url: '/u/pepe'
    });

    spyOn(TableModel.prototype, 'fetch');

    this.analysisDefinitionNodesCollection = new AnalysisDefinitionNodesCollection(null, {
      configModel: configModel,
      userModel: {}
    });

    this.analysisDefinitionNodesCollection.add({
      id: 'a0',
      type: 'source',
      params: {
        table_name: 'hello',
        query: 'SELECT * FROM first'
      }
    });

    var layerModel = new Backbone.Model({
      name: 'Layer Name',
      table_name: 'table_name',
      source: 'a0',
      color: '#000'
    });
    layerModel.getName = function () { return this.get('name'); };
    layerModel.getTableName = function () { return this.get('table_name'); };
    this.layerDefinitionsCollection = new Backbone.Collection(layerModel);
    this.layerDefinitionsCollection.findOwnerOfAnalysisNode = function (node) {
      return layerModel;
    };

    this.tablesCollection = new TablesCollection(null, {
      configModel: configModel
    });

    this.model = new AnalysisSourceOptionsModel(null, {
      analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
      layerDefinitionsCollection: this.layerDefinitionsCollection,
      tablesCollection: this.tablesCollection
    });
  });

  it('should not have any select options just yet', function () {
    expect(this.model.getSelectOptions('point')).toEqual([]);
    expect(this.model.getSelectOptions('polygon')).toEqual([]);
  });

  describe('when analysis are fetching but there are nodes', function () {
    beforeEach(function () {
      var nodes = [{
        label: 'a0',
        simpleGeometryType: 'polygon',
        val: 'a0'
      }];

      this.model.set({
        nodes_options: nodes,
        fetching: true
      });
    });

    it('should populate nodes from already fetched nodes', function () {
      expect(this.model.getSelectOptions('polygon')).toEqual([
        jasmine.objectContaining({
          val: 'a0',
          label: 'a0',
          type: 'node'
        })
      ]);
    });
  });

  describe('when analysis are already fetched', function () {
    beforeEach(function () {
      spyOn(this.tablesCollection, 'fetch');
      spyOn(this.analysisDefinitionNodesCollection.first().queryGeometryModel, 'fetch');
      this.analysisDefinitionNodesCollection.first().queryGeometryModel.set({
        simple_geom: 'polygon',
        status: 'fetched'
      });

      this.model.fetch();

      this.tablesCollection.trigger('sync');
      expect(this.model.get('fetching')).toBe(false);
    });

    it('should populate nodes from already fetched nodes', function () {
      expect(this.model.getSelectOptions('polygon')).toEqual([
        jasmine.objectContaining({
          val: 'a0',
          label: 'a0',
          type: 'node'
        })
      ]);
    });
  });

  describe('when analysis nodes are fetched', function () {
    beforeEach(function () {
      spyOn(this.tablesCollection, 'fetch');
      spyOn(this.analysisDefinitionNodesCollection.first().queryGeometryModel, 'fetch');
      this.model.fetch();
    });

    it('should fetch tables reducing the data to be retrieved', function () {
      expect(this.tablesCollection.fetch.calls.first().args).toContain(
        {
          data: Object({
            show_liked: false,
            show_stats: false,
            show_table_size_and_row_count: false,
            show_permission: false,
            show_synchronization: false,
            show_uses_builder_features: false,
            load_totals: false,
            per_page: 1000
          })
        }
      );
    });

    it('should be in fetching state', function () {
      expect(this.model.get('fetching')).toBe(true);
    });

    describe('when all items are fetched successfully', function () {
      beforeEach(function () {
        this.tablesCollection.add({
          name: 'table_with_points',
          geometry_types: ['ST_POINT']
        });
        this.tablesCollection.trigger('sync');
        expect(this.model.get('fetching')).toBe(true);

        this.analysisDefinitionNodesCollection.first().queryGeometryModel.set({
          simple_geom: 'polygon',
          status: 'fetched'
        });
      });

      it('should not be fetching anymore', function () {
        expect(this.model.get('fetching')).toBe(false);
      });

      it('should have items matching geometry type', function () {
        expect(this.model.getSelectOptions('polygon')).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          })
        ]);

        expect(this.model.getSelectOptions('point')).toEqual([
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });

      it('should have items matching multiple accepted geometries', function () {
        expect(this.model.getSelectOptions(['polygon', 'point'])).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          }),
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });

      it('should have items matching a wildcard geometry', function () {
        expect(this.model.getSelectOptions(['*'])).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          }),
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });
    });
  });
});
<commit_msg>Change spec<commit_after>var Backbone = require('backbone');
var ConfigModel = require('builder/data/config-model');
var AnalysisDefinitionNodesCollection = require('builder/data/analysis-definition-nodes-collection');
var TablesCollection = require('builder/data/tables-collection');
var TableModel = require('builder/data/table-model');
var AnalysisSourceOptionsModel = require('builder/editor/layers/layer-content-views/analyses/analysis-source-options-model');

describe('builder/editor/layers/layer-content-views/analyses/analysis-source-options-model', function () {
  beforeEach(function () {
    var configModel = new ConfigModel({
      base_url: '/u/pepe'
    });

    spyOn(TableModel.prototype, 'fetch');

    this.analysisDefinitionNodesCollection = new AnalysisDefinitionNodesCollection(null, {
      configModel: configModel,
      userModel: {}
    });

    this.analysisDefinitionNodesCollection.add({
      id: 'a0',
      type: 'source',
      params: {
        table_name: 'hello',
        query: 'SELECT * FROM first'
      }
    });

    var layerModel = new Backbone.Model({
      name: 'Layer Name',
      table_name: 'table_name',
      source: 'a0',
      color: '#000'
    });
    layerModel.getName = function () { return this.get('name'); };
    layerModel.getTableName = function () { return this.get('table_name'); };
    this.layerDefinitionsCollection = new Backbone.Collection(layerModel);
    this.layerDefinitionsCollection.findOwnerOfAnalysisNode = function (node) {
      return layerModel;
    };

    this.tablesCollection = new TablesCollection(null, {
      configModel: configModel
    });

    this.model = new AnalysisSourceOptionsModel(null, {
      analysisDefinitionNodesCollection: this.analysisDefinitionNodesCollection,
      layerDefinitionsCollection: this.layerDefinitionsCollection,
      tablesCollection: this.tablesCollection
    });
  });

  it('should not have any select options just yet', function () {
    expect(this.model.getSelectOptions('point')).toEqual([]);
    expect(this.model.getSelectOptions('polygon')).toEqual([]);
  });

  describe('when analysis are fetching but there are nodes', function () {
    beforeEach(function () {
      var nodes = [{
        label: 'a0',
        simpleGeometryType: 'polygon',
        val: 'a0'
      }];

      this.model.set({
        nodes_options: nodes,
        fetching: true
      });
    });

    it('should populate nodes from already fetched nodes', function () {
      expect(this.model.getSelectOptions('polygon')).toEqual([
        jasmine.objectContaining({
          val: 'a0',
          label: 'a0',
          type: 'node'
        })
      ]);
    });
  });

  describe('when analysis are already fetched', function () {
    beforeEach(function () {
      spyOn(this.tablesCollection, 'fetch');
      spyOn(this.analysisDefinitionNodesCollection.first().queryGeometryModel, 'fetch');
      this.analysisDefinitionNodesCollection.first().queryGeometryModel.set({
        simple_geom: 'polygon',
        status: 'fetched'
      });

      this.model.fetch();

      this.tablesCollection.trigger('sync');
      expect(this.model.get('fetching')).toBe(false);
    });

    it('should populate nodes from already fetched nodes', function () {
      expect(this.model.getSelectOptions('polygon')).toEqual([
        jasmine.objectContaining({
          val: 'a0',
          label: 'a0',
          type: 'node'
        })
      ]);
    });
  });

  describe('when analysis nodes are fetched', function () {
    beforeEach(function () {
      spyOn(this.tablesCollection, 'fetch');
      spyOn(this.analysisDefinitionNodesCollection.first().queryGeometryModel, 'fetch');
      this.model.fetch();
    });

    it('should fetch tables reducing the data to be retrieved', function () {
      var data = this.tablesCollection.fetch.calls.first().args.data;

      expect(data.load_totals).toBeDefined();
      expect(data.per_page).toBeDefined();
      expect(data.show_liked).toBeDefined();
      expect(data.show_permission).toBeDefined();
      expect(data.show_stats).toBeDefined();
      expect(data.show_synchronization).toBeDefined();
      expect(data.show_table_size_and_row_count).toBeDefined();
      expect(data.show_table).toBeDefined();
      expect(data.show_uses_builder_features).toBeDefined();
    });

    it('should be in fetching state', function () {
      expect(this.model.get('fetching')).toBe(true);
    });

    describe('when all items are fetched successfully', function () {
      beforeEach(function () {
        this.tablesCollection.add({
          name: 'table_with_points',
          geometry_types: ['ST_POINT']
        });
        this.tablesCollection.trigger('sync');
        expect(this.model.get('fetching')).toBe(true);

        this.analysisDefinitionNodesCollection.first().queryGeometryModel.set({
          simple_geom: 'polygon',
          status: 'fetched'
        });
      });

      it('should not be fetching anymore', function () {
        expect(this.model.get('fetching')).toBe(false);
      });

      it('should have items matching geometry type', function () {
        expect(this.model.getSelectOptions('polygon')).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          })
        ]);

        expect(this.model.getSelectOptions('point')).toEqual([
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });

      it('should have items matching multiple accepted geometries', function () {
        expect(this.model.getSelectOptions(['polygon', 'point'])).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          }),
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });

      it('should have items matching a wildcard geometry', function () {
        expect(this.model.getSelectOptions(['*'])).toEqual([
          jasmine.objectContaining({
            val: 'a0',
            label: 'a0',
            type: 'node'
          }),
          jasmine.objectContaining({
            val: 'table_with_points',
            label: 'table_with_points',
            type: 'dataset'
          })
        ]);
      });
    });
  });
});
<|endoftext|>


<commit_before>

<commit_msg>Removes empty file<commit_after><|endoftext|>


<commit_before>var require = patchRequire(require);

exports.init = phantomXHRInit;
exports.fake = fake;
exports.requests = getAllRequests;
exports.completed = allRequestsCompleted;

var page;

function phantomXHRInit(initPage, options){
	var fs = require('fs');

	var inject = false;

	options = options || {};

	if(initPage.injectJs){

		initPage.evaluate(function(){
			// Shim these constructors to make progress events work in PhantomJS
    		window.ProgressEvent = function(){};
    		window.CustomEvent = function(){};
		});

		inject = initPage.injectJs(options.libraryRoot ? (fs.absolute(options.libraryRoot) + 'sinon.js') : './node_modules/phantomxhr/sinon.js');

		initPage.evaluate(function(){

			// A naive implementation for simulating upload events
		    function FakeEvent(type, bubbles, cancelable, target) {
		        this.initEvent(type, bubbles, cancelable, target);
		    };

			FakeEvent.prototype = {
				initEvent: function(type, event, cancelable, target) {
					var key;

					this.type = type;
					this.bubbles = event;
					this.lengthComputable = true;
					this.isTrusted = true;
					this.cancelable = cancelable;
					this.target = target;
					this.loaded = 0;
					this.total = 0;

					for ( key in event ){
						this[key] = event[key];
					}
				},
				stopPropagation: function () {},
				stopImmediatePropagation: function () {},
				preventDefault: function () {
					this.defaultPrevented = true;
				}
			};

    		window.ProgressEvent = FakeEvent;
    		window.CustomEvent = FakeEvent;
		});

	}

	if(inject){
		page = initPage;
		setup(options);
	} else {
		console.log("[PhantomXHR] Can't find sinon.js");
	}
}

function setup(options){
	page.evaluate(function (options) {

		if (!window._ajaxmock_) {
			window._ajaxmock_ = {
				matches: [],
				requests_created: 0,
				requests_completed: 0,
				requests: {},
				call: {},
				fake: function (match) {

					function S4() {
						return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
					}

					function makeGuid() {
						return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
					}

					var urlIsString = match.url.indexOf('REGEXP') === -1;
					var guid = makeGuid();

					match.guid = guid;
					match.url = match.url.replace('REGEXP', '');

					console.log('[PhantomXHR] Match added [' + (match.method || 'All REST verbs') + "] : " + match.url + "'");

					match.requests = [];
					match.responses = [];
					match.respondMethods = [];

					window._ajaxmock_.call[guid] = match;

					window._ajaxmock_.matches.push(function (method, url) {

						if (!url) {
							return false;
						}

						var urlMatch = urlIsString ? url.indexOf(match.url) !== -1 : new RegExp(match.url).test(url);
						var methodMatch = (typeof match.method === 'undefined') ? true : match.method.toLowerCase() === method.toLowerCase();

						if (urlMatch && methodMatch) {
							return match;
						} else {
							return false;
						}
					});

					return guid;
				},
				init: function () {
					var _xhr = window.sinon.useFakeXMLHttpRequest();

					// overrideMimeType is not mocked see this issue
					// https://github.com/cjohansen/Sinon.JS/issues/559
					window.sinon.FakeXMLHttpRequest.prototype.overrideMimeType = function() {return;}

					// we backup _xhr object
					window.backup_xhr = _xhr;

					// If need create real XHR (by default false)
					if (options.allowRealRequests) {
						_xhr.useFilters = true;
						_xhr.addFilter(function(method, url) {
							var anyMatches = false;
							window._ajaxmock_.matches.reverse().forEach(function (func) {
								anyMatches = anyMatches || func(method, url);
							});
							return !anyMatches;
						});
					}

					_xhr.upload = document.createElement('div');

					_xhr.onCreate = function (request) {

						window._ajaxmock_.requests_created++;
						setTimeout(function () {
							var anyMatches = false;
							var requests = window._ajaxmock_.requests;

							if (!request.url) {
								console.log('[PhantomXHR] XHR has been initialised but not opened.');
								return;
							} // this shouldn't happen, but sometimes does
							// store the request for later matching
							if (requests[request.method.toLowerCase() + request.url]) {
								requests[request.method.toLowerCase() + request.url].count++;
							} else {
								requests[request.method.toLowerCase() + request.url] = {
									data: request,
									count: 1
								};
							}

							window._ajaxmock_.matches.slice(0).reverse().forEach(function (func) {
								anyMatches = anyMatches || func(request.method, request.url);
							});

							if (anyMatches) {
								respond(request, anyMatches);
							} else {
								console.log('[PhantomXHR] did not respond to ' + request.method + ' ' + request.url);
								window._ajaxmock_.requests_completed++;
							}
						}, 100);
					};
				}
			};
			window._ajaxmock_.init();
		}

		function respond(request, response) {

			if(!window._ajaxmock_){
				console.log('[PhantomXHR] could not respond, window._ajaxmock_ does not exist.');
				return;
			}

			var call = window._ajaxmock_.call;
			var callForThisMatch;
			var responseForThisMatch;
			var status;
			var body;

			console.log('[PhantomXHR] recieved request for ' + request.method + ": " + request.url + "'");

			callForThisMatch = call[response.guid];
			callForThisMatch.requests.push(request);

			responseForThisMatch = callForThisMatch.responses[callForThisMatch.requests.length];

			if (responseForThisMatch) {
				status = responseForThisMatch.status;
				body = responseForThisMatch.responseBody;
				console.log('[PhantomXHR] with status: ' + responseForThisMatch.status);
			}

			console.log('[PhantomXHR] with status: ' + response.status);

			if(response.holdResponse){

				callForThisMatch.respondMethods.push(function(responseOverride){
					responseOverride = responseOverride || response;

					console.log('[PhantomXHR] Responding to postponed ' + request.method + ": " + request.url + "'");

					request.respond(
						status || responseOverride.status || 200, responseOverride.headers || {
							"Content-Type": "application/json"
						},
						body || responseOverride.responseBody || ''
					);

					window._ajaxmock_.requests_completed++;
				});

			} else {
				console.log('[PhantomXHR] Responding to ' + request.method + ": " + request.url + "'");

				console.log('[PhantomXHR] status ', status , response.status , 200);

				request.respond(
					status || response.status || 200, response.headers || {
						"Content-Type": "application/json"
					},
					body || response.responseBody || ''
				);

				window._ajaxmock_.requests_completed++;
			}

		}
	}, options);
}

function fake(options) {
	var url = !! options.url.source ? 'REGEXP' + options.url.source : options.url;

	if(typeof(options.responseBody) === "object"){
		options.responseBody = JSON.stringify(options.responseBody);
	}

	var guid = page.evaluate(function (url, method, responseBody, status, headers, holdResponse) {
		if (window._ajaxmock_ && url) {

			if (responseBody && headers && headers["Content-Type"] && headers["Content-Type"] === "application/json") {
				try {
					JSON.parse(responseBody);
				} catch (e) {
					return 'JSON';
				}
			}

			return window._ajaxmock_.fake({
				url: url,
				method: method,
				responseBody: responseBody,
				status: status,
				headers: headers,
				holdResponse: holdResponse
			});
		}
	}, url, options.method, options.responseBody, options.status, options.headers, !!options.holdResponse);

	if (guid === 'JSON') {
		console.log('[PhantomXHR] JSON was invalid : ' + options.method + ' : ' + url);
	}

	return {
		count: function () {
			var c = page.evaluate(function (guid) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				return window._ajaxmock_.call[guid].requests.length;
			}, guid);

			if(typeof c === 'undefined'){
				console.log('[PhantomXHR] Could not get count');
			}

			return c;
		},

		nthRequest: function (index) {
			var r = page.evaluate(function (guid, index) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				var request = window._ajaxmock_.call[guid].requests[index - 1];
				return request.requestBody;
			}, guid, index);

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not get request');
			}

			return r;
		},

		nthRequestOrNull: function (index) {
			var r = page.evaluate(function (guid, index) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				var request = window._ajaxmock_.call[guid].requests[index - 1];
				if (!request) { return null; }
				return request;
			}, guid, index);

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not get request');
			}

			return r;
		},

		last: function () {
			var last = page.evaluate(function (guid) {
				return window._ajaxmock_.call[guid].requests.length;
			}, guid);

			return this.nthRequest(last);
		},

		first: function () {
			return this.nthRequest(1);
		},

		firstOrNull: function () {
			return this.nthRequestOrNull(1);
		},

        nthRequestHeader: function (index, key) {
            var r = page.evaluate(function (guid, index) {
                if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
                    return;
                }
                var request = window._ajaxmock_.call[guid].requests[index - 1];
                return request.requestHeaders;
            }, guid, index);

            if(typeof r === 'undefined'){
                console.log('[PhantomXHR] Could not get request');
            }

            return r[key];
        },

		lastRequestHeader: function (key) {
            var last = page.evaluate(function (guid) {
                return window._ajaxmock_.call[guid].requests.length;
            }, guid);

            return this.nthRequestHeader(last, key);
        },

        firstRequestHeader: function (key) {
            return this.nthRequestHeader(1, key);
        },

		nthResponse: function (num, response) {
			var r = page.evaluate(function (guid, num, response) {
				if (typeof(response.responseBody) === "object") {
					response.responseBody = JSON.stringify(response.responseBody);
				}
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				window._ajaxmock_.call[guid].responses[num-1] = response;
				return true;
			}, guid, num, response );

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not set response');
			}

			return this;
		},

		nthProgress: function(nth, event){

			function isNumber(n) {
				return !isNaN(parseFloat(n)) && isFinite(n);
			}

			if( isNumber(event.loaded) && isNumber(event.total) ){
				page.evaluate(function (guid, nth, event) {
					var mock = window._ajaxmock_;
					var req;

					if( !(mock && mock.call[guid]) ){
						return;
					}

					req = mock.call[guid].requests[nth-1];

					if(req){
						req.uploadProgress(event);
					}

				}, guid, nth, event);
			} else {
				console.log('[PhantomXHR] Could not set progress');
			}
		},

		restore: function(){
			page.evaluate(function(){
				window.backup_xhr.restore();
			});
		},

		nthRespond: function(nth, response){
			// if you don't want to respond immediately

			page.evaluate(function (guid, nth, response) {
				var placeholder = 'placeholder';
				var si;
				var item;

				function processResponse(){
					var res;
					var method;
					var item = window._ajaxmock_.call[guid];
					var queue = item.queuedResponses;

					if(queue && item.respondMethods.length){

						res = queue[nth-1];
						method = item.respondMethods[nth-1];
						if(method){
							method(res === placeholder ? void 0 : res);
						}

						if(si) {
							clearInterval(si);
						}

						return true;
					}
					return false;
				}

				response =  response || placeholder;

				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}

				item = window._ajaxmock_.call[guid];

				if(!item.queuedResponses) {
					item.queuedResponses = [];
				}

				item.queuedResponses = [];
				item.queuedResponses.push(response);

				if(!processResponse()){
					si = setInterval(processResponse,50);
				}

			}, guid, nth, response);
		},

		respond: function(response){
			return this.nthRespond(1, response);
		},

		progress: function(event){
			return this.nthProgress(1, event);
		},

		uri: options.url,
		method: options.method
	};
}

function getAllRequests() {
	var requests = page.evaluate(function () {
		var requests = {};

		if (window._ajaxmock_) {
			requests = window._ajaxmock_.requests;
		}

		return requests;
	});

	return requests;
}

function allRequestsCompleted() {
	return page.evaluate(function () {

		if (window._ajaxmock_) {
			return window._ajaxmock_.requests_created === window._ajaxmock_.requests_completed;
		}

		return false;
	});
}
<commit_msg>hold and progress<commit_after>var require = patchRequire(require);

exports.init = phantomXHRInit;
exports.fake = fake;
exports.requests = getAllRequests;
exports.completed = allRequestsCompleted;

var page;

function phantomXHRInit(initPage, options){
	var fs = require('fs');

	var inject = false;

	options = options || {};

	if(initPage.injectJs){

		initPage.evaluate(function(){
			// Shim these constructors to make progress events work in PhantomJS
    		window.ProgressEvent = function(){};
    		window.CustomEvent = function(){};
		});

		inject = initPage.injectJs(options.libraryRoot ? (fs.absolute(options.libraryRoot) + 'sinon.js') : './node_modules/phantomxhr/sinon.js');

		initPage.evaluate(function(){

			// A naive implementation for simulating upload events
		    function FakeEvent(type, bubbles, cancelable, target) {
		        this.initEvent(type, bubbles, cancelable, target);
		    };

			FakeEvent.prototype = {
				initEvent: function(type, event, cancelable, target) {
					var key;

					this.type = type;
					this.bubbles = event;
					this.lengthComputable = true;
					this.isTrusted = true;
					this.cancelable = cancelable;
					this.target = target;
					this.loaded = 0;
					this.total = 0;

					for ( key in event ){
						this[key] = event[key];
					}
				},
				stopPropagation: function () {},
				stopImmediatePropagation: function () {},
				preventDefault: function () {
					this.defaultPrevented = true;
				}
			};

    		window.ProgressEvent = FakeEvent;
    		window.CustomEvent = FakeEvent;
		});

	}

	if(inject){
		page = initPage;
		setup(options);
	} else {
		console.log("[PhantomXHR] Can't find sinon.js");
	}
}

function setup(options){
	page.evaluate(function (options) {

		if (!window._ajaxmock_) {
			window._ajaxmock_ = {
				matches: [],
				requests_created: 0,
				requests_completed: 0,
				requests: {},
				call: {},
				fake: function (match) {

					function S4() {
						return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
					}

					function makeGuid() {
						return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
					}

					var urlIsString = match.url.indexOf('REGEXP') === -1;
					var guid = makeGuid();

					match.guid = guid;
					match.url = match.url.replace('REGEXP', '');

					console.log('[PhantomXHR] Match added [' + (match.method || 'All REST verbs') + "] : " + match.url + "'");

					match.requests = [];
					match.responses = [];
					match.respondMethods = [];

					window._ajaxmock_.call[guid] = match;

					window._ajaxmock_.matches.push(function (method, url) {

						if (!url) {
							return false;
						}

						var urlMatch = urlIsString ? url.indexOf(match.url) !== -1 : new RegExp(match.url).test(url);
						var methodMatch = (typeof match.method === 'undefined') ? true : match.method.toLowerCase() === method.toLowerCase();

						if (urlMatch && methodMatch) {
							return match;
						} else {
							return false;
						}
					});

					return guid;
				},
				init: function () {
					var _xhr = window.sinon.useFakeXMLHttpRequest();

					// overrideMimeType is not mocked see this issue
					// https://github.com/cjohansen/Sinon.JS/issues/559
					window.sinon.FakeXMLHttpRequest.prototype.overrideMimeType = function() {return;}

					// we backup _xhr object
					window.backup_xhr = _xhr;

					// If need create real XHR (by default false)
					if (options.allowRealRequests) {
						_xhr.useFilters = true;
						_xhr.addFilter(function(method, url) {
							var anyMatches = false;
							window._ajaxmock_.matches.reverse().forEach(function (func) {
								anyMatches = anyMatches || func(method, url);
							});
							return !anyMatches;
						});
					}

					_xhr.upload = document.createElement('div');

					_xhr.onCreate = function (request) {

						window._ajaxmock_.requests_created++;
						setTimeout(function () {
							var anyMatches = false;
							var requests = window._ajaxmock_.requests;

							if (!request.url) {
								console.log('[PhantomXHR] XHR has been initialised but not opened.');
								return;
							} // this shouldn't happen, but sometimes does
							// store the request for later matching
							if (requests[request.method.toLowerCase() + request.url]) {
								requests[request.method.toLowerCase() + request.url].count++;
							} else {
								requests[request.method.toLowerCase() + request.url] = {
									data: request,
									count: 1
								};
							}

							window._ajaxmock_.matches.slice(0).reverse().forEach(function (func) {
								anyMatches = anyMatches || func(request.method, request.url);
							});

							if (anyMatches) {
								respond(request, anyMatches);
							} else {
								console.log('[PhantomXHR] did not respond to ' + request.method + ' ' + request.url);
								window._ajaxmock_.requests_completed++;
							}
						}, 100);
					};
				}
			};
			window._ajaxmock_.init();
		}

		function respond(request, response) {

			if(!window._ajaxmock_){
				console.log('[PhantomXHR] could not respond, window._ajaxmock_ does not exist.');
				return;
			}

			var call = window._ajaxmock_.call;
			var callForThisMatch;
			var responseForThisMatch;
			var status;
			var body;

			console.log('[PhantomXHR] recieved request for ' + request.method + ": " + request.url + "'");

			callForThisMatch = call[response.guid];
			callForThisMatch.requests.push(request);

			responseForThisMatch = callForThisMatch.responses[callForThisMatch.requests.length];

			if (responseForThisMatch) {
				status = responseForThisMatch.status;
				body = responseForThisMatch.responseBody;
				console.log('[PhantomXHR] with status: ' + responseForThisMatch.status);
			}

			console.log('[PhantomXHR] with status: ' + response.status);

			if(response.holdResponse){

				callForThisMatch.respondMethods.push(function(responseOverride){
					responseOverride = responseOverride || response;

					console.log('[PhantomXHR] Responding to postponed ' + request.method + ": " + request.url + "'");

					request.respond(
						status || responseOverride.status || 200, responseOverride.headers || {
							"Content-Type": "application/json"
						},
						body || responseOverride.responseBody || ''
					);

					window._ajaxmock_.requests_completed++;
				});

			} else {
				console.log('[PhantomXHR] Responding to ' + request.method + ": " + request.url + "'");

				console.log('[PhantomXHR] status ', status , response.status , 200);

				request.respond(
					status || response.status || 200, response.headers || {
						"Content-Type": "application/json"
					},
					body || response.responseBody || ''
				);

				window._ajaxmock_.requests_completed++;
			}

		}
	}, options);
}

function fake(options) {
	var url = !! options.url.source ? 'REGEXP' + options.url.source : options.url;

	if(typeof(options.responseBody) === "object"){
		options.responseBody = JSON.stringify(options.responseBody);
	}

	var guid = page.evaluate(function (url, method, responseBody, status, headers, holdResponse) {
		if (window._ajaxmock_ && url) {

			if (responseBody && headers && headers["Content-Type"] && headers["Content-Type"] === "application/json") {
				try {
					JSON.parse(responseBody);
				} catch (e) {
					return 'JSON';
				}
			}

			return window._ajaxmock_.fake({
				url: url,
				method: method,
				responseBody: responseBody,
				status: status,
				headers: headers,
				holdResponse: holdResponse
			});
		}
	}, url, options.method, options.responseBody, options.status, options.headers, !!options.holdResponse);

	if (guid === 'JSON') {
		console.log('[PhantomXHR] JSON was invalid : ' + options.method + ' : ' + url);
	}

	return {
		count: function () {
			var c = page.evaluate(function (guid) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				return window._ajaxmock_.call[guid].requests.length;
			}, guid);

			if(typeof c === 'undefined'){
				console.log('[PhantomXHR] Could not get count');
			}

			return c;
		},

		nthRequest: function (index) {
			var r = page.evaluate(function (guid, index) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				var request = window._ajaxmock_.call[guid].requests[index - 1];
				return request.requestBody;
			}, guid, index);

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not get request');
			}

			return r;
		},

		nthRequestOrNull: function (index) {
			var r = page.evaluate(function (guid, index) {
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				var request = window._ajaxmock_.call[guid].requests[index - 1];
				if (!request) { return null; }
				return request;
			}, guid, index);

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not get request');
			}

			return r;
		},

		last: function () {
			var last = page.evaluate(function (guid) {
				return window._ajaxmock_.call[guid].requests.length;
			}, guid);

			return this.nthRequest(last);
		},

		first: function () {
			return this.nthRequest(1);
		},

		firstOrNull: function () {
			return this.nthRequestOrNull(1);
		},

        nthRequestHeader: function (index, key) {
            var r = page.evaluate(function (guid, index) {
                if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
                    return;
                }
                var request = window._ajaxmock_.call[guid].requests[index - 1];
                return request.requestHeaders;
            }, guid, index);

            if(typeof r === 'undefined'){
                console.log('[PhantomXHR] Could not get request');
            }

            return r ? r[key] : null;
        },

		lastRequestHeader: function (key) {
            var last = page.evaluate(function (guid) {
                return window._ajaxmock_.call[guid].requests.length;
            }, guid);

            return this.nthRequestHeader(last, key);
        },

        firstRequestHeader: function (key) {
            return this.nthRequestHeader(1, key);
        },

		nthResponse: function (num, response) {
			var r = page.evaluate(function (guid, num, response) {
				if (typeof(response.responseBody) === "object") {
					response.responseBody = JSON.stringify(response.responseBody);
				}
				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}
				window._ajaxmock_.call[guid].responses[num-1] = response;
				return true;
			}, guid, num, response );

			if(typeof r === 'undefined'){
				console.log('[PhantomXHR] Could not set response');
			}

			return this;
		},

		nthProgress: function(nth, event){

			function isNumber(n) {
				return !isNaN(parseFloat(n)) && isFinite(n);
			}

			if( isNumber(event.loaded) && isNumber(event.total) ){
				page.evaluate(function (guid, nth, event) {
					var mock = window._ajaxmock_;
					var req;

					if( !(mock && mock.call[guid]) ){
						return;
					}

					req = mock.call[guid].requests[nth-1];

					if(req){
						req.uploadProgress(event);
					}

				}, guid, nth, event);
			} else {
				console.log('[PhantomXHR] Could not set progress');
			}
		},

		restore: function(){
			page.evaluate(function(){
				window.backup_xhr.restore();
			});
		},

		nthRespond: function(nth, response){
			// if you don't want to respond immediately

			page.evaluate(function (guid, nth, response) {
				var placeholder = 'placeholder';
				var si;
				var item;

				function processResponse(){
					var res;
					var method;
					var item = window._ajaxmock_.call[guid];
					var queue = item.queuedResponses;

					if(queue && item.respondMethods.length){

						res = queue[nth-1];
						method = item.respondMethods[nth-1];
						if(method){
							method(res === placeholder ? void 0 : res);
						}

						if(si) {
							clearInterval(si);
						}

						return true;
					}
					return false;
				}

				response =  response || placeholder;

				if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
					return;
				}

				item = window._ajaxmock_.call[guid];

				if(!item.queuedResponses) {
					item.queuedResponses = [];
				}

				item.queuedResponses = [];
				item.queuedResponses.push(response);

				if(!processResponse()){
					si = setInterval(processResponse,50);
				}

			}, guid, nth, response);
		},

		respond: function(response){
			return this.nthRespond(1, response);
		},

		progress: function(event){
			return this.nthProgress(1, event);
		},

        hold: function(){
            page.evaluate(function (guid) {
                if( !(window._ajaxmock_ && window._ajaxmock_.call[guid] )){
                    return;
                }
                window._ajaxmock_.call[guid].holdResponse = true;
            }, guid);
        },

        progress: function(event){
            return this.nthProgress(1, event);
        },

		uri: options.url,
		method: options.method
	};
}

function getAllRequests() {
	var requests = page.evaluate(function () {
		var requests = {};

		if (window._ajaxmock_) {
			requests = window._ajaxmock_.requests;
		}

		return requests;
	});

	return requests;
}

function allRequestsCompleted() {
	return page.evaluate(function () {

		if (window._ajaxmock_) {
			return window._ajaxmock_.requests_created === window._ajaxmock_.requests_completed;
		}

		return false;
	});
}
<|endoftext|>