| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| define(function() { |
| // Equality function based on isEqual in |
| // Underscore.js 1.5.2 |
| // http://underscorejs.org |
| // (c) 2009-2013 Jeremy Ashkenas, |
| // DocumentCloud, |
| // and Investigative Reporters & Editors |
| // Underscore may be freely distributed under the MIT license. |
| // |
| function has(obj, key) { |
| return obj.hasOwnProperty(key); |
| } |
| function isFunction(obj) { |
| return typeof obj === 'function'; |
| } |
| function isArrayBufferClass(className) { |
| return className == '[object ArrayBuffer]' || |
| className.match(/\[object \w+\d+(Clamped)?Array\]/); |
| } |
| // Internal recursive comparison function for `isEqual`. |
| function eq(a, b, aStack, bStack) { |
| // Identical objects are equal. `0 === -0`, but they aren't identical. |
| // See the Harmony `egal` proposal: |
| // http://wiki.ecmascript.org/doku.php?id=harmony:egal. |
| 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; |
| // Compare `[[Class]]` names. |
| var className = toString.call(a); |
| if (className != 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]': |
| // `NaN`s 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; |
| } |
| // Objects with different constructors are not equivalent, but `Object`s |
| // from different frames are. |
| var aCtor = a.constructor, bCtor = b.constructor; |
| if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) && |
| isFunction(bCtor) && (bCtor instanceof bCtor)) |
| && ('constructor' in a && 'constructor' in b)) { |
| return false; |
| } |
| // Add the first object to the stack of traversed objects. |
| aStack.push(a); |
| bStack.push(b); |
| var size = 0, result = true; |
| // Recursively compare objects and arrays. |
| if (className == '[object Array]' || isArrayBufferClass(className)) { |
| // 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))) |
| break; |
| } |
| } |
| } else { |
| // 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))) |
| 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 describe(subjects) { |
| var descriptions = []; |
| Object.getOwnPropertyNames(subjects).forEach(function(name) { |
| if (name === "Description") |
| descriptions.push(subjects[name]); |
| else |
| descriptions.push(name + ": " + JSON.stringify(subjects[name])); |
| }); |
| return descriptions.join(" "); |
| } |
| |
| var predicates = {}; |
| |
| predicates.toBe = function(actual, expected) { |
| return { |
| "result": actual === expected, |
| "message": describe({ |
| "Actual": actual, |
| "Expected": expected, |
| }), |
| }; |
| }; |
| |
| predicates.toEqual = function(actual, expected) { |
| return { |
| "result": eq(actual, expected, [], []), |
| "message": describe({ |
| "Actual": actual, |
| "Expected": expected, |
| }), |
| }; |
| }; |
| |
| predicates.toBeDefined = function(actual) { |
| return { |
| "result": typeof actual !== "undefined", |
| "message": describe({ |
| "Actual": actual, |
| "Description": "Expected a defined value", |
| }), |
| }; |
| }; |
| |
| predicates.toBeUndefined = function(actual) { |
| // Recall: undefined is just a global variable. :) |
| return { |
| "result": typeof actual === "undefined", |
| "message": describe({ |
| "Actual": actual, |
| "Description": "Expected an undefined value", |
| }), |
| }; |
| }; |
| |
| predicates.toBeNull = function(actual) { |
| // Recall: typeof null === "object". |
| return { |
| "result": actual === null, |
| "message": describe({ |
| "Actual": actual, |
| "Expected": null, |
| }), |
| }; |
| }; |
| |
| predicates.toBeTruthy = function(actual) { |
| return { |
| "result": !!actual, |
| "message": describe({ |
| "Actual": actual, |
| "Description": "Expected a truthy value", |
| }), |
| }; |
| }; |
| |
| predicates.toBeFalsy = function(actual) { |
| return { |
| "result": !!!actual, |
| "message": describe({ |
| "Actual": actual, |
| "Description": "Expected a falsy value", |
| }), |
| }; |
| }; |
| |
| predicates.toContain = function(actual, element) { |
| return { |
| "result": (function () { |
| for (var i = 0; i < actual.length; ++i) { |
| if (eq(actual[i], element, [], [])) |
| return true; |
| } |
| return false; |
| })(), |
| "message": describe({ |
| "Actual": actual, |
| "Element": element, |
| }), |
| }; |
| }; |
| |
| predicates.toBeLessThan = function(actual, reference) { |
| return { |
| "result": actual < reference, |
| "message": describe({ |
| "Actual": actual, |
| "Reference": reference, |
| }), |
| }; |
| }; |
| |
| predicates.toBeGreaterThan = function(actual, reference) { |
| return { |
| "result": actual > reference, |
| "message": describe({ |
| "Actual": actual, |
| "Reference": reference, |
| }), |
| }; |
| }; |
| |
| predicates.toThrow = function(actual) { |
| return { |
| "result": (function () { |
| if (!isFunction(actual)) |
| throw new TypeError; |
| try { |
| actual(); |
| } catch (ex) { |
| return true; |
| } |
| return false; |
| })(), |
| "message": "Expected function to throw", |
| }; |
| } |
| |
| function negate(predicate) { |
| return function() { |
| var outcome = predicate.apply(null, arguments); |
| outcome.result = !outcome.result; |
| return outcome; |
| } |
| } |
| |
| function check(predicate) { |
| return function() { |
| var outcome = predicate.apply(null, arguments); |
| if (outcome.result) |
| return; |
| throw outcome.message; |
| }; |
| } |
| |
| function Condition(actual) { |
| this.not = {}; |
| Object.getOwnPropertyNames(predicates).forEach(function(name) { |
| var bound = predicates[name].bind(null, actual); |
| this[name] = check(bound); |
| this.not[name] = check(negate(bound)); |
| }, this); |
| } |
| |
| return function(actual) { |
| return new Condition(actual); |
| }; |
| }); |