matchersUtil.js

getJasmineRequireObj().matchersUtil = function(j$) {

TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?


  return {
    equals: function(a, b, customTesters) {
      customTesters = customTesters || [];

      return eq(a, b, [], [], customTesters);
    },

    contains: function(haystack, needle, customTesters) {
      customTesters = customTesters || [];

      if (Object.prototype.toString.apply(haystack) === '[object Array]') {
        for (var i = 0; i < haystack.length; i++) {
          if (eq(haystack[i], needle, [], [], customTesters)) {
            return true;
          }
        }
        return false;
      }
      return !!haystack && haystack.indexOf(needle) >= 0;
    },

    buildFailureMessage: function() {
      var args = Array.prototype.slice.call(arguments, 0),
        matcherName = args[0],
        isNot = args[1],
        actual = args[2],
        expected = args.slice(3),
        englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });

      var message = 'Expected ' +
        j$.pp(actual) +
        (isNot ? ' not ' : ' ') +
        englishyPredicate;

      if (expected.length > 0) {
        for (var i = 0; i < expected.length; i++) {
          if (i > 0) {
            message += ',';
          }
          message += ' ' + j$.pp(expected[i]);
        }
      }

      return message + '.';
    }
  };


Equality function lovingly adapted from isEqual in Underscore

  function eq(a, b, aStack, bStack, customTesters) {
    var result = true;

    for (var i = 0; i < customTesters.length; i++) {
      var customTesterResult = customTesters[i](a, b);
      if (!j$.util.isUndefined(customTesterResult)) {
        return customTesterResult;
      }
    }

    if (a instanceof j$.Any) {
      result = a.jasmineMatches(b);
      if (result) {
        return true;
      }
    }

    if (b instanceof j$.Any) {
      result = b.jasmineMatches(a);
      if (result) {
        return true;
      }
    }

    if (b instanceof j$.ObjectContaining) {
      result = b.jasmineMatches(a);
      if (result) {
        return true;
      }
    }

    if (a instanceof Error && b instanceof Error) {
      return a.message == b.message;
    }


Identical objects are equal. 0 === -0, but they aren't identical. See the Harmony egal proposal.

    if (a === b) { return a !== 0 || 1 / a == 1 / b; }

A strict comparison is necessary because null == undefined.

    if (a === null || b === null) { return a === b; }
    var className = Object.prototype.toString.call(a);
    if (className != Object.prototype.toString.call(b)) { return false; }
    switch (className) {

Strings, numbers, dates, and booleans are compared by value.

      case '[object String]':

Primitives and their corresponding object wrappers are equivalent; thus, "5" is equivalent to new String("5").

        return a == String(b);
      case '[object Number]':

NaNs are equivalent, but non-reflexive. An egal comparison is performed for other numeric values.

        return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
      case '[object Date]':
      case '[object Boolean]':

Coerce dates and booleans to numeric primitive values. Dates are compared by their millisecond representations. Note that invalid dates with millisecond representations of NaN are not equivalent.

        return +a == +b;

RegExps are compared by their source patterns and flags.

      case '[object RegExp]':
        return a.source == b.source &&
          a.global == b.global &&
          a.multiline == b.multiline &&
          a.ignoreCase == b.ignoreCase;
    }
    if (typeof a != 'object' || typeof b != 'object') { return false; }

Assume equality for cyclic structures. The algorithm for detecting cyclic structures is adapted from ES 5.1 section 15.12.3, abstract operation JO.

    var length = aStack.length;
    while (length--) {

Linear search. Performance is inversely proportional to the number of unique nested structures.

      if (aStack[length] == a) { return bStack[length] == b; }
    }

Add the first object to the stack of traversed objects.

    aStack.push(a);
    bStack.push(b);
    var size = 0;

Recursively compare objects and arrays.

    if (className == '[object Array]') {

Compare array lengths to determine if a deep comparison is necessary.

      size = a.length;
      result = size == b.length;
      if (result) {

Deep compare the contents, ignoring non-numeric properties.

        while (size--) {
          if (!(result = eq(a[size], b[size], aStack, bStack, customTesters))) { break; }
        }
      }
    } else {

Objects with different constructors are not equivalent, but Objects from different frames are.

      var aCtor = a.constructor, bCtor = b.constructor;
      if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
        isFunction(bCtor) && (bCtor instanceof bCtor))) {
        return false;
      }

Deep compare objects.

      for (var key in a) {
        if (has(a, key)) {

Count the expected number of properties.

          size++;

Deep compare each member.

          if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) { break; }
        }
      }

Ensure that both objects contain the same number of properties.

      if (result) {
        for (key in b) {
          if (has(b, key) && !(size--)) { break; }
        }
        result = !size;
      }
    }

Remove the first object from the stack of traversed objects.

    aStack.pop();
    bStack.pop();

    return result;

    function has(obj, key) {
      return obj.hasOwnProperty(key);
    }

    function isFunction(obj) {
      return typeof obj === 'function';
    }
  }
};