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