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