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