testrunner.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview The test runner is a singleton object that is used to execute
  16. * a goog.testing.TestCases, display the results, and expose the results to
  17. * Selenium for automation. If a TestCase hasn't been registered with the
  18. * runner by the time window.onload occurs, the testRunner will try to auto-
  19. * discover JsUnit style test pages.
  20. *
  21. * The hooks for selenium are (see http://go/selenium-hook-setup):-
  22. * - Boolean G_testRunner.isFinished()
  23. * - Boolean G_testRunner.isSuccess()
  24. * - String G_testRunner.getReport()
  25. * - number G_testRunner.getRunTime()
  26. * - Object<string, Array<string>> G_testRunner.getTestResults()
  27. *
  28. * Testing code should not have dependencies outside of goog.testing so as to
  29. * reduce the chance of masking missing dependencies.
  30. *
  31. */
  32. goog.setTestOnly('goog.testing.TestRunner');
  33. goog.provide('goog.testing.TestRunner');
  34. goog.require('goog.dom');
  35. goog.require('goog.dom.TagName');
  36. goog.require('goog.testing.TestCase');
  37. /**
  38. * Construct a test runner.
  39. *
  40. * NOTE(user): This is currently pretty weird, I'm essentially trying to
  41. * create a wrapper that the Selenium test can hook into to query the state of
  42. * the running test case, while making goog.testing.TestCase general.
  43. *
  44. * @constructor
  45. */
  46. goog.testing.TestRunner = function() {
  47. /**
  48. * Errors that occurred in the window.
  49. * @type {!Array<string>}
  50. */
  51. this.errors = [];
  52. /**
  53. * Reference to the active test case.
  54. * @type {?goog.testing.TestCase}
  55. */
  56. this.testCase = null;
  57. /**
  58. * Whether the test runner has been initialized yet.
  59. * @type {boolean}
  60. */
  61. this.initialized = false;
  62. /**
  63. * Element created in the document to add test results to.
  64. * @private {?Element}
  65. */
  66. this.logEl_ = null;
  67. /**
  68. * Function to use when filtering errors.
  69. * @private {(function(string))?}
  70. */
  71. this.errorFilter_ = null;
  72. /**
  73. * Whether an empty test case counts as an error.
  74. * @private {boolean}
  75. */
  76. this.strict_ = true;
  77. /**
  78. * An id unique to this runner. Checked by the server during polling to
  79. * verify that the page was not reloaded.
  80. * @private {!string}
  81. */
  82. this.uniqueId_ = Math.random() + '';
  83. };
  84. /**
  85. * The uuid is embedded in the URL search. This function allows us to mock
  86. * the search in the test.
  87. * @return {string}
  88. */
  89. goog.testing.TestRunner.prototype.getSearchString = function() {
  90. return window.location.search;
  91. };
  92. /**
  93. * Returns the unique id for this test page.
  94. * @return {!string}
  95. */
  96. goog.testing.TestRunner.prototype.getUniqueId = function() {
  97. return this.uniqueId_;
  98. };
  99. /**
  100. * Initializes the test runner.
  101. * @param {goog.testing.TestCase} testCase The test case to initialize with.
  102. */
  103. goog.testing.TestRunner.prototype.initialize = function(testCase) {
  104. if (this.testCase && this.testCase.running) {
  105. throw Error('The test runner is already waiting for a test to complete');
  106. }
  107. this.testCase = testCase;
  108. this.initialized = true;
  109. };
  110. /**
  111. * By default, the test runner is strict, and fails if it runs an empty
  112. * test case.
  113. * @param {boolean} strict Whether the test runner should fail on an empty
  114. * test case.
  115. */
  116. goog.testing.TestRunner.prototype.setStrict = function(strict) {
  117. this.strict_ = strict;
  118. };
  119. /**
  120. * @return {boolean} Whether the test runner should fail on an empty
  121. * test case.
  122. */
  123. goog.testing.TestRunner.prototype.isStrict = function() {
  124. return this.strict_;
  125. };
  126. /**
  127. * Returns true if the test runner is initialized.
  128. * Used by Selenium Hooks.
  129. * @return {boolean} Whether the test runner is active.
  130. */
  131. goog.testing.TestRunner.prototype.isInitialized = function() {
  132. return this.initialized;
  133. };
  134. /**
  135. * Returns true if the test runner is finished.
  136. * Used by Selenium Hooks.
  137. * @return {boolean} Whether the test runner is active.
  138. */
  139. goog.testing.TestRunner.prototype.isFinished = function() {
  140. return this.errors.length > 0 ||
  141. this.initialized && !!this.testCase && this.testCase.started &&
  142. !this.testCase.running;
  143. };
  144. /**
  145. * Returns true if the test case didn't fail.
  146. * Used by Selenium Hooks.
  147. * @return {boolean} Whether the current test returned successfully.
  148. */
  149. goog.testing.TestRunner.prototype.isSuccess = function() {
  150. return !this.hasErrors() && !!this.testCase && this.testCase.isSuccess();
  151. };
  152. /**
  153. * Returns true if the test case runner has errors that were caught outside of
  154. * the test case.
  155. * @return {boolean} Whether there were JS errors.
  156. */
  157. goog.testing.TestRunner.prototype.hasErrors = function() {
  158. return this.errors.length > 0;
  159. };
  160. /**
  161. * Logs an error that occurred. Used in the case of environment setting up
  162. * an onerror handler.
  163. * @param {string} msg Error message.
  164. */
  165. goog.testing.TestRunner.prototype.logError = function(msg) {
  166. if (!this.errorFilter_ || this.errorFilter_.call(null, msg)) {
  167. this.errors.push(msg);
  168. }
  169. };
  170. /**
  171. * Log failure in current running test.
  172. * @param {Error} ex Exception.
  173. */
  174. goog.testing.TestRunner.prototype.logTestFailure = function(ex) {
  175. var testName = /** @type {string} */ (goog.testing.TestCase.currentTestName);
  176. if (this.testCase) {
  177. this.testCase.logError(testName, ex);
  178. } else {
  179. // NOTE: Do not forget to log the original exception raised.
  180. throw new Error(
  181. 'Test runner not initialized with a test case. Original ' +
  182. 'exception: ' + ex.message);
  183. }
  184. };
  185. /**
  186. * Sets a function to use as a filter for errors.
  187. * @param {function(string)} fn Filter function.
  188. */
  189. goog.testing.TestRunner.prototype.setErrorFilter = function(fn) {
  190. this.errorFilter_ = fn;
  191. };
  192. /**
  193. * Returns a report of the test case that ran.
  194. * Used by Selenium Hooks.
  195. * @param {boolean=} opt_verbose If true results will include data about all
  196. * tests, not just what failed.
  197. * @return {string} A report summary of the test.
  198. */
  199. goog.testing.TestRunner.prototype.getReport = function(opt_verbose) {
  200. var report = [];
  201. if (this.testCase) {
  202. report.push(this.testCase.getReport(opt_verbose));
  203. }
  204. if (this.errors.length > 0) {
  205. report.push('JavaScript errors detected by test runner:');
  206. report.push.apply(report, this.errors);
  207. report.push('\n');
  208. }
  209. return report.join('\n');
  210. };
  211. /**
  212. * Returns the amount of time it took for the test to run.
  213. * Used by Selenium Hooks.
  214. * @return {number} The run time, in milliseconds.
  215. */
  216. goog.testing.TestRunner.prototype.getRunTime = function() {
  217. return this.testCase ? this.testCase.getRunTime() : 0;
  218. };
  219. /**
  220. * Returns the number of script files that were loaded in order to run the test.
  221. * @return {number} The number of script files.
  222. */
  223. goog.testing.TestRunner.prototype.getNumFilesLoaded = function() {
  224. return this.testCase ? this.testCase.getNumFilesLoaded() : 0;
  225. };
  226. /**
  227. * Executes a test case and prints the results to the window.
  228. */
  229. goog.testing.TestRunner.prototype.execute = function() {
  230. if (!this.testCase) {
  231. throw Error(
  232. 'The test runner must be initialized with a test case ' +
  233. 'before execute can be called.');
  234. }
  235. if (this.strict_ && this.testCase.getCount() == 0) {
  236. throw Error(
  237. 'No tests found in given test case: ' + this.testCase.getName() + '. ' +
  238. 'By default, the test runner fails if a test case has no tests. ' +
  239. 'To modify this behavior, see goog.testing.TestRunner\'s ' +
  240. 'setStrict() method, or G_testRunner.setStrict()');
  241. }
  242. this.testCase.setCompletedCallback(goog.bind(this.onComplete_, this));
  243. if (goog.testing.TestRunner.shouldUsePromises_(this.testCase)) {
  244. this.testCase.runTestsReturningPromise();
  245. } else {
  246. this.testCase.runTests();
  247. }
  248. };
  249. /**
  250. * @param {!goog.testing.TestCase} testCase
  251. * @return {boolean}
  252. * @private
  253. */
  254. goog.testing.TestRunner.shouldUsePromises_ = function(testCase) {
  255. return testCase.constructor === goog.testing.TestCase;
  256. };
  257. /** @const {string} The ID of the element to log output to. */
  258. goog.testing.TestRunner.TEST_LOG_ID = 'closureTestRunnerLog';
  259. /**
  260. * Writes the results to the document when the test case completes.
  261. * @private
  262. */
  263. goog.testing.TestRunner.prototype.onComplete_ = function() {
  264. var log = this.testCase.getReport(true);
  265. if (this.errors.length > 0) {
  266. log += '\n' + this.errors.join('\n');
  267. }
  268. if (!this.logEl_) {
  269. var el = document.getElementById(goog.testing.TestRunner.TEST_LOG_ID);
  270. if (el == null) {
  271. el = goog.dom.createElement(goog.dom.TagName.DIV);
  272. el.id = goog.testing.TestRunner.TEST_LOG_ID;
  273. document.body.appendChild(el);
  274. }
  275. this.logEl_ = el;
  276. }
  277. // Highlight the page to indicate the overall outcome.
  278. this.writeLog(log);
  279. // TODO(chrishenry): Make this work with multiple test cases (b/8603638).
  280. var runAgainLink = goog.dom.createElement(goog.dom.TagName.A);
  281. runAgainLink.style.display = 'inline-block';
  282. runAgainLink.style.fontSize = 'small';
  283. runAgainLink.style.marginBottom = '16px';
  284. runAgainLink.href = '';
  285. runAgainLink.onclick = goog.bind(function() {
  286. this.execute();
  287. return false;
  288. }, this);
  289. runAgainLink.innerHTML = 'Run again without reloading';
  290. this.logEl_.appendChild(runAgainLink);
  291. };
  292. /**
  293. * Writes a nicely formatted log out to the document.
  294. * @param {string} log The string to write.
  295. */
  296. goog.testing.TestRunner.prototype.writeLog = function(log) {
  297. var lines = log.split('\n');
  298. for (var i = 0; i < lines.length; i++) {
  299. var line = lines[i];
  300. var color;
  301. var isPassed = /PASSED/.test(line);
  302. var isFailOrError =
  303. /FAILED/.test(line) || /ERROR/.test(line) || /NO TESTS RUN/.test(line);
  304. if (isPassed) {
  305. color = 'darkgreen';
  306. } else if (isFailOrError) {
  307. color = 'darkred';
  308. } else {
  309. color = '#333';
  310. }
  311. var div = goog.dom.createElement(goog.dom.TagName.DIV);
  312. if (line.substr(0, 2) == '> ') {
  313. // The stack trace may contain links so it has to be interpreted as HTML.
  314. div.innerHTML = line;
  315. } else {
  316. div.appendChild(document.createTextNode(line));
  317. }
  318. var testNameMatch = /(\S+) (\[[^\]]*] )?: (FAILED|ERROR|PASSED)/.exec(line);
  319. if (testNameMatch) {
  320. // Build a URL to run the test individually. If this test was already
  321. // part of another subset test, we need to overwrite the old runTests
  322. // query parameter. We also need to do this without bringing in any
  323. // extra dependencies, otherwise we could mask missing dependency bugs.
  324. var newSearch = 'runTests=' + testNameMatch[1];
  325. var search = window.location.search;
  326. if (search) {
  327. var oldTests = /runTests=([^&]*)/.exec(search);
  328. if (oldTests) {
  329. newSearch = search.substr(0, oldTests.index) + newSearch +
  330. search.substr(oldTests.index + oldTests[0].length);
  331. } else {
  332. newSearch = search + '&' + newSearch;
  333. }
  334. } else {
  335. newSearch = '?' + newSearch;
  336. }
  337. var href = window.location.href;
  338. var hash = window.location.hash;
  339. if (hash && hash.charAt(0) != '#') {
  340. hash = '#' + hash;
  341. }
  342. href = href.split('#')[0].split('?')[0] + newSearch + hash;
  343. // Add the link.
  344. var a = goog.dom.createElement(goog.dom.TagName.A);
  345. a.innerHTML = '(run individually)';
  346. a.style.fontSize = '0.8em';
  347. a.style.color = '#888';
  348. a.href = href;
  349. div.appendChild(document.createTextNode(' '));
  350. div.appendChild(a);
  351. }
  352. div.style.color = color;
  353. div.style.font = 'normal 100% monospace';
  354. div.style.wordWrap = 'break-word';
  355. if (i == 0) {
  356. // Highlight the first line as a header that indicates the test outcome.
  357. div.style.padding = '20px';
  358. div.style.marginBottom = '10px';
  359. if (isPassed) {
  360. div.style.border = '1px solid ' + color;
  361. div.style.backgroundColor = '#eeffee';
  362. } else if (isFailOrError) {
  363. div.style.border = '5px solid ' + color;
  364. div.style.backgroundColor = '#ffeeee';
  365. } else {
  366. div.style.border = '1px solid black';
  367. div.style.backgroundColor = '#eeeeee';
  368. }
  369. }
  370. try {
  371. div.style.whiteSpace = 'pre-wrap';
  372. } catch (e) {
  373. // NOTE(brenneman): IE raises an exception when assigning to pre-wrap.
  374. // Thankfully, it doesn't collapse whitespace when using monospace fonts,
  375. // so it will display correctly if we ignore the exception.
  376. }
  377. if (i < 2) {
  378. div.style.fontWeight = 'bold';
  379. }
  380. this.logEl_.appendChild(div);
  381. }
  382. };
  383. /**
  384. * Logs a message to the current test case.
  385. * @param {string} s The text to output to the log.
  386. */
  387. goog.testing.TestRunner.prototype.log = function(s) {
  388. if (this.testCase) {
  389. this.testCase.log(s);
  390. }
  391. };
  392. // TODO(nnaze): Properly handle serving test results when multiple test cases
  393. // are run.
  394. /**
  395. * @return {Object<string, !Array<!goog.testing.TestCase.IResult>>} A map of
  396. * test names to a list of test failures (if any) to provide formatted data
  397. * for the test runner.
  398. */
  399. goog.testing.TestRunner.prototype.getTestResults = function() {
  400. if (this.testCase) {
  401. return this.testCase.getTestResults();
  402. }
  403. return null;
  404. };
  405. /**
  406. * Returns the test results as json.
  407. * This is called by the testing infrastructure through G_testrunner.
  408. * @return {?string} Tests results object.
  409. */
  410. goog.testing.TestRunner.prototype.getTestResultsAsJson = function() {
  411. if (this.testCase) {
  412. return this.testCase.getTestResultsAsJson();
  413. }
  414. return null;
  415. };