OLD | NEW |
| (Empty) |
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. | |
4 | |
5 'use strict'; | |
6 | |
7 /** | |
8 * @fileoverview A test harness loosely based on Python unittest, but that | |
9 * installs global assert methods during the test for backward compatibility | |
10 * with Closure tests. | |
11 */ | |
12 base.requireStylesheet('base.unittest'); | |
13 base.exportTo('base.unittest', function() { | |
14 | |
15 var NOCATCH_MODE = false; | |
16 | |
17 // Uncomment the line below to make unit test failures throw exceptions. | |
18 NOCATCH_MODE = true; | |
19 | |
20 function createTestCaseDiv(testName, opt_href, opt_alwaysShowErrorLink) { | |
21 var el = document.createElement('test-case'); | |
22 | |
23 var titleBlockEl = document.createElement('title'); | |
24 titleBlockEl.style.display = 'inline'; | |
25 el.appendChild(titleBlockEl); | |
26 | |
27 var titleEl = document.createElement('span'); | |
28 titleEl.style.marginRight = '20px'; | |
29 titleBlockEl.appendChild(titleEl); | |
30 | |
31 var errorLink = document.createElement('a'); | |
32 errorLink.textContent = 'Run individually...'; | |
33 if (opt_href) | |
34 errorLink.href = opt_href; | |
35 else | |
36 errorLink.href = '#' + testName; | |
37 errorLink.style.display = 'none'; | |
38 titleBlockEl.appendChild(errorLink); | |
39 | |
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 = ''; | |
45 else | |
46 errorLink.style.display = 'none'; | |
47 }); | |
48 | |
49 el.addError = function(test, e) { | |
50 var errorEl = createErrorDiv(test, e); | |
51 el.appendChild(errorEl); | |
52 return errorEl; | |
53 }; | |
54 | |
55 el.addHTMLOutput = function(opt_title, opt_element) { | |
56 var outputEl = createOutputDiv(opt_title, opt_element); | |
57 el.appendChild(outputEl); | |
58 return outputEl.contents; | |
59 }; | |
60 | |
61 el.status = 'READY'; | |
62 return el; | |
63 } | |
64 | |
65 function createErrorDiv(test, e) { | |
66 var el = document.createElement('test-case-error'); | |
67 el.className = 'unittest-error'; | |
68 | |
69 var stackEl = document.createElement('test-case-stack'); | |
70 if (typeof e == 'string') { | |
71 stackEl.textContent = e; | |
72 } else if (e.stack) { | |
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; | |
79 } else { | |
80 stackEl.textContent = e; | |
81 } | |
82 el.appendChild(stackEl); | |
83 return el; | |
84 } | |
85 | |
86 function createOutputDiv(opt_title, opt_element) { | |
87 var el = document.createElement('test-case-output'); | |
88 if (opt_title) { | |
89 var titleEl = document.createElement('div'); | |
90 titleEl.textContent = opt_title; | |
91 el.appendChild(titleEl); | |
92 } | |
93 var contentEl = opt_element || document.createElement('div'); | |
94 el.appendChild(contentEl); | |
95 | |
96 el.__defineGetter__('contents', function() { | |
97 return contentEl; | |
98 }); | |
99 return el; | |
100 } | |
101 | |
102 function statusToClassName(status) { | |
103 if (status == 'PASSED') | |
104 return 'unittest-green'; | |
105 else if (status == 'RUNNING' || status == 'READY') | |
106 return 'unittest-yellow'; | |
107 else | |
108 return 'unittest-red'; | |
109 } | |
110 | |
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'); | |
119 | |
120 el.classList.add(newClass); | |
121 } | |
122 | |
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); | |
130 return outputEl; | |
131 } | |
132 | |
133 HTMLTestRunner.prototype = { | |
134 __proto__: HTMLDivElement.prototype, | |
135 | |
136 decorate: function(opt_title, opt_curHash) { | |
137 this.running = false; | |
138 | |
139 this.currentTest_ = undefined; | |
140 this.results = undefined; | |
141 if (opt_curHash) { | |
142 var trimmedHash = opt_curHash.substring(1); | |
143 this.filterFunc_ = function(testName) { | |
144 return testName.indexOf(trimmedHash) == 0; | |
145 }; | |
146 } else | |
147 this.filterFunc_ = function(testName) { return true; }; | |
148 | |
149 this.statusEl_ = document.createElement('title'); | |
150 this.appendChild(this.statusEl_); | |
151 | |
152 this.resultsEl_ = document.createElement('div'); | |
153 this.appendChild(this.resultsEl_); | |
154 | |
155 this.title_ = opt_title || document.title; | |
156 | |
157 this.updateStatus(); | |
158 }, | |
159 | |
160 computeResultStats: function() { | |
161 var numTestsRun = 0; | |
162 var numTestsPassed = 0; | |
163 var numTestsWithErrors = 0; | |
164 if (this.results) { | |
165 for (var i = 0; i < this.results.length; i++) { | |
166 numTestsRun++; | |
167 if (this.results[i].errors.length) | |
168 numTestsWithErrors++; | |
169 else | |
170 numTestsPassed++; | |
171 } | |
172 } | |
173 return { | |
174 numTestsRun: numTestsRun, | |
175 numTestsPassed: numTestsPassed, | |
176 numTestsWithErrors: numTestsWithErrors | |
177 }; | |
178 }, | |
179 | |
180 updateStatus: function() { | |
181 var stats = this.computeResultStats(); | |
182 var status; | |
183 if (!this.results) { | |
184 status = 'READY'; | |
185 } else if (this.running) { | |
186 status = 'RUNNING'; | |
187 } else { | |
188 if (stats.numTestsRun && stats.numTestsWithErrors == 0) | |
189 status = 'PASSED'; | |
190 else | |
191 status = 'FAILED'; | |
192 } | |
193 | |
194 updateClassListGivenStatus(this.statusEl_, status); | |
195 this.statusEl_.textContent = this.title_ + ' [' + status + ']'; | |
196 }, | |
197 | |
198 get done() { | |
199 return this.results && this.running == false; | |
200 }, | |
201 | |
202 run: function(tests) { | |
203 this.results = []; | |
204 this.running = true; | |
205 this.updateStatus(); | |
206 for (var i = 0; i < tests.length; i++) { | |
207 if (!this.filterFunc_(tests[i].testName)) | |
208 continue; | |
209 tests[i].run(this); | |
210 this.updateStatus(); | |
211 } | |
212 this.running = false; | |
213 this.updateStatus(); | |
214 }, | |
215 | |
216 willRunTest: function(test) { | |
217 this.currentTest_ = test; | |
218 this.currentResults_ = {testName: test.testName, | |
219 errors: []}; | |
220 this.results.push(this.currentResults_); | |
221 | |
222 this.currentTestCaseEl_ = createTestCaseDiv(test.testName); | |
223 this.currentTestCaseEl_.status = 'RUNNING'; | |
224 this.resultsEl_.appendChild(this.currentTestCaseEl_); | |
225 }, | |
226 | |
227 /** | |
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 | |
232 * created. | |
233 */ | |
234 addHTMLOutput: function(opt_title, opt_element) { | |
235 return this.currentTestCaseEl_.addHTMLOutput(opt_title, opt_element); | |
236 }, | |
237 | |
238 addError: function(e) { | |
239 this.currentResults_.errors.push(e); | |
240 return this.currentTestCaseEl_.addError(this.currentTest_, e); | |
241 }, | |
242 | |
243 didRunTest: function(test) { | |
244 if (!this.currentResults_.errors.length) | |
245 this.currentTestCaseEl_.status = 'PASSED'; | |
246 else | |
247 this.currentTestCaseEl_.status = 'FAILED'; | |
248 | |
249 this.currentResults_ = undefined; | |
250 this.currentTest_ = undefined; | |
251 } | |
252 }; | |
253 | |
254 function TestError(opt_message) { | |
255 var that = new Error(opt_message); | |
256 Error.captureStackTrace(that, TestError); | |
257 that.__proto__ = TestError.prototype; | |
258 return that; | |
259 } | |
260 | |
261 TestError.prototype = { | |
262 __proto__: Error.prototype | |
263 }; | |
264 | |
265 /* | |
266 * @constructor TestCase | |
267 */ | |
268 function TestCase(testMethod, opt_testMethodName) { | |
269 if (!testMethod) | |
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.'); | |
274 | |
275 this.testMethod_ = testMethod; | |
276 this.testMethodName_ = opt_testMethodName || testMethod.name; | |
277 this.results_ = undefined; | |
278 }; | |
279 | |
280 function forAllAssertAndEnsureMethodsIn_(prototype, fn) { | |
281 for (var fieldName in prototype) { | |
282 if (fieldName.indexOf('assert') != 0 && | |
283 fieldName.indexOf('ensure') != 0) | |
284 continue; | |
285 var fieldValue = prototype[fieldName]; | |
286 if (typeof fieldValue != 'function') | |
287 continue; | |
288 fn(fieldName, fieldValue); | |
289 } | |
290 } | |
291 | |
292 TestCase.prototype = { | |
293 __proto__: Object.prototype, | |
294 | |
295 get testName() { | |
296 return this.testMethodName_; | |
297 }, | |
298 | |
299 bindGlobals_: function() { | |
300 forAllAssertAndEnsureMethodsIn_(TestCase.prototype, | |
301 function(fieldName, fieldValue) { | |
302 global[fieldName] = fieldValue.bind(this); | |
303 }); | |
304 }, | |
305 | |
306 unbindGlobals_: function() { | |
307 forAllAssertAndEnsureMethodsIn_(TestCase.prototype, | |
308 function(fieldName, fieldValue) { | |
309 delete global[fieldName]; | |
310 }); | |
311 }, | |
312 | |
313 /** | |
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 | |
318 * created. | |
319 */ | |
320 addHTMLOutput: function(opt_title, opt_element) { | |
321 return this.results_.addHTMLOutput(opt_title, opt_element); | |
322 }, | |
323 | |
324 assertTrue: function(a, opt_message) { | |
325 if (a) | |
326 return; | |
327 var message = opt_message || 'Expected true, got ' + a; | |
328 throw new TestError(message); | |
329 }, | |
330 | |
331 assertFalse: function(a, opt_message) { | |
332 if (!a) | |
333 return; | |
334 var message = opt_message || 'Expected false, got ' + a; | |
335 throw new TestError(message); | |
336 }, | |
337 | |
338 assertUndefined: function(a, opt_message) { | |
339 if (a === undefined) | |
340 return; | |
341 var message = opt_message || 'Expected undefined, got ' + a; | |
342 throw new TestError(message); | |
343 }, | |
344 | |
345 assertNotUndefined: function(a, opt_message) { | |
346 if (a !== undefined) | |
347 return; | |
348 var message = opt_message || 'Expected not undefined, got ' + a; | |
349 throw new TestError(message); | |
350 }, | |
351 | |
352 assertNull: function(a, opt_message) { | |
353 if (a === null) | |
354 return; | |
355 var message = opt_message || 'Expected null, got ' + a; | |
356 throw new TestError(message); | |
357 }, | |
358 | |
359 assertNotNull: function(a, opt_message) { | |
360 if (a !== null) | |
361 return; | |
362 var message = opt_message || 'Expected non-null, got ' + a; | |
363 throw new TestError(message); | |
364 }, | |
365 | |
366 assertEquals: function(a, b, opt_message) { | |
367 if (a == b) | |
368 return; | |
369 var message = opt_message || 'Expected ' + a + ', got ' + b; | |
370 throw new TestError(message); | |
371 }, | |
372 | |
373 assertNotEquals: function(a, b, opt_message) { | |
374 if (a != b) | |
375 return; | |
376 var message = opt_message || 'Expected something not equal to ' + b; | |
377 throw new TestError(message); | |
378 }, | |
379 | |
380 assertArrayEquals: function(a, b, opt_message) { | |
381 if (a.length == b.length) { | |
382 var ok = true; | |
383 for (var i = 0; i < a.length; i++) { | |
384 ok &= a[i] === b[i]; | |
385 } | |
386 if (ok) | |
387 return; | |
388 } | |
389 | |
390 var message = opt_message || 'Expected array ' + a + ', got array ' + b; | |
391 throw new TestError(message); | |
392 }, | |
393 | |
394 assertArrayShallowEquals: function(a, b, opt_message) { | |
395 if (a.length == b.length) { | |
396 var ok = true; | |
397 for (var i = 0; i < a.length; i++) { | |
398 ok &= a[i] === b[i]; | |
399 } | |
400 if (ok) | |
401 return; | |
402 } | |
403 | |
404 var message = opt_message || 'Expected array ' + b + ', got array ' + a; | |
405 throw new TestError(message); | |
406 }, | |
407 | |
408 assertAlmostEquals: function(a, b, opt_message) { | |
409 if (Math.abs(a - b) < 0.00001) | |
410 return; | |
411 var message = opt_message || 'Expected almost ' + a + ', got ' + b; | |
412 throw new TestError(message); | |
413 }, | |
414 | |
415 assertThrows: function(fn, opt_message) { | |
416 try { | |
417 fn(); | |
418 } catch (e) { | |
419 return; | |
420 } | |
421 var message = opt_message || 'Expected throw from ' + fn; | |
422 throw new TestError(message); | |
423 }, | |
424 | |
425 setUp: function() { | |
426 }, | |
427 | |
428 run: function(results) { | |
429 this.bindGlobals_(); | |
430 try { | |
431 this.results_ = results; | |
432 results.willRunTest(this); | |
433 | |
434 if (NOCATCH_MODE) { | |
435 this.setUp(); | |
436 this.testMethod_(); | |
437 this.tearDown(); | |
438 } else { | |
439 // Set up. | |
440 try { | |
441 this.setUp(); | |
442 } catch (e) { | |
443 results.addError(e); | |
444 return; | |
445 } | |
446 | |
447 // Run. | |
448 try { | |
449 this.testMethod_(); | |
450 } catch (e) { | |
451 results.addError(e); | |
452 } | |
453 | |
454 // Tear down. | |
455 try { | |
456 this.tearDown(); | |
457 } catch (e) { | |
458 if (typeof e == 'string') | |
459 e = new TestError(e); | |
460 results.addError(e); | |
461 } | |
462 } | |
463 } finally { | |
464 this.unbindGlobals_(); | |
465 results.didRunTest(this); | |
466 this.results_ = undefined; | |
467 } | |
468 }, | |
469 | |
470 tearDown: function() { | |
471 } | |
472 | |
473 }; | |
474 | |
475 /** | |
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. | |
479 * | |
480 * @param {object} opt_objectToEnumerate The object to enumerate, or global if | |
481 * not specified. | |
482 * @param {RegExp} opt_filter Return only tests that match this regexp. | |
483 */ | |
484 function discoverTests(opt_objectToEnumerate, opt_filter) { | |
485 var objectToEnumerate = opt_objectToEnumerate || global; | |
486 | |
487 var tests = []; | |
488 for (var testMethodName in objectToEnumerate) { | |
489 if (testMethodName.search(/^test.+/) != 0) | |
490 continue; | |
491 | |
492 if (opt_filter && testMethodName.search(opt_filter) == -1) | |
493 continue; | |
494 | |
495 var testMethod = objectToEnumerate[testMethodName]; | |
496 if (typeof testMethod != 'function') | |
497 continue; | |
498 var testCase = new TestCase(testMethod, testMethodName); | |
499 tests.push(testCase); | |
500 } | |
501 tests.sort(function(a, b) { | |
502 return a.testName < b.testName; | |
503 }); | |
504 return tests; | |
505 } | |
506 | |
507 /** | |
508 * Runs all unit tests. | |
509 */ | |
510 function runAllTests(opt_objectToEnumerate) { | |
511 var runner; | |
512 function init() { | |
513 if (runner) | |
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 | |
517 // can get to it. | |
518 global.G_testRunner = runner; | |
519 } | |
520 | |
521 function append() { | |
522 document.body.appendChild(runner); | |
523 } | |
524 | |
525 function run() { | |
526 var objectToEnumerate = opt_objectToEnumerate || global; | |
527 var tests = discoverTests(objectToEnumerate); | |
528 runner.run(tests); | |
529 } | |
530 | |
531 global.addEventListener('hashchange', function() { | |
532 init(); | |
533 append(); | |
534 run(); | |
535 }); | |
536 | |
537 init(); | |
538 if (document.body) | |
539 append(); | |
540 else | |
541 document.addEventListener('DOMContentLoaded', append); | |
542 global.addEventListener('load', run); | |
543 } | |
544 | |
545 if (/_test.html$/.test(document.location.pathname)) | |
546 runAllTests(); | |
547 | |
548 return { | |
549 HTMLTestRunner: HTMLTestRunner, | |
550 TestError: TestError, | |
551 TestCase: TestCase, | |
552 discoverTests: discoverTests, | |
553 runAllTests: runAllTests, | |
554 createErrorDiv_: createErrorDiv, | |
555 createTestCaseDiv_: createTestCaseDiv | |
556 }; | |
557 }); | |
OLD | NEW |