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.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 | */ |
49 | goog.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 | */ |
62 | goog.testing.TestRunner.prototype.testCase = null; |
63 | |
64 | |
65 | /** |
66 | * Whether the test runner has been initialized yet. |
67 | * @type {boolean} |
68 | */ |
69 | goog.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 | */ |
77 | goog.testing.TestRunner.prototype.logEl_ = null; |
78 | |
79 | |
80 | /** |
81 | * Function to use when filtering errors. |
82 | * @type {(function(string))?} |
83 | * @private |
84 | */ |
85 | goog.testing.TestRunner.prototype.errorFilter_ = null; |
86 | |
87 | |
88 | /** |
89 | * Whether an empty test case counts as an error. |
90 | * @type {boolean} |
91 | * @private |
92 | */ |
93 | goog.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 | */ |
100 | goog.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 | */ |
115 | goog.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 | */ |
124 | goog.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 | */ |
134 | goog.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 | */ |
144 | goog.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 | */ |
156 | goog.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 | */ |
166 | goog.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 | */ |
176 | goog.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 | */ |
187 | goog.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 | */ |
203 | goog.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 | */ |
215 | goog.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 | */ |
234 | goog.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 | */ |
243 | goog.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 | */ |
251 | goog.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 | this.testCase.runTests(); |
268 | }; |
269 | |
270 | |
271 | /** |
272 | * Writes the results to the document when the test case completes. |
273 | * @private |
274 | */ |
275 | goog.testing.TestRunner.prototype.onComplete_ = function() { |
276 | var log = this.testCase.getReport(true); |
277 | if (this.errors.length > 0) { |
278 | log += '\n' + this.errors.join('\n'); |
279 | } |
280 | |
281 | if (!this.logEl_) { |
282 | var el = document.getElementById('closureTestRunnerLog'); |
283 | if (el == null) { |
284 | el = document.createElement('div'); |
285 | document.body.appendChild(el); |
286 | } |
287 | this.logEl_ = el; |
288 | } |
289 | |
290 | // Highlight the page to indicate the overall outcome. |
291 | this.writeLog(log); |
292 | |
293 | // TODO(user): Make this work with multiple test cases (b/8603638). |
294 | var runAgainLink = document.createElement('a'); |
295 | runAgainLink.style.display = 'inline-block'; |
296 | runAgainLink.style.fontSize = 'small'; |
297 | runAgainLink.style.marginBottom = '16px'; |
298 | runAgainLink.href = ''; |
299 | runAgainLink.onclick = goog.bind(function() { |
300 | this.execute(); |
301 | return false; |
302 | }, this); |
303 | runAgainLink.innerHTML = 'Run again without reloading'; |
304 | this.logEl_.appendChild(runAgainLink); |
305 | }; |
306 | |
307 | |
308 | /** |
309 | * Writes a nicely formatted log out to the document. |
310 | * @param {string} log The string to write. |
311 | */ |
312 | goog.testing.TestRunner.prototype.writeLog = function(log) { |
313 | var lines = log.split('\n'); |
314 | for (var i = 0; i < lines.length; i++) { |
315 | var line = lines[i]; |
316 | var color; |
317 | var isFailOrError = /FAILED/.test(line) || /ERROR/.test(line); |
318 | if (/PASSED/.test(line)) { |
319 | color = 'darkgreen'; |
320 | } else if (isFailOrError) { |
321 | color = 'darkred'; |
322 | } else { |
323 | color = '#333'; |
324 | } |
325 | var div = document.createElement('div'); |
326 | if (line.substr(0, 2) == '> ') { |
327 | // The stack trace may contain links so it has to be interpreted as HTML. |
328 | div.innerHTML = line; |
329 | } else { |
330 | div.appendChild(document.createTextNode(line)); |
331 | } |
332 | |
333 | var testNameMatch = |
334 | /(\S+) (\[[^\]]*] )?: (FAILED|ERROR|PASSED)/.exec(line); |
335 | if (testNameMatch) { |
336 | // Build a URL to run the test individually. If this test was already |
337 | // part of another subset test, we need to overwrite the old runTests |
338 | // query parameter. We also need to do this without bringing in any |
339 | // extra dependencies, otherwise we could mask missing dependency bugs. |
340 | var newSearch = 'runTests=' + testNameMatch[1]; |
341 | var search = window.location.search; |
342 | if (search) { |
343 | var oldTests = /runTests=([^&]*)/.exec(search); |
344 | if (oldTests) { |
345 | newSearch = search.substr(0, oldTests.index) + |
346 | newSearch + |
347 | search.substr(oldTests.index + oldTests[0].length); |
348 | } else { |
349 | newSearch = search + '&' + newSearch; |
350 | } |
351 | } else { |
352 | newSearch = '?' + newSearch; |
353 | } |
354 | var href = window.location.href; |
355 | var hash = window.location.hash; |
356 | if (hash && hash.charAt(0) != '#') { |
357 | hash = '#' + hash; |
358 | } |
359 | href = href.split('#')[0].split('?')[0] + newSearch + hash; |
360 | |
361 | // Add the link. |
362 | var a = document.createElement('A'); |
363 | a.innerHTML = '(run individually)'; |
364 | a.style.fontSize = '0.8em'; |
365 | a.style.color = '#888'; |
366 | a.href = href; |
367 | div.appendChild(document.createTextNode(' ')); |
368 | div.appendChild(a); |
369 | } |
370 | |
371 | div.style.color = color; |
372 | div.style.font = 'normal 100% monospace'; |
373 | div.style.wordWrap = 'break-word'; |
374 | if (i == 0) { |
375 | // Highlight the first line as a header that indicates the test outcome. |
376 | div.style.padding = '20px'; |
377 | div.style.marginBottom = '10px'; |
378 | if (isFailOrError) { |
379 | div.style.border = '5px solid ' + color; |
380 | div.style.backgroundColor = '#ffeeee'; |
381 | } else { |
382 | div.style.border = '1px solid black'; |
383 | div.style.backgroundColor = '#eeffee'; |
384 | } |
385 | } |
386 | |
387 | try { |
388 | div.style.whiteSpace = 'pre-wrap'; |
389 | } catch (e) { |
390 | // NOTE(brenneman): IE raises an exception when assigning to pre-wrap. |
391 | // Thankfully, it doesn't collapse whitespace when using monospace fonts, |
392 | // so it will display correctly if we ignore the exception. |
393 | } |
394 | |
395 | if (i < 2) { |
396 | div.style.fontWeight = 'bold'; |
397 | } |
398 | this.logEl_.appendChild(div); |
399 | } |
400 | }; |
401 | |
402 | |
403 | /** |
404 | * Logs a message to the current test case. |
405 | * @param {string} s The text to output to the log. |
406 | */ |
407 | goog.testing.TestRunner.prototype.log = function(s) { |
408 | if (this.testCase) { |
409 | this.testCase.log(s); |
410 | } |
411 | }; |
412 | |
413 | |
414 | // TODO(nnaze): Properly handle serving test results when multiple test cases |
415 | // are run. |
416 | /** |
417 | * @return {Object.<string, !Array.<string>>} A map of test names to a list of |
418 | * test failures (if any) to provide formatted data for the test runner. |
419 | */ |
420 | goog.testing.TestRunner.prototype.getTestResults = function() { |
421 | if (this.testCase) { |
422 | return this.testCase.getTestResults(); |
423 | } |
424 | return null; |
425 | }; |