lib/goog/testing/testrunner.js

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