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