1 // Copyright (c) 2012 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.
8 * @fileoverview A test harness loosely based on Python unittest, but that
9 * installs global assert methods during the test for backward compatibility
12 base
.requireStylesheet('base.unittest');
13 base
.exportTo('base.unittest', function() {
15 var NOCATCH_MODE
= false;
17 // Uncomment the line below to make unit test failures throw exceptions.
20 function createTestCaseDiv(testName
, opt_href
, opt_alwaysShowErrorLink
) {
21 var el
= document
.createElement('test-case');
23 var titleBlockEl
= document
.createElement('title');
24 titleBlockEl
.style
.display
= 'inline';
25 el
.appendChild(titleBlockEl
);
27 var titleEl
= document
.createElement('span');
28 titleEl
.style
.marginRight
= '20px';
29 titleBlockEl
.appendChild(titleEl
);
31 var errorLink
= document
.createElement('a');
32 errorLink
.textContent
= 'Run individually...';
34 errorLink
.href
= opt_href
;
36 errorLink
.href
= '#' + testName
;
37 errorLink
.style
.display
= 'none';
38 titleBlockEl
.appendChild(errorLink
);
40 el
.__defineSetter__('status', function(status
) {
41 titleEl
.textContent
= testName
+ ': ' + status
;
42 updateClassListGivenStatus(titleEl
, status
);
43 if (status
== 'FAILED' || opt_alwaysShowErrorLink
)
44 errorLink
.style
.display
= '';
46 errorLink
.style
.display
= 'none';
49 el
.addError = function(test
, e
) {
50 var errorEl
= createErrorDiv(test
, e
);
51 el
.appendChild(errorEl
);
55 el
.addHTMLOutput = function(opt_title
, opt_element
) {
56 var outputEl
= createOutputDiv(opt_title
, opt_element
);
57 el
.appendChild(outputEl
);
58 return outputEl
.contents
;
65 function createErrorDiv(test
, e
) {
66 var el
= document
.createElement('test-case-error');
67 el
.className
= 'unittest-error';
69 var stackEl
= document
.createElement('test-case-stack');
70 if (typeof e
== 'string') {
71 stackEl
.textContent
= e
;
73 var i
= document
.location
.pathname
.lastIndexOf('/');
74 var path
= document
.location
.origin
+
75 document
.location
.pathname
.substring(0, i
);
76 var pathEscaped
= path
.replace(/[-/\\^$*+?.()|[\]{}]/g
, '\\$&');
77 var cleanStack
= e
.stack
.replace(new RegExp(pathEscaped
, 'g'), '.');
78 stackEl
.textContent
= cleanStack
;
80 stackEl
.textContent
= e
;
82 el
.appendChild(stackEl
);
86 function createOutputDiv(opt_title
, opt_element
) {
87 var el
= document
.createElement('test-case-output');
89 var titleEl
= document
.createElement('div');
90 titleEl
.textContent
= opt_title
;
91 el
.appendChild(titleEl
);
93 var contentEl
= opt_element
|| document
.createElement('div');
94 el
.appendChild(contentEl
);
96 el
.__defineGetter__('contents', function() {
102 function statusToClassName(status
) {
103 if (status
== 'PASSED')
104 return 'unittest-green';
105 else if (status
== 'RUNNING' || status
== 'READY')
106 return 'unittest-yellow';
108 return 'unittest-red';
111 function updateClassListGivenStatus(el
, status
) {
112 var newClass
= statusToClassName(status
);
113 if (newClass
!= 'unittest-green')
114 el
.classList
.remove('unittest-green');
115 if (newClass
!= 'unittest-yellow')
116 el
.classList
.remove('unittest-yellow');
117 if (newClass
!= 'unittest-red')
118 el
.classList
.remove('unittest-red');
120 el
.classList
.add(newClass
);
123 function HTMLTestRunner(opt_title
, opt_curHash
) {
124 // This constructs a HTMLDivElement and then adds our own runner methods to
125 // it. This is usually done via ui.js' define system, but we dont want our
126 // test runner to be dependent on the UI lib. :)
127 var outputEl
= document
.createElement('unittest-test-runner');
128 outputEl
.__proto__
= HTMLTestRunner
.prototype;
129 this.decorate
.call(outputEl
, opt_title
, opt_curHash
);
133 HTMLTestRunner
.prototype = {
134 __proto__
: HTMLDivElement
.prototype,
136 decorate: function(opt_title
, opt_curHash
) {
137 this.running
= false;
139 this.currentTest_
= undefined;
140 this.results
= undefined;
142 var trimmedHash
= opt_curHash
.substring(1);
143 this.filterFunc_ = function(testName
) {
144 return testName
.indexOf(trimmedHash
) == 0;
147 this.filterFunc_ = function(testName
) { return true; };
149 this.statusEl_
= document
.createElement('title');
150 this.appendChild(this.statusEl_
);
152 this.resultsEl_
= document
.createElement('div');
153 this.appendChild(this.resultsEl_
);
155 this.title_
= opt_title
|| document
.title
;
160 computeResultStats: function() {
162 var numTestsPassed
= 0;
163 var numTestsWithErrors
= 0;
165 for (var i
= 0; i
< this.results
.length
; i
++) {
167 if (this.results
[i
].errors
.length
)
168 numTestsWithErrors
++;
174 numTestsRun
: numTestsRun
,
175 numTestsPassed
: numTestsPassed
,
176 numTestsWithErrors
: numTestsWithErrors
180 updateStatus: function() {
181 var stats
= this.computeResultStats();
185 } else if (this.running
) {
188 if (stats
.numTestsRun
&& stats
.numTestsWithErrors
== 0)
194 updateClassListGivenStatus(this.statusEl_
, status
);
195 this.statusEl_
.textContent
= this.title_
+ ' [' + status
+ ']';
199 return this.results
&& this.running
== false;
202 run: function(tests
) {
206 for (var i
= 0; i
< tests
.length
; i
++) {
207 if (!this.filterFunc_(tests
[i
].testName
))
212 this.running
= false;
216 willRunTest: function(test
) {
217 this.currentTest_
= test
;
218 this.currentResults_
= {testName
: test
.testName
,
220 this.results
.push(this.currentResults_
);
222 this.currentTestCaseEl_
= createTestCaseDiv(test
.testName
);
223 this.currentTestCaseEl_
.status
= 'RUNNING';
224 this.resultsEl_
.appendChild(this.currentTestCaseEl_
);
228 * Adds some html content to the currently running test
229 * @param {String} opt_title The title for the output.
230 * @param {HTMLElement} opt_element The element to add. If not added, then.
231 * @return {HTMLElement} The element added, or if !opt_element, the element
234 addHTMLOutput: function(opt_title
, opt_element
) {
235 return this.currentTestCaseEl_
.addHTMLOutput(opt_title
, opt_element
);
238 addError: function(e
) {
239 this.currentResults_
.errors
.push(e
);
240 return this.currentTestCaseEl_
.addError(this.currentTest_
, e
);
243 didRunTest: function(test
) {
244 if (!this.currentResults_
.errors
.length
)
245 this.currentTestCaseEl_
.status
= 'PASSED';
247 this.currentTestCaseEl_
.status
= 'FAILED';
249 this.currentResults_
= undefined;
250 this.currentTest_
= undefined;
254 function TestError(opt_message
) {
255 var that
= new Error(opt_message
);
256 Error
.captureStackTrace(that
, TestError
);
257 that
.__proto__
= TestError
.prototype;
261 TestError
.prototype = {
262 __proto__
: Error
.prototype
266 * @constructor TestCase
268 function TestCase(testMethod
, opt_testMethodName
) {
270 throw new Error('testMethod must be provided');
271 if (testMethod
.name
== '' && !opt_testMethodName
)
272 throw new Error('testMethod must have a name, ' +
273 'or opt_testMethodName must be provided.');
275 this.testMethod_
= testMethod
;
276 this.testMethodName_
= opt_testMethodName
|| testMethod
.name
;
277 this.results_
= undefined;
280 function forAllAssertAndEnsureMethodsIn_(prototype, fn
) {
281 for (var fieldName
in prototype) {
282 if (fieldName
.indexOf('assert') != 0 &&
283 fieldName
.indexOf('ensure') != 0)
285 var fieldValue
= prototype[fieldName
];
286 if (typeof fieldValue
!= 'function')
288 fn(fieldName
, fieldValue
);
292 TestCase
.prototype = {
293 __proto__
: Object
.prototype,
296 return this.testMethodName_
;
299 bindGlobals_: function() {
300 forAllAssertAndEnsureMethodsIn_(TestCase
.prototype,
301 function(fieldName
, fieldValue
) {
302 global
[fieldName
] = fieldValue
.bind(this);
306 unbindGlobals_: function() {
307 forAllAssertAndEnsureMethodsIn_(TestCase
.prototype,
308 function(fieldName
, fieldValue
) {
309 delete global
[fieldName
];
314 * Adds some html content to the currently running test
315 * @param {String} opt_title The title for the output.
316 * @param {HTMLElement} opt_element The element to add. If not added, then.
317 * @return {HTMLElement} The element added, or if !opt_element, the element
320 addHTMLOutput: function(opt_title
, opt_element
) {
321 return this.results_
.addHTMLOutput(opt_title
, opt_element
);
324 assertTrue: function(a
, opt_message
) {
327 var message
= opt_message
|| 'Expected true, got ' + a
;
328 throw new TestError(message
);
331 assertFalse: function(a
, opt_message
) {
334 var message
= opt_message
|| 'Expected false, got ' + a
;
335 throw new TestError(message
);
338 assertUndefined: function(a
, opt_message
) {
341 var message
= opt_message
|| 'Expected undefined, got ' + a
;
342 throw new TestError(message
);
345 assertNotUndefined: function(a
, opt_message
) {
348 var message
= opt_message
|| 'Expected not undefined, got ' + a
;
349 throw new TestError(message
);
352 assertNull: function(a
, opt_message
) {
355 var message
= opt_message
|| 'Expected null, got ' + a
;
356 throw new TestError(message
);
359 assertNotNull: function(a
, opt_message
) {
362 var message
= opt_message
|| 'Expected non-null, got ' + a
;
363 throw new TestError(message
);
366 assertEquals: function(a
, b
, opt_message
) {
369 var message
= opt_message
|| 'Expected ' + a
+ ', got ' + b
;
370 throw new TestError(message
);
373 assertNotEquals: function(a
, b
, opt_message
) {
376 var message
= opt_message
|| 'Expected something not equal to ' + b
;
377 throw new TestError(message
);
380 assertArrayEquals: function(a
, b
, opt_message
) {
381 if (a
.length
== b
.length
) {
383 for (var i
= 0; i
< a
.length
; i
++) {
390 var message
= opt_message
|| 'Expected array ' + a
+ ', got array ' + b
;
391 throw new TestError(message
);
394 assertArrayShallowEquals: function(a
, b
, opt_message
) {
395 if (a
.length
== b
.length
) {
397 for (var i
= 0; i
< a
.length
; i
++) {
404 var message
= opt_message
|| 'Expected array ' + b
+ ', got array ' + a
;
405 throw new TestError(message
);
408 assertAlmostEquals: function(a
, b
, opt_message
) {
409 if (Math
.abs(a
- b
) < 0.00001)
411 var message
= opt_message
|| 'Expected almost ' + a
+ ', got ' + b
;
412 throw new TestError(message
);
415 assertThrows: function(fn
, opt_message
) {
421 var message
= opt_message
|| 'Expected throw from ' + fn
;
422 throw new TestError(message
);
428 run: function(results
) {
431 this.results_
= results
;
432 results
.willRunTest(this);
458 if (typeof e
== 'string')
459 e
= new TestError(e
);
464 this.unbindGlobals_();
465 results
.didRunTest(this);
466 this.results_
= undefined;
470 tearDown: function() {
476 * Returns an array of TestCase objects correpsonding to the tests
477 * found in the given object. This considers any functions beginning with test
478 * as a potential test.
480 * @param {object} opt_objectToEnumerate The object to enumerate, or global if
482 * @param {RegExp} opt_filter Return only tests that match this regexp.
484 function discoverTests(opt_objectToEnumerate
, opt_filter
) {
485 var objectToEnumerate
= opt_objectToEnumerate
|| global
;
488 for (var testMethodName
in objectToEnumerate
) {
489 if (testMethodName
.search(/^test.+/) != 0)
492 if (opt_filter
&& testMethodName
.search(opt_filter
) == -1)
495 var testMethod
= objectToEnumerate
[testMethodName
];
496 if (typeof testMethod
!= 'function')
498 var testCase
= new TestCase(testMethod
, testMethodName
);
499 tests
.push(testCase
);
501 tests
.sort(function(a
, b
) {
502 return a
.testName
< b
.testName
;
508 * Runs all unit tests.
510 function runAllTests(opt_objectToEnumerate
) {
514 runner
.parentElement
.removeChild(runner
);
515 runner
= new HTMLTestRunner(document
.title
, document
.location
.hash
);
516 // Stash the runner on global so that the global test runner
518 global
.G_testRunner
= runner
;
522 document
.body
.appendChild(runner
);
526 var objectToEnumerate
= opt_objectToEnumerate
|| global
;
527 var tests
= discoverTests(objectToEnumerate
);
531 global
.addEventListener('hashchange', function() {
541 document
.addEventListener('DOMContentLoaded', append
);
542 global
.addEventListener('load', run
);
545 if (/_test.html$/.test(document
.location
.pathname
))
549 HTMLTestRunner
: HTMLTestRunner
,
550 TestError
: TestError
,
552 discoverTests
: discoverTests
,
553 runAllTests
: runAllTests
,
554 createErrorDiv_
: createErrorDiv
,
555 createTestCaseDiv_
: createTestCaseDiv