| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * @fileoverview A test harness loosely based on Python unittest, but that |
| * installs global assert methods during the test for backward compatibility |
| * with Closure tests. |
| */ |
| base.requireStylesheet('unittest'); |
| base.exportTo('unittest', function() { |
| |
| function createTestCaseDiv(testName, opt_href, opt_alwaysShowErrorLink) { |
| var el = document.createElement('test-case'); |
| |
| var titleBlockEl = document.createElement('title'); |
| titleBlockEl.style.display = 'inline'; |
| el.appendChild(titleBlockEl); |
| |
| var titleEl = document.createElement('span'); |
| titleEl.style.marginRight = '20px'; |
| titleBlockEl.appendChild(titleEl); |
| |
| var errorLink = document.createElement('a'); |
| errorLink.textContent = 'Run individually...'; |
| if (opt_href) |
| errorLink.href = opt_href; |
| else |
| errorLink.href = '#' + testName; |
| errorLink.style.display = 'none'; |
| titleBlockEl.appendChild(errorLink); |
| |
| el.__defineSetter__('status', function(status) { |
| titleEl.textContent = testName + ': ' + status; |
| updateClassListGivenStatus(titleEl, status); |
| if (status == 'FAILED' || opt_alwaysShowErrorLink) |
| errorLink.style.display = ''; |
| else |
| errorLink.style.display = 'none'; |
| }); |
| |
| el.addError = function(test, e) { |
| var errorEl = createErrorDiv(test, e); |
| el.appendChild(errorEl); |
| return errorEl; |
| }; |
| |
| el.addHTMLOutput = function(opt_title, opt_element) { |
| var outputEl = createOutputDiv(opt_title, opt_element); |
| el.appendChild(outputEl); |
| return outputEl.contents; |
| }; |
| |
| el.status = 'READY'; |
| return el; |
| } |
| |
| function createErrorDiv(test, e) { |
| var el = document.createElement('test-case-error'); |
| el.className = 'unittest-error'; |
| |
| var stackEl = document.createElement('test-case-stack'); |
| if (typeof e == 'string') { |
| stackEl.textContent = e; |
| } else if (e.stack) { |
| var i = document.location.pathname.lastIndexOf('/'); |
| var path = document.location.origin + |
| document.location.pathname.substring(0, i); |
| var pathEscaped = path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); |
| var cleanStack = e.stack.replace(new RegExp(pathEscaped, 'g'), '.'); |
| stackEl.textContent = cleanStack; |
| } else { |
| stackEl.textContent = e; |
| } |
| el.appendChild(stackEl); |
| return el; |
| } |
| |
| function createOutputDiv(opt_title, opt_element) { |
| var el = document.createElement('test-case-output'); |
| if (opt_title) { |
| var titleEl = document.createElement('div'); |
| titleEl.textContent = opt_title; |
| el.appendChild(titleEl); |
| } |
| var contentEl = opt_element || document.createElement('div'); |
| contentEl.style.border = '1px solid black'; |
| el.appendChild(contentEl); |
| |
| el.__defineGetter__('contents', function() { |
| return contentEl; |
| }); |
| return el; |
| } |
| |
| function statusToClassName(status) { |
| if (status == 'PASSED') |
| return 'unittest-green'; |
| else if (status == 'RUNNING' || status == 'READY') |
| return 'unittest-yellow'; |
| else |
| return 'unittest-red'; |
| } |
| |
| function updateClassListGivenStatus(el, status) { |
| var newClass = statusToClassName(status); |
| if (newClass != 'unittest-green') |
| el.classList.remove('unittest-green'); |
| if (newClass != 'unittest-yellow') |
| el.classList.remove('unittest-yellow'); |
| if (newClass != 'unittest-red') |
| el.classList.remove('unittest-red'); |
| |
| el.classList.add(newClass); |
| } |
| |
| function HTMLTestRunner(opt_title, opt_curHash) { |
| // This constructs a HTMLDivElement and then adds our own runner methods to |
| // it. This is usually done via ui.js' define system, but we dont want our |
| // test runner to be dependent on the UI lib. :) |
| var outputEl = document.createElement('unittest-test-runner'); |
| outputEl.__proto__ = HTMLTestRunner.prototype; |
| this.decorate.call(outputEl, opt_title, opt_curHash); |
| return outputEl; |
| } |
| |
| HTMLTestRunner.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| decorate: function(opt_title, opt_curHash) { |
| this.running = false; |
| |
| this.currentTest_ = undefined; |
| this.results = undefined; |
| if (opt_curHash) { |
| var trimmedHash = opt_curHash.substring(1); |
| this.filterFunc_ = function(testName) { |
| return testName.indexOf(trimmedHash) == 0; |
| }; |
| } else |
| this.filterFunc_ = function(testName) { return true; }; |
| |
| this.statusEl_ = document.createElement('title'); |
| this.appendChild(this.statusEl_); |
| |
| this.resultsEl_ = document.createElement('div'); |
| this.appendChild(this.resultsEl_); |
| |
| this.title_ = opt_title || document.title; |
| |
| this.updateStatus(); |
| }, |
| |
| computeResultStats: function() { |
| var numTestsRun = 0; |
| var numTestsPassed = 0; |
| var numTestsWithErrors = 0; |
| if (this.results) { |
| for (var i = 0; i < this.results.length; i++) { |
| numTestsRun++; |
| if (this.results[i].errors.length) |
| numTestsWithErrors++; |
| else |
| numTestsPassed++; |
| } |
| } |
| return { |
| numTestsRun: numTestsRun, |
| numTestsPassed: numTestsPassed, |
| numTestsWithErrors: numTestsWithErrors |
| }; |
| }, |
| |
| updateStatus: function() { |
| var stats = this.computeResultStats(); |
| var status; |
| if (!this.results) { |
| status = 'READY'; |
| } else if (this.running) { |
| status = 'RUNNING'; |
| } else { |
| if (stats.numTestsRun && stats.numTestsWithErrors == 0) |
| status = 'PASSED'; |
| else |
| status = 'FAILED'; |
| } |
| |
| updateClassListGivenStatus(this.statusEl_, status); |
| this.statusEl_.textContent = this.title_ + ' [' + status + ']'; |
| }, |
| |
| get done() { |
| return this.results && this.running == false; |
| }, |
| |
| run: function(tests) { |
| this.results = []; |
| this.running = true; |
| this.updateStatus(); |
| for (var i = 0; i < tests.length; i++) { |
| if (!this.filterFunc_(tests[i].testName)) |
| continue; |
| tests[i].run(this); |
| this.updateStatus(); |
| } |
| this.running = false; |
| this.updateStatus(); |
| }, |
| |
| willRunTest: function(test) { |
| this.currentTest_ = test; |
| this.currentResults_ = {testName: test.testName, |
| errors: []}; |
| this.results.push(this.currentResults_); |
| |
| this.currentTestCaseEl_ = createTestCaseDiv(test.testName); |
| this.currentTestCaseEl_.status = 'RUNNING'; |
| this.resultsEl_.appendChild(this.currentTestCaseEl_); |
| }, |
| |
| /** |
| * Adds some html content to the currently running test |
| * @param {String} opt_title The title for the output. |
| * @param {HTMLElement} opt_element The element to add. If not added, then. |
| * @return {HTMLElement} The element added, or if !opt_element, the element |
| * created. |
| */ |
| addHTMLOutput: function(opt_title, opt_element) { |
| return this.currentTestCaseEl_.addHTMLOutput(opt_title, opt_element); |
| }, |
| |
| addError: function(e) { |
| this.currentResults_.errors.push(e); |
| return this.currentTestCaseEl_.addError(this.currentTest_, e); |
| }, |
| |
| didRunTest: function(test) { |
| if (!this.currentResults_.errors.length) |
| this.currentTestCaseEl_.status = 'PASSED'; |
| else |
| this.currentTestCaseEl_.status = 'FAILED'; |
| |
| this.currentResults_ = undefined; |
| this.currentTest_ = undefined; |
| } |
| }; |
| |
| function TestError(opt_message) { |
| var that = new Error(opt_message); |
| Error.captureStackTrace(that, TestError); |
| that.__proto__ = TestError.prototype; |
| return that; |
| } |
| |
| TestError.prototype = { |
| __proto__: Error.prototype |
| }; |
| |
| /* |
| * @constructor TestCase |
| */ |
| function TestCase(testMethod, opt_testMethodName) { |
| if (!testMethod) |
| throw new Error('testMethod must be provided'); |
| if (testMethod.name == '' && !opt_testMethodName) |
| throw new Error('testMethod must have a name, ' + |
| 'or opt_testMethodName must be provided.'); |
| |
| this.testMethod_ = testMethod; |
| this.testMethodName_ = opt_testMethodName || testMethod.name; |
| this.results_ = undefined; |
| }; |
| |
| function forAllAssertAndEnsureMethodsIn_(prototype, fn) { |
| for (var fieldName in prototype) { |
| if (fieldName.indexOf('assert') != 0 && |
| fieldName.indexOf('ensure') != 0) |
| continue; |
| var fieldValue = prototype[fieldName]; |
| if (typeof fieldValue != 'function') |
| continue; |
| fn(fieldName, fieldValue); |
| } |
| } |
| |
| TestCase.prototype = { |
| __proto__: Object.prototype, |
| |
| get testName() { |
| return this.testMethodName_; |
| }, |
| |
| bindGlobals_: function() { |
| forAllAssertAndEnsureMethodsIn_(TestCase.prototype, |
| function(fieldName, fieldValue) { |
| global[fieldName] = fieldValue.bind(this); |
| }); |
| }, |
| |
| unbindGlobals_: function() { |
| forAllAssertAndEnsureMethodsIn_(TestCase.prototype, |
| function(fieldName, fieldValue) { |
| delete global[fieldName]; |
| }); |
| }, |
| |
| /** |
| * Adds some html content to the currently running test |
| * @param {String} opt_title The title for the output. |
| * @param {HTMLElement} opt_element The element to add. If not added, then. |
| * @return {HTMLElement} The element added, or if !opt_element, the element |
| * created. |
| */ |
| addHTMLOutput: function(opt_title, opt_element) { |
| return this.results_.addHTMLOutput(opt_title, opt_element); |
| }, |
| |
| assertTrue: function(a, opt_message) { |
| if (a) |
| return; |
| var message = opt_message || 'Expected true, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertFalse: function(a, opt_message) { |
| if (!a) |
| return; |
| var message = opt_message || 'Expected false, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertUndefined: function(a, opt_message) { |
| if (a === undefined) |
| return; |
| var message = opt_message || 'Expected undefined, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertNotUndefined: function(a, opt_message) { |
| if (a !== undefined) |
| return; |
| var message = opt_message || 'Expected not undefined, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertNull: function(a, opt_message) { |
| if (a === null) |
| return; |
| var message = opt_message || 'Expected null, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertNotNull: function(a, opt_message) { |
| if (a !== null) |
| return; |
| var message = opt_message || 'Expected non-null, got ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertEquals: function(a, b, opt_message) { |
| if (a == b) |
| return; |
| var message = opt_message || 'Expected ' + a + ', got ' + b; |
| throw new TestError(message); |
| }, |
| |
| assertNotEquals: function(a, b, opt_message) { |
| if (a != b) |
| return; |
| var message = opt_message || 'Expected something not equal to ' + b; |
| throw new TestError(message); |
| }, |
| |
| assertArrayEquals: function(a, b, opt_message) { |
| if (a.length == b.length) { |
| var ok = true; |
| for (var i = 0; i < a.length; i++) { |
| ok &= a[i] === b[i]; |
| } |
| if (ok) |
| return; |
| } |
| |
| var message = opt_message || 'Expected array ' + a + ', got array ' + b; |
| throw new TestError(message); |
| }, |
| |
| assertArrayShallowEquals: function(a, b, opt_message) { |
| if (a.length == b.length) { |
| var ok = true; |
| for (var i = 0; i < a.length; i++) { |
| ok &= a[i] === b[i]; |
| } |
| if (ok) |
| return; |
| } |
| |
| var message = opt_message || 'Expected array ' + b + ', got array ' + a; |
| throw new TestError(message); |
| }, |
| |
| assertAlmostEquals: function(a, b, opt_message) { |
| if (Math.abs(a - b) < 0.00001) |
| return; |
| var message = opt_message || 'Expected almost ' + a + ', got ' + b; |
| throw new TestError(message); |
| }, |
| |
| assertThrows: function(fn, opt_message) { |
| try { |
| fn(); |
| } catch (e) { |
| return; |
| } |
| var message = opt_message || 'Expected throw from ' + fn; |
| throw new TestError(message); |
| }, |
| |
| setUp: function() { |
| }, |
| |
| run: function(results) { |
| this.bindGlobals_(); |
| try { |
| this.results_ = results; |
| results.willRunTest(this); |
| |
| // Set up. |
| try { |
| this.setUp(); |
| } catch (e) { |
| results.addError(e); |
| return; |
| } |
| |
| // Run. |
| try { |
| this.testMethod_(); |
| } catch (e) { |
| results.addError(e); |
| } |
| |
| // Tear down. |
| try { |
| this.tearDown(); |
| } catch (e) { |
| if (typeof e == 'string') |
| e = new TestError(e); |
| results.addError(e); |
| } |
| } finally { |
| this.unbindGlobals_(); |
| results.didRunTest(this); |
| this.results_ = undefined; |
| } |
| }, |
| |
| tearDown: function() { |
| } |
| |
| }; |
| |
| /** |
| * Returns an array of TestCase objects correpsonding to the tests |
| * found in the given object. This considers any functions beginning with test |
| * as a potential test. |
| * |
| * @param {object} opt_objectToEnumerate The object to enumerate, or global if |
| * not specified. |
| * @param {RegExp} opt_filter Return only tests that match this regexp. |
| */ |
| function discoverTests(opt_objectToEnumerate, opt_filter) { |
| var objectToEnumerate = opt_objectToEnumerate || global; |
| |
| var tests = []; |
| for (var testMethodName in objectToEnumerate) { |
| if (testMethodName.search(/^test.+/) != 0) |
| continue; |
| |
| if (opt_filter && testMethodName.search(opt_filter) == -1) |
| continue; |
| |
| var testMethod = objectToEnumerate[testMethodName]; |
| if (typeof testMethod != 'function') |
| continue; |
| var testCase = new TestCase(testMethod, testMethodName); |
| tests.push(testCase); |
| } |
| tests.sort(function(a, b) { |
| return a.testName.localeCompare(b.testName); |
| }); |
| return tests; |
| } |
| |
| /** |
| * Runs all unit tests. |
| */ |
| function runAllTests(opt_objectToEnumerate) { |
| var runner; |
| function init() { |
| if (runner) |
| runner.parentElement.removeChild(runner); |
| runner = new HTMLTestRunner(document.title, document.location.hash); |
| // Stash the runner on global so that the global test runner |
| // can get to it. |
| global.G_testRunner = runner; |
| } |
| |
| function append() { |
| document.body.appendChild(runner); |
| } |
| |
| function run() { |
| var objectToEnumerate = opt_objectToEnumerate || global; |
| var tests = discoverTests(objectToEnumerate); |
| runner.run(tests); |
| } |
| |
| global.addEventListener('hashchange', function() { |
| init(); |
| append(); |
| run(); |
| }); |
| |
| init(); |
| if (document.body) |
| append(); |
| else |
| document.addEventListener('DOMContentLoaded', append); |
| global.addEventListener('load', run); |
| } |
| |
| if (/_test.html$/.test(document.location.pathname)) |
| runAllTests(); |
| |
| return { |
| HTMLTestRunner: HTMLTestRunner, |
| TestError: TestError, |
| TestCase: TestCase, |
| discoverTests: discoverTests, |
| runAllTests: runAllTests, |
| createErrorDiv_: createErrorDiv, |
| createTestCaseDiv_: createTestCaseDiv |
| }; |
| }); |