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
5define(function() {
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