Home | History | Annotate | Download | only in test
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 define(["console"], function(console) {
      6   // Equality function based on isEqual in
      7   // Underscore.js 1.5.2
      8   // http://underscorejs.org
      9   // (c) 2009-2013 Jeremy Ashkenas,
     10   //               DocumentCloud,
     11   //               and Investigative Reporters & Editors
     12   // Underscore may be freely distributed under the MIT license.
     13   //
     14   function has(obj, key) {
     15     return obj.hasOwnProperty(key);
     16   }
     17   function isFunction(obj) {
     18     return typeof obj === 'function';
     19   }
     20   function isArrayBufferClass(className) {
     21     return className == '[object ArrayBuffer]' ||
     22         className.match(/\[object \w+\d+(Clamped)?Array\]/);
     23   }
     24   // Internal recursive comparison function for `isEqual`.
     25   function eq(a, b, aStack, bStack) {
     26     // Identical objects are equal. `0 === -0`, but they aren't identical.
     27     // See the Harmony `egal` proposal:
     28     // http://wiki.ecmascript.org/doku.php?id=harmony:egal.
     29     if (a === b)
     30       return a !== 0 || 1 / a == 1 / b;
     31     // A strict comparison is necessary because `null == undefined`.
     32     if (a == null || b == null)
     33       return a === b;
     34     // Compare `[[Class]]` names.
     35     var className = toString.call(a);
     36     if (className != toString.call(b))
     37       return false;
     38     switch (className) {
     39       // Strings, numbers, dates, and booleans are compared by value.
     40       case '[object String]':
     41         // Primitives and their corresponding object wrappers are equivalent;
     42         // thus, `"5"` is equivalent to `new String("5")`.
     43         return a == String(b);
     44       case '[object Number]':
     45         // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is
     46         // performed for other numeric values.
     47         return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
     48       case '[object Date]':
     49       case '[object Boolean]':
     50         // Coerce dates and booleans to numeric primitive values. Dates are
     51         // compared by their millisecond representations. Note that invalid
     52         // dates with millisecond representations of `NaN` are not equivalent.
     53         return +a == +b;
     54       // RegExps are compared by their source patterns and flags.
     55       case '[object RegExp]':
     56         return a.source == b.source &&
     57                a.global == b.global &&
     58                a.multiline == b.multiline &&
     59                a.ignoreCase == b.ignoreCase;
     60     }
     61     if (typeof a != 'object' || typeof b != 'object')
     62       return false;
     63     // Assume equality for cyclic structures. The algorithm for detecting
     64     // cyclic structures is adapted from ES 5.1 section 15.12.3, abstract
     65     // operation `JO`.
     66     var length = aStack.length;
     67     while (length--) {
     68       // Linear search. Performance is inversely proportional to the number of
     69       // unique nested structures.
     70       if (aStack[length] == a)
     71         return bStack[length] == b;
     72     }
     73     // Objects with different constructors are not equivalent, but `Object`s
     74     // from different frames are.
     75     var aCtor = a.constructor, bCtor = b.constructor;
     76     if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
     77                              isFunction(bCtor) && (bCtor instanceof bCtor))
     78                         && ('constructor' in a && 'constructor' in b)) {
     79       return false;
     80     }
     81     // Add the first object to the stack of traversed objects.
     82     aStack.push(a);
     83     bStack.push(b);
     84     var size = 0, result = true;
     85     // Recursively compare objects and arrays.
     86     if (className == '[object Array]' || isArrayBufferClass(className)) {
     87       // Compare array lengths to determine if a deep comparison is necessary.
     88       size = a.length;
     89       result = size == b.length;
     90       if (result) {
     91         // Deep compare the contents, ignoring non-numeric properties.
     92         while (size--) {
     93           if (!(result = eq(a[size], b[size], aStack, bStack)))
     94             break;
     95         }
     96       }
     97     } else {
     98       // Deep compare objects.
     99       for (var key in a) {
    100         if (has(a, key)) {
    101           // Count the expected number of properties.
    102           size++;
    103           // Deep compare each member.
    104           if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack)))
    105             break;
    106         }
    107       }
    108       // Ensure that both objects contain the same number of properties.
    109       if (result) {
    110         for (key in b) {
    111           if (has(b, key) && !(size--))
    112             break;
    113         }
    114         result = !size;
    115       }
    116     }
    117     // Remove the first object from the stack of traversed objects.
    118     aStack.pop();
    119     bStack.pop();
    120     return result;
    121   };
    122 
    123   function describe(subjects) {
    124     var descriptions = [];
    125     Object.getOwnPropertyNames(subjects).forEach(function(name) {
    126       if (name === "Description")
    127         descriptions.push(subjects[name]);
    128       else
    129         descriptions.push(name + ": " + JSON.stringify(subjects[name]));
    130     });
    131     return descriptions.join(" ");
    132   }
    133 
    134   var predicates = {};
    135 
    136   predicates.toBe = function(actual, expected) {
    137     return {
    138       "result": actual === expected,
    139       "message": describe({
    140         "Actual": actual,
    141         "Expected": expected,
    142       }),
    143     };
    144   };
    145 
    146   predicates.toEqual = function(actual, expected) {
    147     return {
    148       "result": eq(actual, expected, [], []),
    149       "message": describe({
    150         "Actual": actual,
    151         "Expected": expected,
    152       }),
    153     };
    154   };
    155 
    156   predicates.toBeDefined = function(actual) {
    157     return {
    158       "result": typeof actual !== "undefined",
    159       "message": describe({
    160         "Actual": actual,
    161         "Description": "Expected a defined value",
    162       }),
    163     };
    164   };
    165 
    166   predicates.toBeUndefined = function(actual) {
    167     // Recall: undefined is just a global variable. :)
    168     return {
    169       "result": typeof actual === "undefined",
    170       "message": describe({
    171         "Actual": actual,
    172         "Description": "Expected an undefined value",
    173       }),
    174     };
    175   };
    176 
    177   predicates.toBeNull = function(actual) {
    178     // Recall: typeof null === "object".
    179     return {
    180       "result": actual === null,
    181       "message": describe({
    182         "Actual": actual,
    183         "Expected": null,
    184       }),
    185     };
    186   };
    187 
    188   predicates.toBeTruthy = function(actual) {
    189     return {
    190       "result": !!actual,
    191       "message": describe({
    192         "Actual": actual,
    193         "Description": "Expected a truthy value",
    194       }),
    195     };
    196   };
    197 
    198   predicates.toBeFalsy = function(actual) {
    199     return {
    200       "result": !!!actual,
    201       "message": describe({
    202         "Actual": actual,
    203         "Description": "Expected a falsy value",
    204       }),
    205     };
    206   };
    207 
    208   predicates.toContain = function(actual, element) {
    209     return {
    210       "result": (function () {
    211         for (var i = 0; i < actual.length; ++i) {
    212           if (eq(actual[i], element, [], []))
    213             return true;
    214         }
    215         return false;
    216       })(),
    217       "message": describe({
    218         "Actual": actual,
    219         "Element": element,
    220       }),
    221     };
    222   };
    223 
    224   predicates.toBeLessThan = function(actual, reference) {
    225     return {
    226       "result": actual < reference,
    227       "message": describe({
    228         "Actual": actual,
    229         "Reference": reference,
    230       }),
    231     };
    232   };
    233 
    234   predicates.toBeGreaterThan = function(actual, reference) {
    235     return {
    236       "result": actual > reference,
    237       "message": describe({
    238         "Actual": actual,
    239         "Reference": reference,
    240       }),
    241     };
    242   };
    243 
    244   predicates.toThrow = function(actual) {
    245     return {
    246       "result": (function () {
    247         if (!isFunction(actual))
    248           throw new TypeError;
    249         try {
    250           actual();
    251         } catch (ex) {
    252           return true;
    253         }
    254         return false;
    255       })(),
    256       "message": "Expected function to throw",
    257     };
    258   }
    259 
    260   function negate(predicate) {
    261     return function() {
    262       var outcome = predicate.apply(null, arguments);
    263       outcome.result = !outcome.result;
    264       return outcome;
    265     }
    266   }
    267 
    268   function check(predicate) {
    269     return function() {
    270       var outcome = predicate.apply(null, arguments);
    271       if (outcome.result)
    272         return;
    273       throw outcome.message;
    274     };
    275   }
    276 
    277   function Condition(actual) {
    278     this.not = {};
    279     Object.getOwnPropertyNames(predicates).forEach(function(name) {
    280       var bound = predicates[name].bind(null, actual);
    281       this[name] = check(bound);
    282       this.not[name] = check(negate(bound));
    283     }, this);
    284   }
    285 
    286   return function(actual) {
    287     return new Condition(actual);
    288   };
    289 });
    290