Home | History | Annotate | Download | only in src
      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('unittest');
     13 base.exportTo('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     assertApproxEquals: function(a, b, opt_epsilon, opt_message) {
    426       if (a == b)
    427         return;
    428       var epsilon = opt_epsilon || 0.000001; // 6 digits.
    429       a = Math.abs(a);
    430       b = Math.abs(b);
    431       var delta = Math.abs(a - b);
    432       var sum = a + b;
    433       var relative_error = delta / sum;
    434       if (relative_error < epsilon)
    435         return;
    436       var message = opt_message || 'Expect ' + a + ' and ' + b +
    437         ' to be within ' + epsilon + ' was ' + relative_error;
    438       throw new TestError(message);
    439     },
    440 
    441     setUp: function() {
    442     },
    443 
    444     run: function(results) {
    445       this.bindGlobals_();
    446       try {
    447         this.results_ = results;
    448         results.willRunTest(this);
    449 
    450         if (NOCATCH_MODE) {
    451           this.setUp();
    452           this.testMethod_();
    453           this.tearDown();
    454         } else {
    455           // Set up.
    456           try {
    457             this.setUp();
    458           } catch (e) {
    459             results.addError(e);
    460             return;
    461           }
    462 
    463           // Run.
    464           try {
    465             this.testMethod_();
    466           } catch (e) {
    467             results.addError(e);
    468           }
    469 
    470           // Tear down.
    471           try {
    472             this.tearDown();
    473           } catch (e) {
    474             if (typeof e == 'string')
    475               e = new TestError(e);
    476             results.addError(e);
    477           }
    478         }
    479       } finally {
    480         this.unbindGlobals_();
    481         results.didRunTest(this);
    482         this.results_ = undefined;
    483       }
    484     },
    485 
    486     tearDown: function() {
    487     }
    488 
    489   };
    490 
    491   /**
    492    * Returns an array of TestCase objects correpsonding to the tests
    493    * found in the given object. This considers any functions beginning with test
    494    * as a potential test.
    495    *
    496    * @param {object} opt_objectToEnumerate The object to enumerate, or global if
    497    * not specified.
    498    * @param {RegExp} opt_filter Return only tests that match this regexp.
    499    */
    500   function discoverTests(opt_objectToEnumerate, opt_filter) {
    501     var objectToEnumerate = opt_objectToEnumerate || global;
    502 
    503     var tests = [];
    504     for (var testMethodName in objectToEnumerate) {
    505       if (testMethodName.search(/^test.+/) != 0)
    506         continue;
    507 
    508       if (opt_filter && testMethodName.search(opt_filter) == -1)
    509         continue;
    510 
    511       var testMethod = objectToEnumerate[testMethodName];
    512       if (typeof testMethod != 'function')
    513         continue;
    514       var testCase = new TestCase(testMethod, testMethodName);
    515       tests.push(testCase);
    516     }
    517     tests.sort(function(a, b) {
    518       return a.testName < b.testName;
    519     });
    520     return tests;
    521   }
    522 
    523   /**
    524    * Runs all unit tests.
    525    */
    526   function runAllTests(opt_objectToEnumerate) {
    527     var runner;
    528     function init() {
    529       if (runner)
    530         runner.parentElement.removeChild(runner);
    531       runner = new HTMLTestRunner(document.title, document.location.hash);
    532       // Stash the runner on global so that the global test runner
    533       // can get to it.
    534       global.G_testRunner = runner;
    535     }
    536 
    537     function append() {
    538       document.body.appendChild(runner);
    539     }
    540 
    541     function run() {
    542       var objectToEnumerate = opt_objectToEnumerate || global;
    543       var tests = discoverTests(objectToEnumerate);
    544       runner.run(tests);
    545     }
    546 
    547     global.addEventListener('hashchange', function() {
    548       init();
    549       append();
    550       run();
    551     });
    552 
    553     init();
    554     if (document.body)
    555       append();
    556     else
    557       document.addEventListener('DOMContentLoaded', append);
    558     global.addEventListener('load', run);
    559   }
    560 
    561   if (/_test.html$/.test(document.location.pathname))
    562     runAllTests();
    563 
    564   return {
    565     HTMLTestRunner: HTMLTestRunner,
    566     TestError: TestError,
    567     TestCase: TestCase,
    568     discoverTests: discoverTests,
    569     runAllTests: runAllTests,
    570     createErrorDiv_: createErrorDiv,
    571     createTestCaseDiv_: createTestCaseDiv
    572   };
    573 });
    574