Home | History | Annotate | Download | only in base
      1 // Copyright (c) 2013 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 base.requireStylesheet('base.unittest');
      8 base.require('base.settings');
      9 base.require('base.unittest.test_error');
     10 base.require('base.unittest.assertions');
     11 
     12 base.exportTo('base.unittest', function() {
     13   var TestResults = {
     14     FAILED: 0,
     15     PASSED: 1,
     16     PENDING: 2
     17   };
     18 
     19   var showCondensed_ = false;
     20   function showCondensed(val) {
     21     showCondensed_ = val;
     22   }
     23 
     24   function logWarningMessage(message) {
     25     var messagesEl = document.querySelector('#messages');
     26     messagesEl.setAttribute('hasMessages', true);
     27 
     28     var li = document.createElement('li');
     29     li.innerText = message;
     30 
     31     var list = document.querySelector('#message-list');
     32     list.appendChild(li);
     33   }
     34 
     35   function TestRunner(suitePaths, tests) {
     36     this.suitePaths_ = suitePaths || [];
     37     this.suites_ = [];
     38     this.suiteNames_ = {};
     39     this.tests_ = tests || [];
     40     this.moduleCount_ = 0;
     41 
     42     this.stats_ = {
     43       tests: 0,
     44       failures: 0,
     45       exceptions: [],
     46       duration: 0.0
     47     };
     48   }
     49 
     50   TestRunner.prototype = {
     51     __proto__: Object.prototype,
     52 
     53     loadSuites: function() {
     54       this.loadSuiteFiles();
     55     },
     56 
     57     run: function() {
     58       this.clear_(document.querySelector('#test-results'));
     59       this.clear_(document.querySelector('#exception-list'));
     60       this.clear_(document.querySelector('#message-list'));
     61 
     62       this.updateStats_();
     63       this.runSuites_();
     64     },
     65 
     66     addSuite: function(suite) {
     67       if (this.suiteNames_[suite.name] === true)
     68         logWarningMessage('Duplicate test suite name detected: ' + suite.name);
     69 
     70       this.suites_.push(suite);
     71       this.suiteNames_[suite.name] = true;
     72 
     73       // This assumes one test suite per file.
     74       if (this.suites_.length === this.suitePaths_.length)
     75         this.run();
     76     },
     77 
     78     loadSuiteFiles: function() {
     79       var modules = [];
     80       this.suitePaths_.forEach(function(path) {
     81         var moduleName = path.slice(5, path.length - 3);
     82         moduleName = moduleName.replace(/\//g, '.');
     83         modules.push(moduleName);
     84       });
     85 
     86       base.require(modules);
     87     },
     88 
     89     clear_: function(el) {
     90       while (el.firstChild)
     91         el.removeChild(el.firstChild);
     92     },
     93 
     94     runSuites_: function(opt_idx) {
     95       var idx = opt_idx || 0;
     96 
     97       var suiteCount = this.suites_.length;
     98       if (idx >= suiteCount) {
     99         var harness = document.querySelector('#test-results');
    100         harness.appendChild(document.createElement('br'));
    101         harness.appendChild(document.createTextNode('Test Run Complete'));
    102         return;
    103       }
    104 
    105       var suite = this.suites_[idx];
    106 
    107       suite.showLongResults = (suiteCount === 1);
    108       suite.displayInfo();
    109       suite.runTests(this.tests_);
    110 
    111       this.stats_.duration += suite.duration;
    112       this.stats_.tests += suite.testCount;
    113       this.stats_.failures += suite.failureCount;
    114 
    115       this.updateStats_();
    116 
    117       // Give the view time to update.
    118       window.setTimeout(function() {
    119         this.runSuites_(idx + 1);
    120       }.bind(this), 1);
    121     },
    122 
    123     onAnimationFrameError: function(e, opt_stack) {
    124       if (e.message)
    125         console.error(e.message, e.stack);
    126       else
    127         console.error(e);
    128 
    129       var exception = {e: e, stack: opt_stack};
    130       this.stats_.exceptions.push(exception);
    131       this.appendException(exception);
    132       this.updateStats_();
    133     },
    134 
    135     updateStats_: function() {
    136       var statEl = document.querySelector('#stats');
    137       statEl.innerHTML =
    138           this.suites_.length + ' suites, ' +
    139           '<span class="passed">' + this.stats_.tests + '</span> tests, ' +
    140           '<span class="failed">' + this.stats_.failures +
    141           '</span> failures, ' +
    142           '<span class="exception">' + this.stats_.exceptions.length +
    143           '</span> exceptions,' +
    144           ' in ' + this.stats_.duration + 'ms.';
    145     },
    146 
    147     appendException: function(exc) {
    148       var exceptionsEl = document.querySelector('#exceptions');
    149       exceptionsEl.setAttribute('hasExceptions', this.stats_.exceptions.length);
    150 
    151       var excEl = document.createElement('li');
    152       excEl.innerHTML = exc.e + '<pre>' + exc.stack + '</pre>';
    153 
    154       var exceptionsEl = document.querySelector('#exception-list');
    155       exceptionsEl.appendChild(excEl);
    156     }
    157   };
    158 
    159   function TestSuite(name, suite) {
    160     this.name_ = name;
    161     this.tests_ = [];
    162     this.testNames_ = {};
    163     this.failures_ = [];
    164     this.results_ = TestResults.PENDING;
    165     this.showLongResults = false;
    166     this.duration_ = 0.0;
    167     this.resultsEl_ = undefined;
    168 
    169     global.setup = function(fn) { this.setupFn_ = fn; }.bind(this);
    170     global.teardown = function(fn) { this.teardownFn_ = fn; }.bind(this);
    171 
    172     global.test = function(name, test) {
    173       if (this.testNames_[name] === true)
    174         logWarningMessage('Duplicate test name detected: ' + name);
    175 
    176       this.tests_.push(new Test(name, test));
    177       this.testNames_[name] = true;
    178     }.bind(this);
    179 
    180     suite.call();
    181 
    182     global.setup = undefined;
    183     global.teardown = undefined;
    184     global.test = undefined;
    185   }
    186 
    187   TestSuite.prototype = {
    188     __proto__: Object.prototype,
    189 
    190     get name() {
    191       return this.name_;
    192     },
    193 
    194     get results() {
    195       return this.results_;
    196     },
    197 
    198     get testCount() {
    199       return this.tests_.length;
    200     },
    201 
    202     get failureCount() {
    203       return this.failures.length;
    204     },
    205 
    206     get failures() {
    207       return this.failures_;
    208     },
    209 
    210     get duration() {
    211       return this.duration_;
    212     },
    213 
    214     displayInfo: function() {
    215       this.resultsEl_ = document.createElement('div');
    216       this.resultsEl_.className = 'test-result';
    217 
    218       var resultsPanel = document.querySelector('#test-results');
    219       resultsPanel.appendChild(this.resultsEl_);
    220 
    221       if (this.showLongResults) {
    222         this.resultsEl_.innerText = this.name;
    223       } else {
    224         var link = '/src/tests.html?suite=';
    225         link += this.name.replace(/\./g, '/');
    226 
    227         var suiteInfo = document.createElement('a');
    228         suiteInfo.href = link;
    229         suiteInfo.innerText = this.name;
    230         this.resultsEl_.appendChild(suiteInfo);
    231       }
    232 
    233       var statusEl = document.createElement('span');
    234       statusEl.classList.add('results');
    235       statusEl.classList.add('pending');
    236       statusEl.innerText = 'pending';
    237       this.resultsEl_.appendChild(statusEl);
    238     },
    239 
    240     runTests: function(testsToRun) {
    241       this.testsToRun_ = testsToRun;
    242 
    243       var start = new Date().getTime();
    244       this.results_ = TestResults.PENDING;
    245       this.tests_.forEach(function(test) {
    246         if (this.testsToRun_.length !== 0 &&
    247             this.testsToRun_.indexOf(test.name) === -1)
    248           return;
    249 
    250         // Clear settings storage before each test.
    251         global.sessionStorage.clear();
    252         base.Settings.setAlternativeStorageInstance(global.sessionStorage);
    253         base.onAnimationFrameError =
    254             testRunner.onAnimationFrameError.bind(testRunner);
    255 
    256         if (this.setupFn_ !== undefined)
    257           this.setupFn_.bind(test).call();
    258 
    259         var testWorkAreaEl_ = document.createElement('div');
    260 
    261         this.resultsEl_.appendChild(testWorkAreaEl_);
    262         test.run(testWorkAreaEl_);
    263         this.resultsEl_.removeChild(testWorkAreaEl_);
    264 
    265         if (this.teardownFn_ !== undefined)
    266           this.teardownFn_.bind(test).call();
    267 
    268         if (test.result === TestResults.FAILED) {
    269           this.failures_.push({
    270             error: test.failure,
    271             test: test.name
    272           });
    273           this.results_ = TestResults.FAILED;
    274         }
    275       }, this);
    276       if (this.results_ === TestResults.PENDING)
    277         this.results_ = TestResults.PASSED;
    278 
    279       this.duration_ = new Date().getTime() - start;
    280       this.outputResults();
    281     },
    282 
    283     outputResults: function() {
    284       if ((this.results === TestResults.PASSED) && showCondensed_ &&
    285           !this.showLongResults) {
    286         var parent = this.resultsEl_.parentNode;
    287         parent.removeChild(this.resultsEl_);
    288         this.resultsEl_ = undefined;
    289 
    290         parent.appendChild(document.createTextNode('.'));
    291         return;
    292       }
    293 
    294       var status = this.resultsEl_.querySelector('.results');
    295       status.classList.remove('pending');
    296       if (this.results === TestResults.PASSED) {
    297         status.innerText = 'passed';
    298         status.classList.add('passed');
    299       } else {
    300         status.innerText = 'FAILED';
    301         status.classList.add('failed');
    302       }
    303 
    304       status.innerText += ' (' + this.duration_ + 'ms)';
    305 
    306       var child = this.showLongResults ? this.outputLongResults() :
    307                                          this.outputShortResults();
    308       if (child !== undefined)
    309         this.resultsEl_.appendChild(child);
    310     },
    311 
    312     outputShortResults: function() {
    313       if (this.results === TestResults.PASSED)
    314         return undefined;
    315 
    316       var parent = document.createElement('div');
    317 
    318       var failureList = this.failures;
    319       for (var i = 0; i < failureList.length; ++i) {
    320         var fail = failureList[i];
    321 
    322         var preEl = document.createElement('pre');
    323         preEl.className = 'failure';
    324         preEl.innerText = 'Test: ' + fail.test + '\n' + fail.error.stack;
    325         parent.appendChild(preEl);
    326       }
    327 
    328       return parent;
    329     },
    330 
    331     outputLongResults: function() {
    332       var parent = document.createElement('div');
    333 
    334       this.tests_.forEach(function(test) {
    335         if (this.testsToRun_.length !== 0 &&
    336             this.testsToRun_.indexOf(test.name) === -1)
    337           return;
    338 
    339         var testEl = document.createElement('div');
    340         testEl.className = 'individual-result';
    341 
    342         var link = '/src/tests.html?suite=';
    343         link += this.name.replace(/\./g, '/');
    344         link += '&test=' + test.name.replace(/\./g, '/');
    345 
    346         var suiteInfo = document.createElement('a');
    347         suiteInfo.href = link;
    348         suiteInfo.innerText = test.name;
    349         testEl.appendChild(suiteInfo);
    350 
    351         parent.appendChild(testEl);
    352 
    353         var resultEl = document.createElement('span');
    354         resultEl.classList.add('results');
    355         testEl.appendChild(resultEl);
    356         if (test.result === TestResults.PASSED) {
    357           resultEl.classList.add('passed');
    358           resultEl.innerText = 'passed';
    359         } else {
    360           resultEl.classList.add('failed');
    361           resultEl.innerText = 'FAILED';
    362 
    363           var preEl = document.createElement('pre');
    364           preEl.className = 'failure';
    365           preEl.innerText = test.failure.stack;
    366           testEl.appendChild(preEl);
    367         }
    368 
    369         if (test.hasAppendedContent)
    370           testEl.appendChild(test.appendedContent);
    371 
    372       }.bind(this));
    373 
    374       return parent;
    375     },
    376 
    377     toString: function() {
    378       return this.name_;
    379     }
    380   };
    381 
    382   function Test(name, test) {
    383     this.name_ = name;
    384     this.test_ = test;
    385     this.result_ = TestResults.FAILED;
    386     this.failure_ = undefined;
    387 
    388     this.appendedContent_ = undefined;
    389   }
    390 
    391   Test.prototype = {
    392     __proto__: Object.prototype,
    393 
    394     run: function(workArea) {
    395       this.testWorkArea_ = workArea;
    396       try {
    397         this.test_.bind(this).call();
    398         this.result_ = TestResults.PASSED;
    399       } catch (e) {
    400         console.error(e, e.stack);
    401         this.failure_ = e;
    402       }
    403     },
    404 
    405     get failure() {
    406       return this.failure_;
    407     },
    408 
    409     get name() {
    410       return this.name_;
    411     },
    412 
    413     get result() {
    414       return this.result_;
    415     },
    416 
    417     get hasAppendedContent() {
    418       return (this.appendedContent_ !== undefined);
    419     },
    420 
    421     get appendedContent() {
    422       return this.appendedContent_;
    423     },
    424 
    425     addHTMLOutput: function(element) {
    426       this.testWorkArea_.appendChild(element);
    427       this.appendedContent_ = element;
    428     },
    429 
    430     toString: function() {
    431       return this.name_;
    432     }
    433   };
    434 
    435   var testRunner;
    436   function testSuite(name, suite) {
    437     testRunner.addSuite(new TestSuite(name, suite));
    438   }
    439 
    440   function Suites(suitePaths, tests) {
    441     testRunner = new TestRunner(suitePaths, tests);
    442     testRunner.loadSuites();
    443   }
    444 
    445   function runSuites() {
    446     testRunner.run();
    447   }
    448 
    449   return {
    450     showCondensed: showCondensed,
    451     testSuite: testSuite,
    452     runSuites: runSuites,
    453     Suites: Suites
    454   };
    455 });
    456