• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7base.requireStylesheet('base.unittest');
8base.require('base.settings');
9base.require('base.unittest.test_error');
10base.require('base.unittest.assertions');
11
12base.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