Path: blob/trunk/third_party/closure/goog/testing/testrunner.js
4501 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview The test runner is a singleton object that is used to execute8* a goog.testing.TestCases, display the results, and expose the results to9* Selenium for automation. If a TestCase hasn't been registered with the10* runner by the time window.onload occurs, the testRunner will try to auto-11* discover JsUnit style test pages.12*13* The hooks for selenium are (see http://go/selenium-hook-setup):-14* - Boolean G_testRunner.isFinished()15* - Boolean G_testRunner.isSuccess()16* - String G_testRunner.getReport()17* - number G_testRunner.getRunTime()18* - Object<string, Array<string>> G_testRunner.getTestResults()19*20* Testing code should not have dependencies outside of goog.testing so as to21* reduce the chance of masking missing dependencies.22*/2324goog.setTestOnly('goog.testing.TestRunner');25goog.provide('goog.testing.TestRunner');2627goog.require('goog.dom');28goog.require('goog.dom.TagName');29goog.require('goog.dom.safe');30goog.require('goog.json');31goog.require('goog.testing.TestCase');32333435/**36* Construct a test runner.37*38* NOTE(user): This is currently pretty weird, I'm essentially trying to39* create a wrapper that the Selenium test can hook into to query the state of40* the running test case, while making goog.testing.TestCase general.41*42* @constructor43*/44goog.testing.TestRunner = function() {45'use strict';46/**47* Errors that occurred in the window.48* @type {!Array<string>}49*/50this.errors = [];5152/**53* Reference to the active test case.54* @type {?goog.testing.TestCase}55*/56this.testCase = null;5758/**59* Whether the test runner has been initialized yet.60* @type {boolean}61*/62this.initialized = false;6364/**65* Element created in the document to add test results to.66* @private {?Element}67*/68this.logEl_ = null;6970/**71* Function to use when filtering errors.72* @private {(function(string))?}73*/74this.errorFilter_ = null;7576/**77* Whether an empty test case counts as an error.78* @private {boolean}79*/80this.strict_ = true;8182/**83* Store the serializer to avoid it being overwritten by a mock.84* @private {function(!Object): string}85*/86this.jsonStringify_ = goog.json.serialize;8788/**89* An id unique to this runner. Checked by the server during polling to90* verify that the page was not reloaded.91* @private {string}92*/93this.uniqueId_ = ((Math.random() * 1e9) >>> 0) + '-' +94window.location.pathname.replace(/.*\//, '').replace(/\.html.*$/, '');9596var self = this;97function onPageHide() {98self.clearUniqueId();99}100window.addEventListener('pagehide', onPageHide);101};102103/**104* The uuid is embedded in the URL search. This function allows us to mock105* the search in the test.106* @return {string}107*/108goog.testing.TestRunner.prototype.getSearchString = function() {109'use strict';110return window.location.search;111};112113/**114* Returns the unique id for this test page.115* @return {string}116*/117goog.testing.TestRunner.prototype.getUniqueId = function() {118'use strict';119return this.uniqueId_;120};121122/**123* Clears the unique id for this page. The value will hint the reason.124*/125goog.testing.TestRunner.prototype.clearUniqueId = function() {126'use strict';127this.uniqueId_ = 'pagehide';128};129130/**131* Initializes the test runner.132* @param {goog.testing.TestCase} testCase The test case to initialize with.133*/134goog.testing.TestRunner.prototype.initialize = function(testCase) {135'use strict';136if (this.testCase && this.testCase.running) {137throw new Error(138'The test runner is already waiting for a test to complete');139}140this.testCase = testCase;141this.initialized = true;142};143144145/**146* By default, the test runner is strict, and fails if it runs an empty147* test case.148* @param {boolean} strict Whether the test runner should fail on an empty149* test case.150*/151goog.testing.TestRunner.prototype.setStrict = function(strict) {152'use strict';153this.strict_ = strict;154};155156157/**158* @return {boolean} Whether the test runner should fail on an empty159* test case.160*/161goog.testing.TestRunner.prototype.isStrict = function() {162'use strict';163return this.strict_;164};165166167/**168* Returns true if the test runner is initialized.169* Used by Selenium Hooks.170* @return {boolean} Whether the test runner is active.171*/172goog.testing.TestRunner.prototype.isInitialized = function() {173'use strict';174return this.initialized;175};176177178/**179* Returns false if the test runner has not finished successfully.180* Used by Selenium Hooks.181* @return {boolean} Whether the test runner is not active.182*/183goog.testing.TestRunner.prototype.isFinished = function() {184'use strict';185return this.errors.length > 0 || this.isComplete();186};187188189/**190* Returns true if the test runner is finished.191* @return {boolean} True if the test runner started and subsequently completed.192*/193goog.testing.TestRunner.prototype.isComplete = function() {194'use strict';195return this.initialized && !!this.testCase && this.testCase.started &&196!this.testCase.running;197};198199/**200* Returns true if the test case didn't fail.201* Used by Selenium Hooks.202* @return {boolean} Whether the current test returned successfully.203*/204goog.testing.TestRunner.prototype.isSuccess = function() {205'use strict';206return !this.hasErrors() && !!this.testCase && this.testCase.isSuccess();207};208209210/**211* Returns true if the test case runner has errors that were caught outside of212* the test case.213* @return {boolean} Whether there were JS errors.214*/215goog.testing.TestRunner.prototype.hasErrors = function() {216'use strict';217return this.errors.length > 0;218};219220221/**222* Logs an error that occurred. Used in the case of environment setting up223* an onerror handler.224* @param {string} msg Error message.225*/226goog.testing.TestRunner.prototype.logError = function(msg) {227'use strict';228if (this.isComplete()) {229// Once the user has checked their code, subsequent errors can occur230// because of tearDown actions. For now, log these but do not fail the test.231this.log('Error after test completed: ' + msg);232return;233}234if (!this.errorFilter_ || this.errorFilter_.call(null, msg)) {235this.errors.push(msg);236}237};238239240/**241* Log failure in current running test.242* @param {Error} ex Exception.243*/244goog.testing.TestRunner.prototype.logTestFailure = function(ex) {245'use strict';246var testName = /** @type {string} */ (goog.testing.TestCase.currentTestName);247if (this.testCase) {248this.testCase.logError(testName, ex);249} else {250// NOTE: Do not forget to log the original exception raised.251throw new Error(252'Test runner not initialized with a test case. Original ' +253'exception: ' + ex.message);254}255};256257258/**259* Sets a function to use as a filter for errors.260* @param {function(string)} fn Filter function.261*/262goog.testing.TestRunner.prototype.setErrorFilter = function(fn) {263'use strict';264this.errorFilter_ = fn;265};266267268/**269* Returns a report of the test case that ran.270* Used by Selenium Hooks.271* @param {boolean=} opt_verbose If true results will include data about all272* tests, not just what failed.273* @return {string} A report summary of the test.274*/275goog.testing.TestRunner.prototype.getReport = function(opt_verbose) {276'use strict';277var report = [];278if (this.testCase) {279report.push(this.testCase.getReport(opt_verbose));280}281if (this.errors.length > 0) {282report.push('JavaScript errors detected by test runner:');283report.push.apply(report, this.errors);284report.push('\n');285}286return report.join('\n');287};288289290/**291* Returns the amount of time it took for the test to run.292* Used by Selenium Hooks.293* @return {number} The run time, in milliseconds.294*/295goog.testing.TestRunner.prototype.getRunTime = function() {296'use strict';297return this.testCase ? this.testCase.getRunTime() : 0;298};299300301/**302* Returns the number of script files that were loaded in order to run the test.303* @return {number} The number of script files.304*/305goog.testing.TestRunner.prototype.getNumFilesLoaded = function() {306'use strict';307return this.testCase ? this.testCase.getNumFilesLoaded() : 0;308};309310311/**312* Executes a test case and prints the results to the window.313*/314goog.testing.TestRunner.prototype.execute = function() {315'use strict';316if (!this.testCase) {317throw new Error(318'The test runner must be initialized with a test case ' +319'before execute can be called.');320}321322if (this.strict_ && this.testCase.getCount() == 0) {323throw new Error(324'No tests found in given test case: ' + this.testCase.getName() + '. ' +325'By default, the test runner fails if a test case has no tests. ' +326'To modify this behavior, see goog.testing.TestRunner\'s ' +327'setStrict() method, or G_testRunner.setStrict()');328}329330this.testCase.addCompletedCallback(goog.bind(this.onComplete_, this));331if (goog.testing.TestRunner.shouldUsePromises_(this.testCase)) {332this.testCase.runTestsReturningPromise();333} else {334this.testCase.runTests();335}336};337338339/**340* @param {!goog.testing.TestCase} testCase341* @return {boolean}342* @private343*/344goog.testing.TestRunner.shouldUsePromises_ = function(testCase) {345'use strict';346return testCase.constructor === goog.testing.TestCase;347};348349350/** @const {string} The ID of the element to log output to. */351goog.testing.TestRunner.TEST_LOG_ID = 'closureTestRunnerLog';352353354/**355* Writes the results to the document when the test case completes.356* @private357*/358goog.testing.TestRunner.prototype.onComplete_ = function() {359'use strict';360var log = this.testCase.getReport(true);361if (this.errors.length > 0) {362log += '\n' + this.errors.join('\n');363}364365if (!this.logEl_) {366var el = document.getElementById(goog.testing.TestRunner.TEST_LOG_ID);367if (el == null) {368el = goog.dom.createElement(goog.dom.TagName.DIV);369el.id = goog.testing.TestRunner.TEST_LOG_ID;370el.dir = 'ltr';371document.body.appendChild(el);372}373this.logEl_ = el;374}375376// Highlight the page to indicate the overall outcome.377this.writeLog(log);378379// TODO(chrishenry): Make this work with multiple test cases (b/8603638).380var runAgainLink = goog.dom.createElement(goog.dom.TagName.A);381runAgainLink.style.display = 'inline-block';382runAgainLink.style.fontSize = 'small';383runAgainLink.style.marginBottom = '16px';384runAgainLink.href = '';385runAgainLink.onclick = goog.bind(function() {386'use strict';387this.execute();388return false;389}, this);390runAgainLink.textContent = 'Run again without reloading';391this.logEl_.appendChild(runAgainLink);392};393394395/**396* Writes a nicely formatted log out to the document.397* @param {string} log The string to write.398*/399goog.testing.TestRunner.prototype.writeLog = function(log) {400'use strict';401var lines = log.split('\n');402for (var i = 0; i < lines.length; i++) {403var line = lines[i];404var color;405var isPassed = /PASSED/.test(line);406var isSkipped = /SKIPPED/.test(line);407var isFailOrError =408/FAILED/.test(line) || /ERROR/.test(line) || /NO TESTS RUN/.test(line);409if (isPassed) {410color = 'darkgreen';411} else if (isSkipped) {412color = 'slategray';413} else if (isFailOrError) {414color = 'darkred';415} else {416color = '#333';417}418var div = goog.dom.createElement(goog.dom.TagName.DIV);419// Empty divs don't take up any space, use \n to take up space and preserve420// newlines when copying the logs.421if (line == '') {422line = '\n';423}424if (line.slice(0, 2) == '> ') {425// The stack trace may contain links so it has to be interpreted as HTML.426div.innerHTML = line;427} else {428div.appendChild(document.createTextNode(line));429}430431// Example line we are parsing the test name from:432// 16:07:49.317 testSomething : PASSED433var testNameMatch = /\S+\s+(test.*)\s+: (FAILED|ERROR|PASSED)/.exec(line);434if (testNameMatch) {435// Build a URL to run the test individually. If this test was already436// part of another subset test, we need to overwrite the old runTests437// query parameter. We also need to do this without bringing in any438// extra dependencies, otherwise we could mask missing dependency bugs.439// We manually encode commas because they are also used to separate test440// names.441var newSearch = 'runTests=' +442encodeURIComponent(testNameMatch[1].replace(/,/g, '%2C'));443var search = window.location.search;444if (search) {445var oldTests = /runTests=([^&]*)/.exec(search);446if (oldTests) {447newSearch = search.slice(0, oldTests.index) + newSearch +448search.slice(oldTests.index + oldTests[0].length);449} else {450newSearch = search + '&' + newSearch;451}452} else {453newSearch = '?' + newSearch;454}455var href = window.location.href;456var hash = window.location.hash;457if (hash && hash.charAt(0) != '#') {458hash = '#' + hash;459}460href = href.split('#')[0].split('?')[0] + newSearch + hash;461462// Add the link.463var a = goog.dom.createElement(goog.dom.TagName.A);464a.textContent = '(run individually)';465a.style.fontSize = '0.8em';466a.style.color = '#888';467goog.dom.safe.setAnchorHref(a, href);468div.appendChild(document.createTextNode(' '));469div.appendChild(a);470}471472div.style.color = color;473div.style.font = 'normal 100% monospace';474div.style.wordWrap = 'break-word';475if (i == 0) {476// Highlight the first line as a header that indicates the test outcome.477div.style.padding = '20px';478div.style.marginBottom = '10px';479if (isPassed) {480div.style.border = '1px solid ' + color;481div.style.backgroundColor = '#eeffee';482} else if (isFailOrError) {483div.style.border = '5px solid ' + color;484div.style.backgroundColor = '#ffeeee';485} else {486div.style.border = '1px solid black';487div.style.backgroundColor = '#eeeeee';488}489}490491try {492div.style.whiteSpace = 'pre-wrap';493} catch (e) {494// NOTE(brenneman): IE raises an exception when assigning to pre-wrap.495// Thankfully, it doesn't collapse whitespace when using monospace fonts,496// so it will display correctly if we ignore the exception.497}498499if (i < 2) {500div.style.fontWeight = 'bold';501}502this.logEl_.appendChild(div);503}504};505506507/**508* Logs a message to the current test case.509* @param {string} s The text to output to the log.510*/511goog.testing.TestRunner.prototype.log = function(s) {512'use strict';513if (this.testCase) {514this.testCase.log(s);515}516};517518519// TODO(nnaze): Properly handle serving test results when multiple test cases520// are run.521/**522* @return {Object<string, !Array<!goog.testing.TestCase.IResult>>} A map of523* test names to a list of test failures (if any) to provide formatted data524* for the test runner.525*/526goog.testing.TestRunner.prototype.getTestResults = function() {527'use strict';528if (this.testCase) {529return this.testCase.getTestResults();530}531return null;532};533534535/**536* Returns the test results as json.537* This is called by the testing infrastructure through G_testrunner.538* @return {?string} Tests results object.539*/540goog.testing.TestRunner.prototype.getTestResultsAsJson = function() {541'use strict';542if (this.testCase) {543var testCaseResults544/** {Object<string, !Array<!goog.testing.TestCase.IResult>>} */545= this.testCase.getTestResults();546if (this.hasErrors()) {547var globalErrors = [];548for (var i = 0; i < this.errors.length; i++) {549globalErrors.push(550{source: '', message: this.errors[i], stacktrace: ''});551}552// We are writing on our testCase results, but the test is over.553testCaseResults['globalErrors'] = globalErrors;554}555return this.jsonStringify_(testCaseResults);556}557return null;558};559560561