master
Raw Download raw file
  1if(typeof environment == 'undefined') environment = 'default'; // Override me!
  2
  3// The scope when none is set.
  4nullScope = (function(){ return this; }).call();
  5
  6var results;
  7var currentTest;
  8var moduleName;
  9var moduleSetupMethod;
 10var moduleTeardownMethod;
 11
 12var syncTestsRunning;
 13var asyncTestsRunning;
 14
 15// Capturing the timers here b/c Mootools (and maybe other frameworks) may clear a timeout that
 16// it kicked off after this script is loaded, which would throw off a simple incrementing mechanism.
 17var capturedTimers = [];
 18var testStartTime;
 19var runtime;
 20
 21
 22// Arrays and objects must be treated separately here because in IE arrays with undefined
 23// elements will not pass the .hasOwnProperty check. For example [undefined].hasOwnProperty('0')
 24// will report false.
 25var arrayEqual = function(one, two) {
 26  var i, result = true;
 27  if(!one || !two) {
 28    return false;
 29  }
 30  testArrayEach(one, function(a, i) {
 31    if(!testIsEqual(one[i], two[i])) {
 32      result = false;
 33    }
 34  });
 35  return result && one.length === two.length;
 36}
 37
 38var sortOnStringValue = function(arr) {
 39  return arr.sort(function(a, b) {
 40    var aType = typeof a;
 41    var bType = typeof b;
 42    var aVal = String(a);
 43    var bVal = String(b);
 44    if(aType != bType) {
 45      return aType < bType;
 46    }
 47    if(aVal === bVal) return 0;
 48    return a < b ? -1 : 1;
 49  });
 50}
 51
 52var testArrayIndexOf = function(arr, obj) {
 53  for(var i = 0; i < arr.length; i++) {
 54    if(arr[i] === obj) {
 55      return i;
 56    }
 57  }
 58  return -1;
 59}
 60
 61testCloneObject = function(obj) {
 62  var result = {}, key;
 63  for(key in obj) {
 64    if(!obj.hasOwnProperty(key)) continue;
 65    result[key] = obj[key];
 66  }
 67  return result;
 68}
 69
 70testArrayEach = function(arr, fn, sparse) {
 71  var length = arr.length, i = 0;
 72  while(i < length) {
 73    if(!(i in arr)) {
 74      return testIterateOverSparseArray(arr, fn, i);
 75    } else if(fn.call(arr, arr[i], i, arr) === false) {
 76      break;
 77    }
 78    i++;
 79  }
 80}
 81
 82testIterateOverSparseArray = function(arr, fn, fromIndex) {
 83  var indexes = [], i;
 84  for(i in arr) {
 85    if(testIsArrayIndex(arr, i) && i >= fromIndex) {
 86      indexes.push(parseInt(i));
 87    }
 88  }
 89  testArrayEach(indexes.sort(), function(index) {
 90    return fn.call(arr, arr[index], index, arr);
 91  });
 92  return arr;
 93}
 94
 95testIsArrayIndex = function(arr, i) {
 96  return i in arr && (i >>> 0) == i && i != 0xffffffff;
 97}
 98
 99testIsArray = function(obj) {
100  return Object.prototype.toString.call(obj) === '[object Array]';
101}
102
103testPadNumber = function(val, place, sign) {
104  var num = Math.abs(val);
105  var len = Math.abs(num).toString().replace(/\.\d+/, '').length;
106  var str =  new Array(Math.max(0, place - len) + 1).join('0') + num;
107  if(val < 0 || sign) {
108    str = (val < 0 ? '-' : '+') + str;
109  }
110  return str;
111}
112
113testCapitalize = function(str) {
114  return str.slice(0,1).toUpperCase() + str.slice(1);
115}
116
117var objectEqual = function(one, two) {
118  var onep = 0, twop = 0, key;
119  if(one && two) {
120    for(key in one) {
121      if(!one.hasOwnProperty(key)) continue;
122      onep++;
123      if(!testIsEqual(one[key], two[key])) {
124        return false;
125      }
126    }
127    for(key in two) {
128      if(!two.hasOwnProperty(key)) continue;
129      twop++;
130    }
131  }
132  return onep === twop && String(one) === String(two);
133}
134
135var testIsEqual = function(one, two) {
136
137  var type, klass;
138
139  type = typeof one;
140
141  if(type === 'string' || type === 'boolean' || one == null) {
142    return one === two;
143  } else if(type === 'number') {
144    return typeof two === 'number' && ((isNaN(one) && isNaN(two)) || one === two);
145  }
146
147  klass = Object.prototype.toString.call(one);
148
149  if(klass === '[object Date]') {
150    return one.getTime() === two.getTime();
151  } else if(klass === '[object RegExp]') {
152    return String(one) === String(two);
153  } else if(klass === '[object Array]') {
154    return arrayEqual(one, two);
155  } else if((klass === '[object Object]' || klass === '[object Arguments]') && ('hasOwnProperty' in one) && type === 'object') {
156    return objectEqual(one, two);
157  } else if(klass === '[object Number]' && isNaN(one) && isNaN(two)) {
158    return true;
159  }
160
161  return one === two;
162}
163
164var testIsClass = function(obj, klass) {
165  return Object.prototype.toString.call(obj) === '[object ' + klass + ']';
166}
167
168var addFailure = function(actual, expected, message, stack, warning) {
169  var meta = getMeta(stack);
170  currentTest.failures.push({
171    actual: actual,
172    expected: expected,
173    message: message,
174    file: meta.file,
175    line: meta.line,
176    col: meta.col,
177    warning: !!warning
178  });
179}
180
181var getMeta = function(stack) {
182  var level = 4;
183  if(stack !== undefined) {
184    level += stack;
185  }
186  var e = new Error();
187  if(!e.stack) {
188    return {};
189  }
190  var s = e.stack.split(/@|^\s+at/m);
191  var match = s[level].match(/(.+\.js):(\d+)(?::(\d+))?/);
192  if(!match) match = [];
193  return { file: match[1], line: match[2], col: match[3] };
194}
195
196var checkCanFinish = function() {
197  if(!syncTestsRunning && asyncTestsRunning === 0) {
198    testsFinished();
199  }
200}
201
202var testsStarted = function() {
203  if(typeof testsStartedCallback != 'undefined') {
204    testsStartedCallback(results);
205  }
206  if(environment == 'node') {
207    console.info('\n----------------------- STARTING TESTS ----------------------------\n');
208  }
209  testStartTime = new Date();
210}
211
212var testsFinished = function() {
213  runtime = new Date() - testStartTime;
214  if(typeof testsFinishedCallback != 'undefined') {
215    testsFinishedCallback(results, runtime);
216  }
217  if(environment == 'node') {
218    this.totalFailures = 0
219    // displayResults will increment totalFailures by 1 for each failed test encountered
220    displayResults();
221    // will exit now setting the status to the number of failed tests
222    process.exit(this.totalFailures);
223  }
224  results = [];
225}
226
227var displayResults = function() {
228  var i, j, failure, totalAssertions = 0, totalFailures = 0;
229  for (i = 0; i < results.length; i += 1) {
230    totalAssertions += results[i].assertions;
231    this.totalFailures += results[i].failures.length;
232    for(j = 0; j < results[i].failures.length; j++) {
233      failure = results[i].failures[j];
234      console.info('\n'+ (j + 1) + ') Failure:');
235      console.info(failure.message);
236      console.info('Expected: ' + JSON.stringify(failure.expected) + ' but was: ' + JSON.stringify(failure.actual));
237      console.info('File: ' + failure.file + ', Line: ' + failure.line, ' Col: ' + failure.col + '\n');
238    }
239  };
240  var time = (runtime / 1000);
241  console.info(results.length + ' tests, ' + totalAssertions + ' assertions, ' + this.totalFailures + ' failures, ' + time + 's\n');
242}
243
244test = function(name, fn) {
245  if(moduleSetupMethod) {
246    moduleSetupMethod();
247  }
248  if(!results) {
249    results = [];
250    syncTestsRunning = true;
251    asyncTestsRunning = 0;
252    testsStarted();
253  }
254  currentTest = {
255    name: name,
256    assertions: 0,
257    failures: []
258  };
259  try {
260    fn.call();
261  } catch(e) {
262    console.info(e.stack);
263  }
264  results.push(currentTest);
265  if(moduleTeardownMethod) {
266    moduleTeardownMethod();
267  }
268}
269
270var removeCapturedTimer = function(remove) {
271  var result = [], timer;
272  for (var i = 0, len = capturedTimers.length; i < len; i++) {
273    timer = capturedTimers[i];
274    if(timer !== remove) {
275      result.push(timer);
276    }
277  };
278  capturedTimers = result;
279};
280
281testModule = function(name, options) {
282  moduleName = name;
283  moduleSetupMethod = options.setup;
284  moduleTeardownMethod = options.teardown;
285}
286
287equal = function(actual, expected, message, exceptions, stack) {
288  exceptions = exceptions || {};
289  if(environment in exceptions) {
290    expected = exceptions[environment];
291  }
292  currentTest.assertions++;
293  if(!testIsEqual(actual, expected)) {
294    addFailure(actual, expected, message, stack);
295  }
296}
297
298notEqual = function(actual, expected, message, exceptions) {
299  equal(actual !== expected, true, message + ' | strict equality', exceptions, 1);
300}
301
302equalWithWarning = function(expected, actual, message) {
303  if(expected != actual) {
304    addFailure(actual, expected, message, null, true);
305  }
306}
307
308equalWithMargin = function(actual, expected, margin, message) {
309  equal((actual > expected - margin) && (actual < expected + margin), true, message, null, 1);
310}
311
312// Array content is equal, but order may differ
313arrayEquivalent = function(a, b, message) {
314  equal(sortOnStringValue(a), sortOnStringValue(b), message);
315}
316
317raisesError = function(fn, message, exceptions) {
318  var raised = false;
319  try {
320    fn.call();
321  } catch(e) {
322    raised = true;
323  }
324  equal(raised, true, message, exceptions, 1);
325}
326
327skipEnvironments = function(environments, test) {
328  if(testArrayIndexOf(environments, environment) === -1) {
329    test.call();
330  }
331}
332
333syncTestsFinished = function() {
334  syncTestsRunning = false;
335  checkCanFinish();
336}
337
338// This method has 2 benefits:
339// 1. It gives asynchronous functions their own scope so vars can't be overwritten later by other asynchronous functions
340// 2. It runs the tests after the CPU is free decreasing the chance of timing based errors.
341async = function(fn) {
342  asyncTestsRunning++;
343  setTimeout(function() {
344    fn();
345  }, 100);
346}
347
348asyncFinished = function() {
349  asyncTestsRunning--;
350  checkCanFinish();
351}
352
353if(typeof console === 'undefined') {
354  var consoleFn = function() {
355    var messages = Array.prototype.slice.call(arguments);
356    messages = messages.map(function(arg) {
357      return String(arg);
358    })
359    $('<p/>').text(messages.join(',')).appendTo(document.body);
360  }
361  console = {
362    log: consoleFn,
363    info: consoleFn
364  }
365}