master
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}