blob: 597b5b1d5e32bb456109f10cea141fb480451b93 [file] [log] [blame]
// 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 Maps, 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 if (className == '[object Map]') {
result = a.size == b.size;
if (result) {
var entries = a.entries();
for (var e = entries.next(); result && !e.done; e = entries.next()) {
var key = e.value[0];
var value = e.value[1];
result = b.has(key) && eq(value, b.get(key), aStack, bStack);
}
}
} 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);
};
});