blob: 1b8f475e9842b927b520fa6b913659df5dff299d [file] [log] [blame]
Siobhan Tullye18b3442014-02-23 14:23:34 -05001// Next three methods are primarily for IE5, which is missing them
2if (!Array.prototype.push) {
3 Array.prototype.push = function() {
4 for (var i = 0; i < arguments.length; i++){
5 this[this.length] = arguments[i];
6 }
7 return this.length;
8 };
9}
10
11if (!Array.prototype.shift) {
12 Array.prototype.shift = function() {
13 if (this.length > 0) {
14 var firstItem = this[0];
15 for (var i = 0; i < this.length - 1; i++) {
16 this[i] = this[i + 1];
17 }
18 this.length = this.length - 1;
19 return firstItem;
20 }
21 };
22}
23
24if (!Function.prototype.apply) {
25 Function.prototype.apply = function(obj, args) {
26 var methodName = "__apply__";
27 if (typeof obj[methodName] != "undefined") {
28 methodName += (String(Math.random())).substr(2);
29 }
30 obj[methodName] = this;
31
32 var argsStrings = new Array(args.length);
33 for (var i = 0; i < args.length; i++) {
34 argsStrings[i] = "args[" + i + "]";
35 }
36 var script = "obj." + methodName + "(" + argsStrings.join(",") + ")";
37 var returnValue = eval(script);
38 delete obj[methodName];
39 return returnValue;
40 };
41}
42
43/* -------------------------------------------------------------------------- */
44
45var xn = new Object();
46
47(function() {
48 // Utility functions
49
50 // Event listeners
51 var getListenersPropertyName = function(eventName) {
52 return "__listeners__" + eventName;
53 };
54
55 var addEventListener = function(node, eventName, listener, useCapture) {
56 useCapture = Boolean(useCapture);
57 if (node.addEventListener) {
58 node.addEventListener(eventName, listener, useCapture);
59 } else if (node.attachEvent) {
60 node.attachEvent("on" + eventName, listener);
61 } else {
62 var propertyName = getListenersPropertyName(eventName);
63 if (!node[propertyName]) {
64 node[propertyName] = new Array();
65
66 // Set event handler
67 node["on" + eventName] = function(evt) {
68 evt = module.getEvent(evt);
69 var listenersPropertyName = getListenersPropertyName(eventName);
70
71 // Clone the array of listeners to leave the original untouched
72 var listeners = cloneArray(this[listenersPropertyName]);
73 var currentListener;
74
75 // Call each listener in turn
76 while (currentListener = listeners.shift()) {
77 currentListener.call(this, evt);
78 }
79 };
80 }
81 node[propertyName].push(listener);
82 }
83 };
84
85 // Clones an array
86 var cloneArray = function(arr) {
87 var clonedArray = [];
88 for (var i = 0; i < arr.length; i++) {
89 clonedArray[i] = arr[i];
90 }
91 return clonedArray;
92 }
93
94 var isFunction = function(f) {
95 if (!f){ return false; }
96 return (f instanceof Function || typeof f == "function");
97 };
98
99 // CSS Utilities
100
101 function array_contains(arr, val) {
102 for (var i = 0, len = arr.length; i < len; i++) {
103 if (arr[i] === val) {
104 return true;
105 }
106 }
107 return false;
108 }
109
110 function addClass(el, cssClass) {
111 if (!hasClass(el, cssClass)) {
112 if (el.className) {
113 el.className += " " + cssClass;
114 } else {
115 el.className = cssClass;
116 }
117 }
118 }
119
120 function hasClass(el, cssClass) {
121 if (el.className) {
122 var classNames = el.className.split(" ");
123 return array_contains(classNames, cssClass);
124 }
125 return false;
126 }
127
128 function removeClass(el, cssClass) {
129 if (hasClass(el, cssClass)) {
130 // Rebuild the className property
131 var existingClasses = el.className.split(" ");
132 var newClasses = [];
133 for (var i = 0; i < existingClasses.length; i++) {
134 if (existingClasses[i] != cssClass) {
135 newClasses[newClasses.length] = existingClasses[i];
136 }
137 }
138 el.className = newClasses.join(" ");
139 }
140 }
141
142 function replaceClass(el, newCssClass, oldCssClass) {
143 removeClass(el, oldCssClass);
144 addClass(el, newCssClass);
145 }
146
147 function getExceptionStringRep(ex) {
148 if (ex) {
149 var exStr = "Exception: ";
150 if (ex.message) {
151 exStr += ex.message;
152 } else if (ex.description) {
153 exStr += ex.description;
154 }
155 if (ex.lineNumber) {
156 exStr += " on line number " + ex.lineNumber;
157 }
158 if (ex.fileName) {
159 exStr += " in file " + ex.fileName;
160 }
161 return exStr;
162 }
163 return null;
164 }
165
166
167 /* ---------------------------------------------------------------------- */
168
169 /* Configure the test logger try to use FireBug */
170 var log, error;
171 if (window["console"] && typeof console.log == "function") {
172 log = function() {
173 if (xn.test.enableTestDebug) {
174 console.log.apply(console, arguments);
175 }
176 };
177 error = function() {
178 if (xn.test.enableTestDebug) {
179 console.error.apply(console, arguments);
180 }
181 };
182 } else {
183 log = function() {};
184 }
185
186 /* Set up something to report to */
187
188 var initialized = false;
189 var container;
190 var progressBarContainer, progressBar, overallSummaryText;
191 var currentTest = null;
192 var suites = [];
193 var totalTestCount = 0;
194 var currentTestIndex = 0;
195 var testFailed = false;
196 var testsPassedCount = 0;
197 var startTime;
198
199 var log4javascriptEnabled = false;
200
201 var nextSuiteIndex = 0;
202
203 function runNextSuite() {
204 if (nextSuiteIndex < suites.length) {
205 suites[nextSuiteIndex++].run();
206 }
207 }
208
209 var init = function() {
210 if (initialized) { return true; }
211
212 container = document.createElement("div");
213
214 // Create the overall progress bar
215 progressBarContainer = container.appendChild(document.createElement("div"));
216 progressBarContainer.className = "xn_test_progressbar_container xn_test_overallprogressbar_container";
217 progressBar = progressBarContainer.appendChild(document.createElement("div"));
218 progressBar.className = "success";
219
220 document.body.appendChild(container);
221
222 var h1 = progressBar.appendChild(document.createElement("h1"));
223 overallSummaryText = h1.appendChild(document.createTextNode(""));
224
225 initialized = true;
226
227 // Set up logging
228 log4javascriptEnabled = !!log4javascript && xn.test.enable_log4javascript;
229
230 function TestLogAppender() {}
231
232 if (log4javascriptEnabled) {
233 TestLogAppender.prototype = new log4javascript.Appender();
234 TestLogAppender.prototype.layout = new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %-5p %m");
235 TestLogAppender.prototype.append = function(loggingEvent) {
236 var formattedMessage = this.getLayout().format(loggingEvent);
237 if (this.getLayout().ignoresThrowable()) {
238 formattedMessage += loggingEvent.getThrowableStrRep();
239 }
240 currentTest.addLogMessage(formattedMessage);
241 };
242
243 var appender = new TestLogAppender();
244 appender.setThreshold(log4javascript.Level.ALL);
245 log4javascript.getRootLogger().addAppender(appender);
246 log4javascript.getRootLogger().setLevel(log4javascript.Level.ALL);
247 }
248
249 startTime = new Date();
250
251 // First, build each suite
252 for (var i = 0; i < suites.length; i++) {
253 suites[i].build();
254 totalTestCount += suites[i].tests.length;
255 }
256
257 // Now run each suite
258 runNextSuite();
259 };
260
261 function updateProgressBar() {
262 progressBar.style.width = "" + parseInt(100 * (currentTestIndex) / totalTestCount) + "%";
263 var s = (totalTestCount === 1) ? "" : "s";
264 var timeTaken = new Date().getTime() - startTime.getTime();
265 overallSummaryText.nodeValue = "" + testsPassedCount + " of " + totalTestCount + " test" + s + " passed in " + timeTaken + "ms";
266 }
267
268 addEventListener(window, "load", init);
269
270 /* ---------------------------------------------------------------------- */
271
272 /* Test Suite */
273 var Suite = function(name, callback, hideSuccessful) {
274 this.name = name;
275 this.callback = callback;
276 this.hideSuccessful = hideSuccessful;
277 this.tests = [];
278 this.log = log;
279 this.error = error;
280 this.expanded = true;
281 suites.push(this);
282 }
283
284 Suite.prototype.test = function(name, callback, setUp, tearDown) {
285 this.log("adding a test named " + name)
286 var t = new Test(name, callback, this, setUp, tearDown);
287 this.tests.push(t);
288 };
289
290 Suite.prototype.build = function() {
291 // Build the elements used by the suite
292 var suite = this;
293 this.testFailed = false;
294 this.container = document.createElement("div");
295 this.container.className = "xn_test_suite_container";
296
297 var heading = document.createElement("h2");
298 this.expander = document.createElement("span");
299 this.expander.className = "xn_test_expander";
300 this.expander.onclick = function() {
301 if (suite.expanded) {
302 suite.collapse();
303 } else {
304 suite.expand();
305 }
306 };
307 heading.appendChild(this.expander);
308
309 this.headingTextNode = document.createTextNode(this.name);
310 heading.appendChild(this.headingTextNode);
311 this.container.appendChild(heading);
312
313 this.reportContainer = document.createElement("dl");
314 this.container.appendChild(this.reportContainer);
315
316 this.progressBarContainer = document.createElement("div");
317 this.progressBarContainer.className = "xn_test_progressbar_container";
318 this.progressBar = document.createElement("div");
319 this.progressBar.className = "success";
320 this.progressBar.innerHTML = "&nbsp;";
321 this.progressBarContainer.appendChild(this.progressBar);
322 this.reportContainer.appendChild(this.progressBarContainer);
323
324 this.expand();
325
326 container.appendChild(this.container);
327
328 // invoke callback to build the tests
329 this.callback.apply(this, [this]);
330 };
331
332 Suite.prototype.run = function() {
333 this.log("running suite '%s'", this.name)
334 this.startTime = new Date();
335
336 // now run the first test
337 this._currentIndex = 0;
338 this.runNextTest();
339 };
340
341 Suite.prototype.updateProgressBar = function() {
342 // Update progress bar
343 this.progressBar.style.width = "" + parseInt(100 * (this._currentIndex) / this.tests.length) + "%";
344 //log(this._currentIndex + ", " + this.tests.length + ", " + progressBar.style.width + ", " + progressBar.className);
345 };
346
347 Suite.prototype.expand = function() {
348 this.expander.innerHTML = "-";
349 replaceClass(this.reportContainer, "xn_test_expanded", "xn_test_collapsed");
350 this.expanded = true;
351 };
352
353 Suite.prototype.collapse = function() {
354 this.expander.innerHTML = "+";
355 replaceClass(this.reportContainer, "xn_test_collapsed", "xn_test_expanded");
356 this.expanded = false;
357 };
358
359 Suite.prototype.finish = function(timeTaken) {
360 var newClass = this.testFailed ? "xn_test_suite_failure" : "xn_test_suite_success";
361 var oldClass = this.testFailed ? "xn_test_suite_success" : "xn_test_suite_failure";
362 replaceClass(this.container, newClass, oldClass);
363
364 this.headingTextNode.nodeValue += " (" + timeTaken + "ms)";
365
366 if (this.hideSuccessful && !this.testFailed) {
367 this.collapse();
368 }
369 runNextSuite();
370 };
371
372 /**
373 * Works recursively with external state (the next index)
374 * so that we can handle async tests differently
375 */
376 Suite.prototype.runNextTest = function() {
377 if (this._currentIndex == this.tests.length) {
378 // finished!
379 var timeTaken = new Date().getTime() - this.startTime.getTime();
380
381 this.finish(timeTaken);
382 return;
383 }
384
385 var suite = this;
386 var t = this.tests[this._currentIndex++];
387 currentTestIndex++;
388
389 if (isFunction(suite.setUp)) {
390 suite.setUp.apply(suite, [t]);
391 }
392 if (isFunction(t.setUp)) {
393 t.setUp.apply(t, [t]);
394 }
395
396 t._run();
397
398 function afterTest() {
399 if (isFunction(suite.tearDown)) {
400 suite.tearDown.apply(suite, [t]);
401 }
402 if (isFunction(t.tearDown)) {
403 t.tearDown.apply(t, [t]);
404 }
405 suite.log("finished test [%s]", t.name);
406 updateProgressBar();
407 suite.updateProgressBar();
408 suite.runNextTest();
409 }
410
411 if (t.isAsync) {
412 t.whenFinished = afterTest;
413 } else {
414 setTimeout(afterTest, 1);
415 }
416 };
417
418 Suite.prototype.reportSuccess = function() {
419 };
420
421 /* ---------------------------------------------------------------------- */
422 /**
423 * Create a new test
424 */
425 var Test = function(name, callback, suite, setUp, tearDown) {
426 this.name = name;
427 this.callback = callback;
428 this.suite = suite;
429 this.setUp = setUp;
430 this.tearDown = tearDown;
431 this.log = log;
432 this.error = error;
433 this.assertCount = 0;
434 this.logMessages = [];
435 this.logExpanded = false;
436 };
437
438 /**
439 * Default success reporter, please override
440 */
441 Test.prototype.reportSuccess = function(name, timeTaken) {
442 /* default success reporting handler */
443 this.reportHeading = document.createElement("dt");
444 var text = this.name + " passed in " + timeTaken + "ms";
445
446 this.reportHeading.appendChild(document.createTextNode(text));
447
448 this.reportHeading.className = "success";
449 var dd = document.createElement("dd");
450 dd.className = "success";
451
452 this.suite.reportContainer.appendChild(this.reportHeading);
453 this.suite.reportContainer.appendChild(dd);
454 this.createLogReport();
455 };
456
457 /**
458 * Cause the test to immediately fail
459 */
460 Test.prototype.reportFailure = function(name, msg, ex) {
461 this.suite.testFailed = true;
462 this.suite.progressBar.className = "failure";
463 progressBar.className = "failure";
464 this.reportHeading = document.createElement("dt");
465 this.reportHeading.className = "failure";
466 var text = document.createTextNode(this.name);
467 this.reportHeading.appendChild(text);
468
469 var dd = document.createElement("dd");
470 dd.appendChild(document.createTextNode(msg));
471 dd.className = "failure";
472
473 this.suite.reportContainer.appendChild(this.reportHeading);
474 this.suite.reportContainer.appendChild(dd);
475 if (ex && ex.stack) {
476 var stackTraceContainer = this.suite.reportContainer.appendChild(document.createElement("code"));
477 stackTraceContainer.className = "xn_test_stacktrace";
478 stackTraceContainer.innerHTML = ex.stack.replace(/\r/g, "\n").replace(/\n{1,2}/g, "<br />");
479 }
480 this.createLogReport();
481 };
482
483 Test.prototype.createLogReport = function() {
484 if (this.logMessages.length > 0) {
485 this.reportHeading.appendChild(document.createTextNode(" ("));
486 var logToggler = this.reportHeading.appendChild(document.createElement("a"));
487 logToggler.href = "#";
488 logToggler.innerHTML = "show log";
489 var test = this;
490
491 logToggler.onclick = function() {
492 if (test.logExpanded) {
493 test.hideLogReport();
494 this.innerHTML = "show log";
495 test.logExpanded = false;
496 } else {
497 test.showLogReport();
498 this.innerHTML = "hide log";
499 test.logExpanded = true;
500 }
501 return false;
502 };
503
504 this.reportHeading.appendChild(document.createTextNode(")"));
505
506 // Create log report
507 this.logReport = this.suite.reportContainer.appendChild(document.createElement("pre"));
508 this.logReport.style.display = "none";
509 this.logReport.className = "xn_test_log_report";
510 var logMessageDiv;
511 for (var i = 0, len = this.logMessages.length; i < len; i++) {
512 logMessageDiv = this.logReport.appendChild(document.createElement("div"));
513 logMessageDiv.appendChild(document.createTextNode(this.logMessages[i]));
514 }
515 }
516 };
517
518 Test.prototype.showLogReport = function() {
519 this.logReport.style.display = "inline-block";
520 };
521
522 Test.prototype.hideLogReport = function() {
523 this.logReport.style.display = "none";
524 };
525
526 Test.prototype.async = function(timeout, callback) {
527 timeout = timeout || 250;
528 var self = this;
529 var timedOutFunc = function() {
530 if (!self.completed) {
531 var message = (typeof callback === "undefined") ?
532 "Asynchronous test timed out" : callback(self);
533 self.fail(message);
534 }
535 }
536 var timer = setTimeout(function () { timedOutFunc.apply(self, []); }, timeout)
537 this.isAsync = true;
538 };
539
540 /**
541 * Run the test
542 */
543 Test.prototype._run = function() {
544 this.log("starting test [%s]", this.name);
545 this.startTime = new Date();
546 currentTest = this;
547 try {
548 this.callback(this);
549 if (!this.completed && !this.isAsync) {
550 this.succeed();
551 }
552 } catch (e) {
553 this.log("test [%s] threw exception [%s]", this.name, e);
554 var s = (this.assertCount === 1) ? "" : "s";
555 this.fail("Exception thrown after " + this.assertCount + " successful assertion" + s + ": " + getExceptionStringRep(e), e);
556 }
557 };
558
559 /**
560 * Cause the test to immediately succeed
561 */
562 Test.prototype.succeed = function() {
563 if (this.completed) { return false; }
564 // this.log("test [%s] succeeded", this.name);
565 this.completed = true;
566 var timeTaken = new Date().getTime() - this.startTime.getTime();
567 testsPassedCount++;
568 this.reportSuccess(this.name, timeTaken);
569 if (this.whenFinished) {
570 this.whenFinished();
571 }
572 };
573
574 Test.prototype.fail = function(msg, ex) {
575 if (typeof msg != "string") {
576 msg = getExceptionStringRep(msg);
577 }
578 if (this.completed) { return false; }
579 this.completed = true;
580 // this.log("test [%s] failed", this.name);
581 this.reportFailure(this.name, msg, ex);
582 if (this.whenFinished) {
583 this.whenFinished();
584 }
585 };
586
587 Test.prototype.addLogMessage = function(logMessage) {
588 this.logMessages.push(logMessage);
589 };
590
591 /* assertions */
592 var displayStringForValue = function(obj) {
593 if (obj === null) {
594 return "null";
595 } else if (typeof obj === "undefined") {
596 return "undefined";
597 }
598 return obj.toString();
599 };
600
601 var assert = function(args, expectedArgsCount, testFunction, defaultComment) {
602 this.assertCount++;
603 var comment = defaultComment;
604 var i;
605 var success;
606 var values = [];
607 if (args.length == expectedArgsCount) {
608 for (i = 0; i < args.length; i++) {
609 values[i] = args[i];
610 }
611 } else if (args.length == expectedArgsCount + 1) {
612 comment = args[0];
613 for (i = 1; i < args.length; i++) {
614 values[i - 1] = args[i];
615 }
616 } else {
617 throw new Error("Invalid number of arguments passed to assert function");
618 }
619 success = testFunction(values);
620 if (!success) {
621 var regex = /\{([0-9]+)\}/;
622 while (regex.test(comment)) {
623 comment = comment.replace(regex, displayStringForValue(values[parseInt(RegExp.$1)]));
624 }
625 this.fail("Test failed on assertion " + this.assertCount + ": " + comment);
626 }
627 };
628
629 var testNull = function(values) {
630 return (values[0] === null);
631 };
632
633 Test.prototype.assertNull = function() {
634 assert.apply(this, [arguments, 1, testNull, "Expected to be null but was {0}"]);
635 }
636
637 var testNotNull = function(values) {
638 return (values[0] !== null);
639 };
640
641 Test.prototype.assertNotNull = function() {
642 assert.apply(this, [arguments, 1, testNotNull, "Expected not to be null but was {0}"]);
643 }
644
645 var testBoolean = function(values) {
646 return (Boolean(values[0]));
647 };
648
649 Test.prototype.assert = function() {
650 assert.apply(this, [arguments, 1, testBoolean, "Expected not to be equivalent to false"]);
651 };
652
653 var testTrue = function(values) {
654 return (values[0] === true);
655 };
656
657 Test.prototype.assertTrue = function() {
658 assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
659 };
660
661 Test.prototype.assert = function() {
662 assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
663 };
664
665 var testFalse = function(values) {
666 return (values[0] === false);
667 };
668
669 Test.prototype.assertFalse = function() {
670 assert.apply(this, [arguments, 1, testFalse, "Expected to be false but was {0}"]);
671 }
672
673 var testEquivalent = function(values) {
674 return (values[0] === values[1]);
675 };
676
677 Test.prototype.assertEquivalent = function() {
678 assert.apply(this, [arguments, 2, testEquivalent, "Expected to be equal but values were {0} and {1}"]);
679 }
680
681 var testNotEquivalent = function(values) {
682 return (values[0] !== values[1]);
683 };
684
685 Test.prototype.assertNotEquivalent = function() {
686 assert.apply(this, [arguments, 2, testNotEquivalent, "Expected to be not equal but values were {0} and {1}"]);
687 }
688
689 var testEquals = function(values) {
690 return (values[0] == values[1]);
691 };
692
693 Test.prototype.assertEquals = function() {
694 assert.apply(this, [arguments, 2, testEquals, "Expected to be equal but values were {0} and {1}"]);
695 }
696
697 var testNotEquals = function(values) {
698 return (values[0] != values[1]);
699 };
700
701 Test.prototype.assertNotEquals = function() {
702 assert.apply(this, [arguments, 2, testNotEquals, "Expected to be not equal but values were {0} and {1}"]);
703 }
704
705 var testRegexMatches = function(values) {
706 return (values[0].test(values[1]));
707 };
708
709 Test.prototype.assertRegexMatches = function() {
710 assert.apply(this, [arguments, 2, testRegexMatches, "Expected regex {0} to match value {1} but it didn't"]);
711 }
712
713 Test.prototype.assertError = function(f, errorType) {
714 try {
715 f();
716 this.fail("Expected error to be thrown");
717 } catch (e) {
718 if (errorType && (!(e instanceof errorType))) {
719 this.fail("Expected error of type " + errorType + " to be thrown but error thrown was " + e);
720 }
721 }
722 };
723
724 /**
725 * Execute a synchronous test
726 */
727 xn.test = function(name, callback) {
728 xn.test.suite("Anonymous", function(s) {
729 s.test(name, callback);
730 });
731 }
732
733 /**
734 * Create a test suite with a given name
735 */
736 xn.test.suite = function(name, callback, hideSuccessful) {
737 var s = new Suite(name, callback, hideSuccessful);
738 }
739})();