| OLD | NEW |
| (Empty) | |
| 1 /** |
| 2 * @license |
| 3 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| 4 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt |
| 5 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt |
| 6 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt |
| 7 * Code distributed by Google as part of the polymer project is also |
| 8 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt |
| 9 */ |
| 10 |
| 11 /** |
| 12 * THIS FILE IS AUTOMATICALLY GENERATED! |
| 13 * To make changes to browser.js, please edit the source files in the repo's `br
owser/` directory! |
| 14 */ |
| 15 |
| 16 (function () { |
| 17 'use strict'; |
| 18 |
| 19 /** |
| 20 * @param {function()} callback A function to call when the active web component |
| 21 * frameworks have loaded. |
| 22 */ |
| 23 function whenFrameworksReady(callback) { |
| 24 debug('whenFrameworksReady'); |
| 25 var done = function() { |
| 26 debug('whenFrameworksReady done'); |
| 27 callback(); |
| 28 }; |
| 29 |
| 30 // If webcomponents script is in the document, wait for WebComponentsReady. |
| 31 if (window.WebComponents && !window.WebComponents.ready) { |
| 32 debug('WebComponentsReady?'); |
| 33 window.addEventListener('WebComponentsReady', function wcReady() { |
| 34 window.removeEventListener('WebComponentsReady', wcReady); |
| 35 debug('WebComponentsReady'); |
| 36 done(); |
| 37 }); |
| 38 } else { |
| 39 done(); |
| 40 } |
| 41 } |
| 42 |
| 43 /** |
| 44 * @param {number} count |
| 45 * @param {string} kind |
| 46 * @return {string} '<count> <kind> tests' or '<count> <kind> test'. |
| 47 */ |
| 48 function pluralizedStat(count, kind) { |
| 49 if (count === 1) { |
| 50 return count + ' ' + kind + ' test'; |
| 51 } else { |
| 52 return count + ' ' + kind + ' tests'; |
| 53 } |
| 54 } |
| 55 |
| 56 /** |
| 57 * @param {string} path The URI of the script to load. |
| 58 * @param {function} done |
| 59 */ |
| 60 function loadScript(path, done) { |
| 61 var script = document.createElement('script'); |
| 62 script.src = path; |
| 63 if (done) { |
| 64 script.onload = done.bind(null, null); |
| 65 script.onerror = done.bind(null, 'Failed to load script ' + script.src); |
| 66 } |
| 67 document.head.appendChild(script); |
| 68 } |
| 69 |
| 70 /** |
| 71 * @param {string} path The URI of the stylesheet to load. |
| 72 * @param {function} done |
| 73 */ |
| 74 function loadStyle(path, done) { |
| 75 var link = document.createElement('link'); |
| 76 link.rel = 'stylesheet'; |
| 77 link.href = path; |
| 78 if (done) { |
| 79 link.onload = done.bind(null, null); |
| 80 link.onerror = done.bind(null, 'Failed to load stylesheet ' + link.href); |
| 81 } |
| 82 document.head.appendChild(link); |
| 83 } |
| 84 |
| 85 /** |
| 86 * @param {...*} var_args Logs values to the console when the `debug` |
| 87 * configuration option is true. |
| 88 */ |
| 89 function debug(var_args) { |
| 90 if (!get('verbose')) return; |
| 91 var args = [window.location.pathname]; |
| 92 args.push.apply(args, arguments); |
| 93 (console.debug || console.log).apply(console, args); |
| 94 } |
| 95 |
| 96 // URL Processing |
| 97 |
| 98 /** |
| 99 * @param {string} url |
| 100 * @return {{base: string, params: string}} |
| 101 */ |
| 102 function parseUrl(url) { |
| 103 var parts = url.match(/^(.*?)(?:\?(.*))?$/); |
| 104 return { |
| 105 base: parts[1], |
| 106 params: getParams(parts[2] || ''), |
| 107 }; |
| 108 } |
| 109 |
| 110 /** |
| 111 * Expands a URL that may or may not be relative to `base`. |
| 112 * |
| 113 * @param {string} url |
| 114 * @param {string} base |
| 115 * @return {string} |
| 116 */ |
| 117 function expandUrl(url, base) { |
| 118 if (!base) return url; |
| 119 if (url.match(/^(\/|https?:\/\/)/)) return url; |
| 120 if (base.substr(base.length - 1) !== '/') { |
| 121 base = base + '/'; |
| 122 } |
| 123 return base + url; |
| 124 } |
| 125 |
| 126 /** |
| 127 * @param {string=} opt_query A query string to parse. |
| 128 * @return {!Object<string, !Array<string>>} All params on the URL's query. |
| 129 */ |
| 130 function getParams(opt_query) { |
| 131 var query = typeof opt_query === 'string' ? opt_query : window.location.search
; |
| 132 if (query.substring(0, 1) === '?') { |
| 133 query = query.substring(1); |
| 134 } |
| 135 // python's SimpleHTTPServer tacks a `/` on the end of query strings :( |
| 136 if (query.slice(-1) === '/') { |
| 137 query = query.substring(0, query.length - 1); |
| 138 } |
| 139 if (query === '') return {}; |
| 140 |
| 141 var result = {}; |
| 142 query.split('&').forEach(function(part) { |
| 143 var pair = part.split('='); |
| 144 if (pair.length !== 2) { |
| 145 console.warn('Invalid URL query part:', part); |
| 146 return; |
| 147 } |
| 148 var key = decodeURIComponent(pair[0]); |
| 149 var value = decodeURIComponent(pair[1]); |
| 150 |
| 151 if (!result[key]) { |
| 152 result[key] = []; |
| 153 } |
| 154 result[key].push(value); |
| 155 }); |
| 156 |
| 157 return result; |
| 158 } |
| 159 |
| 160 /** |
| 161 * Merges params from `source` into `target` (mutating `target`). |
| 162 * |
| 163 * @param {!Object<string, !Array<string>>} target |
| 164 * @param {!Object<string, !Array<string>>} source |
| 165 */ |
| 166 function mergeParams(target, source) { |
| 167 Object.keys(source).forEach(function(key) { |
| 168 if (!(key in target)) { |
| 169 target[key] = []; |
| 170 } |
| 171 target[key] = target[key].concat(source[key]); |
| 172 }); |
| 173 } |
| 174 |
| 175 /** |
| 176 * @param {string} param The param to return a value for. |
| 177 * @return {?string} The first value for `param`, if found. |
| 178 */ |
| 179 function getParam(param) { |
| 180 var params = getParams(); |
| 181 return params[param] ? params[param][0] : null; |
| 182 } |
| 183 |
| 184 /** |
| 185 * @param {!Object<string, !Array<string>>} params |
| 186 * @return {string} `params` encoded as a URI query. |
| 187 */ |
| 188 function paramsToQuery(params) { |
| 189 var pairs = []; |
| 190 Object.keys(params).forEach(function(key) { |
| 191 params[key].forEach(function(value) { |
| 192 pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); |
| 193 }); |
| 194 }); |
| 195 return (pairs.length > 0) ? ('?' + pairs.join('&')) : ''; |
| 196 } |
| 197 |
| 198 /** |
| 199 * @param {!Location|string} location |
| 200 * @return {string} |
| 201 */ |
| 202 function basePath(location) { |
| 203 return (location.pathname || location).match(/^.*\//)[0]; |
| 204 } |
| 205 |
| 206 /** |
| 207 * @param {!Location|string} location |
| 208 * @param {string} basePath |
| 209 * @return {string} |
| 210 */ |
| 211 function relativeLocation(location, basePath) { |
| 212 var path = location.pathname || location; |
| 213 if (path.indexOf(basePath) === 0) { |
| 214 path = path.substring(basePath.length); |
| 215 } |
| 216 return path; |
| 217 } |
| 218 |
| 219 /** |
| 220 * @param {!Location|string} location |
| 221 * @return {string} |
| 222 */ |
| 223 function cleanLocation(location) { |
| 224 var path = location.pathname || location; |
| 225 if (path.slice(-11) === '/index.html') { |
| 226 path = path.slice(0, path.length - 10); |
| 227 } |
| 228 return path; |
| 229 } |
| 230 |
| 231 /** |
| 232 * Like `async.parallelLimit`, but our own so that we don't force a dependency |
| 233 * on downstream code. |
| 234 * |
| 235 * @param {!Array<function(function(*))>} runners Runners that call their given |
| 236 * Node-style callback when done. |
| 237 * @param {number|function(*)} limit Maximum number of concurrent runners. |
| 238 * (optional). |
| 239 * @param {?function(*)} done Callback that should be triggered once all runners |
| 240 * have completed, or encountered an error. |
| 241 */ |
| 242 function parallel(runners, limit, done) { |
| 243 if (typeof limit !== 'number') { |
| 244 done = limit; |
| 245 limit = 0; |
| 246 } |
| 247 if (!runners.length) return done(); |
| 248 |
| 249 var called = false; |
| 250 var total = runners.length; |
| 251 var numActive = 0; |
| 252 var numDone = 0; |
| 253 |
| 254 function runnerDone(error) { |
| 255 if (called) return; |
| 256 numDone = numDone + 1; |
| 257 numActive = numActive - 1; |
| 258 |
| 259 if (error || numDone >= total) { |
| 260 called = true; |
| 261 done(error); |
| 262 } else { |
| 263 runOne(); |
| 264 } |
| 265 } |
| 266 |
| 267 function runOne() { |
| 268 if (limit && numActive >= limit) return; |
| 269 if (!runners.length) return; |
| 270 numActive = numActive + 1; |
| 271 runners.shift()(runnerDone); |
| 272 } |
| 273 runners.forEach(runOne); |
| 274 } |
| 275 |
| 276 /** |
| 277 * Finds the directory that a loaded script is hosted on. |
| 278 * |
| 279 * @param {string} filename |
| 280 * @return {string?} |
| 281 */ |
| 282 function scriptPrefix(filename) { |
| 283 var scripts = document.querySelectorAll('script[src*="' + filename + '"]'); |
| 284 if (scripts.length !== 1) return null; |
| 285 var script = scripts[0].src; |
| 286 return script.substring(0, script.indexOf(filename)); |
| 287 } |
| 288 |
| 289 var util = Object.freeze({ |
| 290 whenFrameworksReady: whenFrameworksReady, |
| 291 pluralizedStat: pluralizedStat, |
| 292 loadScript: loadScript, |
| 293 loadStyle: loadStyle, |
| 294 debug: debug, |
| 295 parseUrl: parseUrl, |
| 296 expandUrl: expandUrl, |
| 297 getParams: getParams, |
| 298 mergeParams: mergeParams, |
| 299 getParam: getParam, |
| 300 paramsToQuery: paramsToQuery, |
| 301 basePath: basePath, |
| 302 relativeLocation: relativeLocation, |
| 303 cleanLocation: cleanLocation, |
| 304 parallel: parallel, |
| 305 scriptPrefix: scriptPrefix |
| 306 }); |
| 307 |
| 308 // TODO(thedeeno): Consider renaming subsuite. IIRC, childRunner is entirely |
| 309 // distinct from mocha suite, which tripped me up badly when trying to add |
| 310 // plugin support. Perhaps something like 'batch', or 'bundle'. Something that |
| 311 // has no mocha correlate. This may also eliminate the need for root/non-root |
| 312 // suite distinctions. |
| 313 |
| 314 /** |
| 315 * A Mocha suite (or suites) run within a child iframe, but reported as if they |
| 316 * are part of the current context. |
| 317 */ |
| 318 function ChildRunner(url, parentScope) { |
| 319 var urlBits = parseUrl(url); |
| 320 mergeParams( |
| 321 urlBits.params, getParams(parentScope.location.search)); |
| 322 delete urlBits.params.cli_browser_id; |
| 323 |
| 324 this.url = urlBits.base + paramsToQuery(urlBits.params); |
| 325 this.parentScope = parentScope; |
| 326 |
| 327 this.state = 'initializing'; |
| 328 } |
| 329 |
| 330 // ChildRunners get a pretty generous load timeout by default. |
| 331 ChildRunner.loadTimeout = 60000; |
| 332 |
| 333 // We can't maintain properties on iframe elements in Firefox/Safari/???, so we |
| 334 // track childRunners by URL. |
| 335 ChildRunner._byUrl = {}; |
| 336 |
| 337 /** |
| 338 * @return {ChildRunner} The `ChildRunner` that was registered for this window. |
| 339 */ |
| 340 ChildRunner.current = function() { |
| 341 return ChildRunner.get(window); |
| 342 }; |
| 343 |
| 344 /** |
| 345 * @param {!Window} target A window to find the ChildRunner of. |
| 346 * @param {boolean} traversal Whether this is a traversal from a child window. |
| 347 * @return {ChildRunner} The `ChildRunner` that was registered for `target`. |
| 348 */ |
| 349 ChildRunner.get = function(target, traversal) { |
| 350 var childRunner = ChildRunner._byUrl[target.location.href]; |
| 351 if (childRunner) return childRunner; |
| 352 if (window.parent === window) { // Top window. |
| 353 if (traversal) { |
| 354 console.warn('Subsuite loaded but was never registered. This most likely i
s due to wonky history behavior. Reloading...'); |
| 355 window.location.reload(); |
| 356 } |
| 357 return null; |
| 358 } |
| 359 // Otherwise, traverse. |
| 360 return window.parent.WCT._ChildRunner.get(target, true); |
| 361 }; |
| 362 |
| 363 /** |
| 364 * Loads and runs the subsuite. |
| 365 * |
| 366 * @param {function} done Node-style callback. |
| 367 */ |
| 368 ChildRunner.prototype.run = function(done) { |
| 369 debug('ChildRunner#run', this.url); |
| 370 this.state = 'loading'; |
| 371 this.onRunComplete = done; |
| 372 |
| 373 this.iframe = document.createElement('iframe'); |
| 374 this.iframe.src = this.url; |
| 375 this.iframe.classList.add('subsuite'); |
| 376 |
| 377 var container = document.getElementById('subsuites'); |
| 378 if (!container) { |
| 379 container = document.createElement('div'); |
| 380 container.id = 'subsuites'; |
| 381 document.body.appendChild(container); |
| 382 } |
| 383 container.appendChild(this.iframe); |
| 384 |
| 385 // let the iframe expand the URL for us. |
| 386 this.url = this.iframe.src; |
| 387 ChildRunner._byUrl[this.url] = this; |
| 388 |
| 389 this.timeoutId = setTimeout( |
| 390 this.loaded.bind(this, new Error('Timed out loading ' + this.url)), ChildR
unner.loadTimeout); |
| 391 |
| 392 this.iframe.addEventListener('error', |
| 393 this.loaded.bind(this, new Error('Failed to load document ' + this.url))); |
| 394 |
| 395 this.iframe.contentWindow.addEventListener('DOMContentLoaded', this.loaded.bin
d(this, null)); |
| 396 }; |
| 397 |
| 398 /** |
| 399 * Called when the sub suite's iframe has loaded (or errored during load). |
| 400 * |
| 401 * @param {*} error The error that occured, if any. |
| 402 */ |
| 403 ChildRunner.prototype.loaded = function(error) { |
| 404 debug('ChildRunner#loaded', this.url, error); |
| 405 |
| 406 // Not all targets have WCT loaded (compatiblity mode) |
| 407 if (this.iframe.contentWindow.WCT) { |
| 408 this.share = this.iframe.contentWindow.WCT.share; |
| 409 } |
| 410 |
| 411 if (error) { |
| 412 this.signalRunComplete(error); |
| 413 this.done(); |
| 414 } |
| 415 }; |
| 416 |
| 417 /** |
| 418 * Called in mocha/run.js when all dependencies have loaded, and the child is |
| 419 * ready to start running tests |
| 420 * |
| 421 * @param {*} error The error that occured, if any. |
| 422 */ |
| 423 ChildRunner.prototype.ready = function(error) { |
| 424 debug('ChildRunner#ready', this.url, error); |
| 425 if (this.timeoutId) { |
| 426 clearTimeout(this.timeoutId); |
| 427 } |
| 428 if (error) { |
| 429 this.signalRunComplete(error); |
| 430 this.done(); |
| 431 } |
| 432 }; |
| 433 |
| 434 /** Called when the sub suite's tests are complete, so that it can clean up. */ |
| 435 ChildRunner.prototype.done = function done() { |
| 436 debug('ChildRunner#done', this.url, arguments); |
| 437 |
| 438 // make sure to clear that timeout |
| 439 this.ready(); |
| 440 this.signalRunComplete(); |
| 441 |
| 442 if (!this.iframe) return; |
| 443 // Be safe and avoid potential browser crashes when logic attempts to interact |
| 444 // with the removed iframe. |
| 445 setTimeout(function() { |
| 446 this.iframe.parentNode.removeChild(this.iframe); |
| 447 this.iframe = null; |
| 448 }.bind(this), 1); |
| 449 }; |
| 450 |
| 451 ChildRunner.prototype.signalRunComplete = function signalRunComplete(error) { |
| 452 if (!this.onRunComplete) return; |
| 453 this.state = 'complete'; |
| 454 this.onRunComplete(error); |
| 455 this.onRunComplete = null; |
| 456 }; |
| 457 |
| 458 /** |
| 459 * The global configuration state for WCT's browser client. |
| 460 */ |
| 461 var _config = { |
| 462 /** |
| 463 * `.js` scripts to be loaded (synchronously) before WCT starts in earnest. |
| 464 * |
| 465 * Paths are relative to `scriptPrefix`. |
| 466 */ |
| 467 environmentScripts: [ |
| 468 'stacky/browser.js', |
| 469 'async/lib/async.js', |
| 470 'lodash/lodash.js', |
| 471 'mocha/mocha.js', |
| 472 'chai/chai.js', |
| 473 'sinonjs/sinon.js', |
| 474 'sinon-chai/lib/sinon-chai.js', |
| 475 'accessibility-developer-tools/dist/js/axs_testing.js' |
| 476 ], |
| 477 |
| 478 environmentImports: [ |
| 479 'test-fixture/test-fixture.html' |
| 480 ], |
| 481 |
| 482 /** Absolute root for client scripts. Detected in `setup()` if not set. */ |
| 483 root: null, |
| 484 |
| 485 /** By default, we wait for any web component frameworks to load. */ |
| 486 waitForFrameworks: true, |
| 487 |
| 488 /** Alternate callback for waiting for tests. |
| 489 * `this` for the callback will be the window currently running tests. |
| 490 */ |
| 491 waitFor: null, |
| 492 |
| 493 /** How many `.html` suites that can be concurrently loaded & run. */ |
| 494 numConcurrentSuites: 1, |
| 495 |
| 496 /** Whether `console.error` should be treated as a test failure. */ |
| 497 trackConsoleError: true, |
| 498 |
| 499 /** Configuration passed to mocha.setup. */ |
| 500 mochaOptions: { |
| 501 timeout: 10 * 1000 |
| 502 }, |
| 503 |
| 504 /** Whether WCT should emit (extremely verbose) debugging log messages. */ |
| 505 verbose: false, |
| 506 }; |
| 507 |
| 508 /** |
| 509 * Merges initial `options` into WCT's global configuration. |
| 510 * |
| 511 * @param {Object} options The options to merge. See `browser/config.js` for a |
| 512 * reference. |
| 513 */ |
| 514 function setup(options) { |
| 515 var childRunner = ChildRunner.current(); |
| 516 if (childRunner) { |
| 517 _deepMerge(_config, childRunner.parentScope.WCT._config); |
| 518 // But do not force the mocha UI |
| 519 delete _config.mochaOptions.ui; |
| 520 } |
| 521 |
| 522 if (options && typeof options === 'object') { |
| 523 _deepMerge(_config, options); |
| 524 } |
| 525 |
| 526 if (!_config.root) { |
| 527 // Sibling dependencies. |
| 528 var root = scriptPrefix('browser.js'); |
| 529 _config.root = basePath(root.substr(0, root.length - 1)); |
| 530 if (!_config.root) { |
| 531 throw new Error('Unable to detect root URL for WCT sources. Please set WCT
.root before including browser.js'); |
| 532 } |
| 533 } |
| 534 } |
| 535 |
| 536 /** |
| 537 * Retrieves a configuration value. |
| 538 * |
| 539 * @param {string} key |
| 540 * @return {*} |
| 541 */ |
| 542 function get(key) { |
| 543 return _config[key]; |
| 544 } |
| 545 |
| 546 // Internal |
| 547 |
| 548 function _deepMerge(target, source) { |
| 549 Object.keys(source).forEach(function (key) { |
| 550 if (target[key] !== null && typeof target[key] === 'object' && !Array.isArra
y(target[key])) { |
| 551 _deepMerge(target[key], source[key]); |
| 552 } else { |
| 553 target[key] = source[key]; |
| 554 } |
| 555 }); |
| 556 } |
| 557 |
| 558 var htmlSuites$1 = []; |
| 559 var jsSuites$1 = []; |
| 560 |
| 561 // We process grep ourselves to avoid loading suites that will be filtered. |
| 562 var GREP = getParam('grep'); |
| 563 // work around mocha bug (https://github.com/mochajs/mocha/issues/2070) |
| 564 if (GREP) { |
| 565 GREP = GREP.replace(/\\\./g, '.'); |
| 566 } |
| 567 |
| 568 /** |
| 569 * Loads suites of tests, supporting both `.js` and `.html` files. |
| 570 * |
| 571 * @param {!Array.<string>} files The files to load. |
| 572 */ |
| 573 function loadSuites(files) { |
| 574 files.forEach(function(file) { |
| 575 if (/\.js(\?.*)?$/.test(file)) { |
| 576 jsSuites$1.push(file); |
| 577 } else if (/\.html(\?.*)?$/.test(file)) { |
| 578 htmlSuites$1.push(file); |
| 579 } else { |
| 580 throw new Error('Unknown resource type: ' + file); |
| 581 } |
| 582 }); |
| 583 } |
| 584 |
| 585 /** |
| 586 * @return {!Array.<string>} The child suites that should be loaded, ignoring |
| 587 * those that would not match `GREP`. |
| 588 */ |
| 589 function activeChildSuites() { |
| 590 var subsuites = htmlSuites$1; |
| 591 if (GREP) { |
| 592 var cleanSubsuites = []; |
| 593 for (var i = 0, subsuite; subsuite = subsuites[i]; i++) { |
| 594 if (GREP.indexOf(cleanLocation(subsuite)) !== -1) { |
| 595 cleanSubsuites.push(subsuite); |
| 596 } |
| 597 } |
| 598 subsuites = cleanSubsuites; |
| 599 } |
| 600 return subsuites; |
| 601 } |
| 602 |
| 603 /** |
| 604 * Loads all `.js` sources requested by the current suite. |
| 605 * |
| 606 * @param {!MultiReporter} reporter |
| 607 * @param {function} done |
| 608 */ |
| 609 function loadJsSuites(reporter, done) { |
| 610 debug('loadJsSuites', jsSuites$1); |
| 611 |
| 612 var loaders = jsSuites$1.map(function(file) { |
| 613 // We only support `.js` dependencies for now. |
| 614 return loadScript.bind(util, file); |
| 615 }); |
| 616 |
| 617 parallel(loaders, done); |
| 618 } |
| 619 |
| 620 /** |
| 621 * @param {!MultiReporter} reporter |
| 622 * @param {!Array.<string>} childSuites |
| 623 * @param {function} done |
| 624 */ |
| 625 function runSuites(reporter, childSuites, done) { |
| 626 debug('runSuites'); |
| 627 |
| 628 var suiteRunners = [ |
| 629 // Run the local tests (if any) first, not stopping on error; |
| 630 _runMocha.bind(null, reporter), |
| 631 ]; |
| 632 |
| 633 // As well as any sub suites. Again, don't stop on error. |
| 634 childSuites.forEach(function(file) { |
| 635 suiteRunners.push(function(next) { |
| 636 var childRunner = new ChildRunner(file, window); |
| 637 reporter.emit('childRunner start', childRunner); |
| 638 childRunner.run(function(error) { |
| 639 reporter.emit('childRunner end', childRunner); |
| 640 if (error) reporter.emitOutOfBandTest(file, error); |
| 641 next(); |
| 642 }); |
| 643 }); |
| 644 }); |
| 645 |
| 646 parallel(suiteRunners, get('numConcurrentSuites'), function(error) { |
| 647 reporter.done(); |
| 648 done(error); |
| 649 }); |
| 650 } |
| 651 |
| 652 /** |
| 653 * Kicks off a mocha run, waiting for frameworks to load if necessary. |
| 654 * |
| 655 * @param {!MultiReporter} reporter Where to send Mocha's events. |
| 656 * @param {function} done A callback fired, _no error is passed_. |
| 657 */ |
| 658 function _runMocha(reporter, done, waited) { |
| 659 if (get('waitForFrameworks') && !waited) { |
| 660 var waitFor = (get('waitFor') || whenFrameworksReady).bind(window); |
| 661 waitFor(function() { |
| 662 _fixCustomElements(); |
| 663 _runMocha(reporter, done, true); |
| 664 }); |
| 665 return; |
| 666 } |
| 667 debug('_runMocha'); |
| 668 var mocha = window.mocha; |
| 669 var Mocha = window.Mocha; |
| 670 |
| 671 mocha.reporter(reporter.childReporter(window.location)); |
| 672 mocha.suite.title = reporter.suiteTitle(window.location); |
| 673 mocha.grep(GREP); |
| 674 |
| 675 // We can't use `mocha.run` because it bashes over grep, invert, and friends. |
| 676 // See https://github.com/visionmedia/mocha/blob/master/support/tail.js#L137 |
| 677 var runner = Mocha.prototype.run.call(mocha, function(error) { |
| 678 if (document.getElementById('mocha')) { |
| 679 Mocha.utils.highlightTags('code'); |
| 680 } |
| 681 done(); // We ignore the Mocha failure count. |
| 682 }); |
| 683 |
| 684 // Mocha's default `onerror` handling strips the stack (to support really old |
| 685 // browsers). We upgrade this to get better stacks for async errors. |
| 686 // |
| 687 // TODO(nevir): Can we expand support to other browsers? |
| 688 if (navigator.userAgent.match(/chrome/i)) { |
| 689 window.onerror = null; |
| 690 window.addEventListener('error', function(event) { |
| 691 if (!event.error) return; |
| 692 if (event.error.ignore) return; |
| 693 runner.uncaught(event.error); |
| 694 }); |
| 695 } |
| 696 } |
| 697 /** |
| 698 * In Chrome57 custom elements in the document might not get upgraded when |
| 699 * there is a high GC https://bugs.chromium.org/p/chromium/issues/detail?id=7016
01 |
| 700 * We clone and replace the ones that weren't upgraded. |
| 701 */ |
| 702 function _fixCustomElements() { |
| 703 // Bail out if it is not Chrome 57. |
| 704 var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); |
| 705 var isM57 = raw && raw[2] === '57'; |
| 706 if (!isM57) return; |
| 707 |
| 708 var elements = document.body.querySelectorAll('*:not(script):not(style)'); |
| 709 var constructors = {}; |
| 710 for (var i = 0; i < elements.length; i++) { |
| 711 var el = elements[i]; |
| 712 // This child has already been cloned and replaced by its parent, skip it! |
| 713 if (!el.isConnected) continue; |
| 714 |
| 715 var tag = el.localName; |
| 716 // Not a custom element! |
| 717 if (tag.indexOf('-') === -1) continue; |
| 718 |
| 719 // Memoize correct constructors. |
| 720 constructors[tag] = constructors[tag] || document.createElement(tag).constru
ctor; |
| 721 // This one was correctly upgraded. |
| 722 if (el instanceof constructors[tag]) continue; |
| 723 |
| 724 debug('_fixCustomElements: found non-upgraded custom element ' + el); |
| 725 var clone = document.importNode(el, true); |
| 726 el.parentNode.replaceChild(clone, el); |
| 727 } |
| 728 } |
| 729 |
| 730 // We capture console events when running tests; so make sure we have a |
| 731 // reference to the original one. |
| 732 var console$1 = window.console; |
| 733 |
| 734 var FONT = ';font: normal 13px "Roboto", "Helvetica Neue", "Helvetica", sans-ser
if;'; |
| 735 var STYLES = { |
| 736 plain: FONT, |
| 737 suite: 'color: #5c6bc0' + FONT, |
| 738 test: FONT, |
| 739 passing: 'color: #259b24' + FONT, |
| 740 pending: 'color: #e65100' + FONT, |
| 741 failing: 'color: #c41411' + FONT, |
| 742 stack: 'color: #c41411', |
| 743 results: FONT + 'font-size: 16px', |
| 744 }; |
| 745 |
| 746 // I don't think we can feature detect this one... |
| 747 var userAgent = navigator.userAgent.toLowerCase(); |
| 748 var CAN_STYLE_LOG = userAgent.match('firefox') || userAgent.match('webkit'); |
| 749 var CAN_STYLE_GROUP = userAgent.match('webkit'); |
| 750 // Track the indent for faked `console.group` |
| 751 var logIndent = ''; |
| 752 |
| 753 function log(text, style) { |
| 754 text = text.split('\n').map(function(l) { return logIndent + l; }).join('\n'); |
| 755 if (CAN_STYLE_LOG) { |
| 756 console$1.log('%c' + text, STYLES[style] || STYLES.plain); |
| 757 } else { |
| 758 console$1.log(text); |
| 759 } |
| 760 } |
| 761 |
| 762 function logGroup(text, style) { |
| 763 if (CAN_STYLE_GROUP) { |
| 764 console$1.group('%c' + text, STYLES[style] || STYLES.plain); |
| 765 } else if (console$1.group) { |
| 766 console$1.group(text); |
| 767 } else { |
| 768 logIndent = logIndent + ' '; |
| 769 log(text, style); |
| 770 } |
| 771 } |
| 772 |
| 773 function logGroupEnd() { |
| 774 if (console$1.groupEnd) { |
| 775 console$1.groupEnd(); |
| 776 } else { |
| 777 logIndent = logIndent.substr(0, logIndent.length - 2); |
| 778 } |
| 779 } |
| 780 |
| 781 function logException(error) { |
| 782 log(error.stack || error.message || error, 'stack'); |
| 783 } |
| 784 |
| 785 /** |
| 786 * A Mocha reporter that logs results out to the web `console`. |
| 787 * |
| 788 * @param {!Mocha.Runner} runner The runner that is being reported on. |
| 789 */ |
| 790 function Console(runner) { |
| 791 Mocha.reporters.Base.call(this, runner); |
| 792 |
| 793 runner.on('suite', function(suite) { |
| 794 if (suite.root) return; |
| 795 logGroup(suite.title, 'suite'); |
| 796 }.bind(this)); |
| 797 |
| 798 runner.on('suite end', function(suite) { |
| 799 if (suite.root) return; |
| 800 logGroupEnd(); |
| 801 }.bind(this)); |
| 802 |
| 803 runner.on('test', function(test) { |
| 804 logGroup(test.title, 'test'); |
| 805 }.bind(this)); |
| 806 |
| 807 runner.on('pending', function(test) { |
| 808 logGroup(test.title, 'pending'); |
| 809 }.bind(this)); |
| 810 |
| 811 runner.on('fail', function(test, error) { |
| 812 logException(error); |
| 813 }.bind(this)); |
| 814 |
| 815 runner.on('test end', function(test) { |
| 816 logGroupEnd(); |
| 817 }.bind(this)); |
| 818 |
| 819 runner.on('end', this.logSummary.bind(this)); |
| 820 } |
| 821 |
| 822 /** Prints out a final summary of test results. */ |
| 823 Console.prototype.logSummary = function logSummary() { |
| 824 logGroup('Test Results', 'results'); |
| 825 |
| 826 if (this.stats.failures > 0) { |
| 827 log(pluralizedStat(this.stats.failures, 'failing'), 'failing'); |
| 828 } |
| 829 if (this.stats.pending > 0) { |
| 830 log(pluralizedStat(this.stats.pending, 'pending'), 'pending'); |
| 831 } |
| 832 log(pluralizedStat(this.stats.passes, 'passing')); |
| 833 |
| 834 if (!this.stats.failures) { |
| 835 log('test suite passed', 'passing'); |
| 836 } |
| 837 log('Evaluated ' + this.stats.tests + ' tests in ' + this.stats.duration + 'ms
.'); |
| 838 logGroupEnd(); |
| 839 }; |
| 840 |
| 841 /** |
| 842 * @license |
| 843 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| 844 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt |
| 845 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt |
| 846 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt |
| 847 * Code distributed by Google as part of the polymer project is also |
| 848 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt |
| 849 */ |
| 850 |
| 851 /** |
| 852 * WCT-specific behavior on top of Mocha's default HTML reporter. |
| 853 * |
| 854 * @param {!Mocha.Runner} runner The runner that is being reported on. |
| 855 */ |
| 856 function HTML(runner) { |
| 857 var output = document.createElement('div'); |
| 858 output.id = 'mocha'; |
| 859 document.body.appendChild(output); |
| 860 |
| 861 runner.on('suite', function(test) { |
| 862 this.total = runner.total; |
| 863 }.bind(this)); |
| 864 |
| 865 Mocha.reporters.HTML.call(this, runner); |
| 866 } |
| 867 |
| 868 // Woo! What a hack. This just saves us from adding a bunch of complexity around |
| 869 // style loading. |
| 870 var style = document.createElement('style'); |
| 871 style.textContent = 'html, body {' + |
| 872 ' position: relative;' + |
| 873 ' height: 100%;' + |
| 874 ' width: 100%;' + |
| 875 ' min-width: 900px;' + |
| 876 '}' + |
| 877 '#mocha, #subsuites {' + |
| 878 ' height: 100%;' + |
| 879 ' position: absolute;' + |
| 880 ' top: 0;' + |
| 881 '}' + |
| 882 '#mocha {' + |
| 883 ' box-sizing: border-box;' + |
| 884 ' margin: 0 !important;' + |
| 885 ' overflow-y: auto;' + |
| 886 ' padding: 60px 20px;' + |
| 887 ' right: 0;' + |
| 888 ' left: 500px;' + |
| 889 '}' + |
| 890 '#subsuites {' + |
| 891 ' -ms-flex-direction: column;' + |
| 892 ' -webkit-flex-direction: column;' + |
| 893 ' display: -ms-flexbox;' + |
| 894 ' display: -webkit-flex;' + |
| 895 ' display: flex;' + |
| 896 ' flex-direction: column;' + |
| 897 ' left: 0;' + |
| 898 ' width: 500px;' + |
| 899 '}' + |
| 900 '#subsuites .subsuite {' + |
| 901 ' border: 0;' + |
| 902 ' width: 100%;' + |
| 903 ' height: 100%;' + |
| 904 '}' + |
| 905 '#mocha .test.pass .duration {' + |
| 906 ' color: #555 !important;' + |
| 907 '}'; |
| 908 document.head.appendChild(style); |
| 909 |
| 910 var STACKY_CONFIG = { |
| 911 indent: ' ', |
| 912 locationStrip: [ |
| 913 /^https?:\/\/[^\/]+/, |
| 914 /\?.*$/, |
| 915 ], |
| 916 filter: function(line) { |
| 917 return line.location.match(/\/web-component-tester\/[^\/]+(\?.*)?$/); |
| 918 }, |
| 919 }; |
| 920 |
| 921 // https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36-46 |
| 922 var MOCHA_EVENTS = [ |
| 923 'start', |
| 924 'end', |
| 925 'suite', |
| 926 'suite end', |
| 927 'test', |
| 928 'test end', |
| 929 'hook', |
| 930 'hook end', |
| 931 'pass', |
| 932 'fail', |
| 933 'pending', |
| 934 'childRunner end' |
| 935 ]; |
| 936 |
| 937 // Until a suite has loaded, we assume this many tests in it. |
| 938 var ESTIMATED_TESTS_PER_SUITE = 3; |
| 939 |
| 940 /** |
| 941 * A Mocha-like reporter that combines the output of multiple Mocha suites. |
| 942 * |
| 943 * @param {number} numSuites The number of suites that will be run, in order to |
| 944 * estimate the total number of tests that will be performed. |
| 945 * @param {!Array.<!Mocha.reporters.Base>} reporters The set of reporters that |
| 946 * should receive the unified event stream. |
| 947 * @param {MultiReporter} parent The parent reporter, if present. |
| 948 */ |
| 949 function MultiReporter(numSuites, reporters, parent) { |
| 950 this.reporters = reporters.map(function(reporter) { |
| 951 return new reporter(this); |
| 952 }.bind(this)); |
| 953 |
| 954 this.parent = parent; |
| 955 this.basePath = parent && parent.basePath || basePath(window.location); |
| 956 |
| 957 this.total = numSuites * ESTIMATED_TESTS_PER_SUITE; |
| 958 // Mocha reporters assume a stream of events, so we have to be careful to only |
| 959 // report on one runner at a time... |
| 960 this.currentRunner = null; |
| 961 // ...while we buffer events for any other active runners. |
| 962 this.pendingEvents = []; |
| 963 |
| 964 this.emit('start'); |
| 965 } |
| 966 |
| 967 /** |
| 968 * @param {!Location|string} location The location this reporter represents. |
| 969 * @return {!Mocha.reporters.Base} A reporter-like "class" for each child suite |
| 970 * that should be passed to `mocha.run`. |
| 971 */ |
| 972 MultiReporter.prototype.childReporter = function childReporter(location) { |
| 973 var name = this.suiteTitle(location); |
| 974 // The reporter is used as a constructor, so we can't depend on `this` being |
| 975 // properly bound. |
| 976 var self = this; |
| 977 function reporter(runner) { |
| 978 runner.name = name; |
| 979 self.bindChildRunner(runner); |
| 980 } |
| 981 reporter.title = name; |
| 982 return reporter; |
| 983 }; |
| 984 |
| 985 /** Must be called once all runners have finished. */ |
| 986 MultiReporter.prototype.done = function done() { |
| 987 this.complete = true; |
| 988 this.flushPendingEvents(); |
| 989 this.emit('end'); |
| 990 }; |
| 991 |
| 992 /** |
| 993 * Emit a top level test that is not part of any suite managed by this reporter. |
| 994 * |
| 995 * Helpful for reporting on global errors, loading issues, etc. |
| 996 * |
| 997 * @param {string} title The title of the test. |
| 998 * @param {*} opt_error An error associated with this test. If falsy, test is |
| 999 * considered to be passing. |
| 1000 * @param {string} opt_suiteTitle Title for the suite that's wrapping the test. |
| 1001 * @param {?boolean} opt_estimated If this test was included in the original |
| 1002 * estimate of `numSuites`. |
| 1003 */ |
| 1004 MultiReporter.prototype.emitOutOfBandTest = function emitOutOfBandTest(title, op
t_error, opt_suiteTitle, opt_estimated) { |
| 1005 debug('MultiReporter#emitOutOfBandTest(', arguments, ')'); |
| 1006 var root = new Mocha.Suite(); |
| 1007 root.title = opt_suiteTitle || ''; |
| 1008 var test = new Mocha.Test(title, function() { |
| 1009 }); |
| 1010 test.parent = root; |
| 1011 test.state = opt_error ? 'failed' : 'passed'; |
| 1012 test.err = opt_error; |
| 1013 |
| 1014 if (!opt_estimated) { |
| 1015 this.total = this.total + ESTIMATED_TESTS_PER_SUITE; |
| 1016 } |
| 1017 |
| 1018 var runner = {total: 1}; |
| 1019 this.proxyEvent('start', runner); |
| 1020 this.proxyEvent('suite', runner, root); |
| 1021 this.proxyEvent('test', runner, test); |
| 1022 if (opt_error) { |
| 1023 this.proxyEvent('fail', runner, test, opt_error); |
| 1024 } else { |
| 1025 this.proxyEvent('pass', runner, test); |
| 1026 } |
| 1027 this.proxyEvent('test end', runner, test); |
| 1028 this.proxyEvent('suite end', runner, root); |
| 1029 this.proxyEvent('end', runner); |
| 1030 }; |
| 1031 |
| 1032 /** |
| 1033 * @param {!Location|string} location |
| 1034 * @return {string} |
| 1035 */ |
| 1036 MultiReporter.prototype.suiteTitle = function suiteTitle(location) { |
| 1037 var path = relativeLocation(location, this.basePath); |
| 1038 path = cleanLocation(path); |
| 1039 return path; |
| 1040 }; |
| 1041 |
| 1042 // Internal Interface |
| 1043 |
| 1044 /** @param {!Mocha.runners.Base} runner The runner to listen to events for. */ |
| 1045 MultiReporter.prototype.bindChildRunner = function bindChildRunner(runner) { |
| 1046 MOCHA_EVENTS.forEach(function(eventName) { |
| 1047 runner.on(eventName, this.proxyEvent.bind(this, eventName, runner)); |
| 1048 }.bind(this)); |
| 1049 }; |
| 1050 |
| 1051 /** |
| 1052 * Evaluates an event fired by `runner`, proxying it forward or buffering it. |
| 1053 * |
| 1054 * @param {string} eventName |
| 1055 * @param {!Mocha.runners.Base} runner The runner that emitted this event. |
| 1056 * @param {...*} var_args Any additional data passed as part of the event. |
| 1057 */ |
| 1058 MultiReporter.prototype.proxyEvent = function proxyEvent(eventName, runner, var_
args) { |
| 1059 var extraArgs = Array.prototype.slice.call(arguments, 2); |
| 1060 if (this.complete) { |
| 1061 console.warn('out of order Mocha event for ' + runner.name + ':', eventName,
extraArgs); |
| 1062 return; |
| 1063 } |
| 1064 |
| 1065 if (this.currentRunner && runner !== this.currentRunner) { |
| 1066 this.pendingEvents.push(arguments); |
| 1067 return; |
| 1068 } |
| 1069 debug('MultiReporter#proxyEvent(', arguments, ')'); |
| 1070 |
| 1071 // This appears to be a Mocha bug: Tests failed by passing an error to their |
| 1072 // done function don't set `err` properly. |
| 1073 // |
| 1074 // TODO(nevir): Track down. |
| 1075 if (eventName === 'fail' && !extraArgs[0].err) { |
| 1076 extraArgs[0].err = extraArgs[1]; |
| 1077 } |
| 1078 |
| 1079 if (eventName === 'start') { |
| 1080 this.onRunnerStart(runner); |
| 1081 } else if (eventName === 'end') { |
| 1082 this.onRunnerEnd(runner); |
| 1083 } else { |
| 1084 this.cleanEvent(eventName, runner, extraArgs); |
| 1085 this.emit.apply(this, [eventName].concat(extraArgs)); |
| 1086 } |
| 1087 }; |
| 1088 |
| 1089 /** |
| 1090 * Cleans or modifies an event if needed. |
| 1091 * |
| 1092 * @param {string} eventName |
| 1093 * @param {!Mocha.runners.Base} runner The runner that emitted this event. |
| 1094 * @param {!Array.<*>} extraArgs |
| 1095 */ |
| 1096 MultiReporter.prototype.cleanEvent = function cleanEvent(eventName, runner, extr
aArgs) { |
| 1097 // Suite hierarchy |
| 1098 if (extraArgs[0]) { |
| 1099 extraArgs[0] = this.showRootSuite(extraArgs[0]); |
| 1100 } |
| 1101 |
| 1102 // Normalize errors |
| 1103 if (eventName === 'fail') { |
| 1104 extraArgs[1] = Stacky.normalize(extraArgs[1], STACKY_CONFIG); |
| 1105 } |
| 1106 if (extraArgs[0] && extraArgs[0].err) { |
| 1107 extraArgs[0].err = Stacky.normalize(extraArgs[0].err, STACKY_CONFIG); |
| 1108 } |
| 1109 }; |
| 1110 |
| 1111 /** |
| 1112 * We like to show the root suite's title, which requires a little bit of |
| 1113 * trickery in the suite hierarchy. |
| 1114 * |
| 1115 * @param {!Mocha.Runnable} node |
| 1116 */ |
| 1117 MultiReporter.prototype.showRootSuite = function showRootSuite(node) { |
| 1118 var leaf = node = Object.create(node); |
| 1119 while (node && node.parent) { |
| 1120 var wrappedParent = Object.create(node.parent); |
| 1121 node.parent = wrappedParent; |
| 1122 node = wrappedParent; |
| 1123 } |
| 1124 node.root = false; |
| 1125 |
| 1126 return leaf; |
| 1127 }; |
| 1128 |
| 1129 /** @param {!Mocha.runners.Base} runner */ |
| 1130 MultiReporter.prototype.onRunnerStart = function onRunnerStart(runner) { |
| 1131 debug('MultiReporter#onRunnerStart:', runner.name); |
| 1132 this.total = this.total - ESTIMATED_TESTS_PER_SUITE + runner.total; |
| 1133 this.currentRunner = runner; |
| 1134 }; |
| 1135 |
| 1136 /** @param {!Mocha.runners.Base} runner */ |
| 1137 MultiReporter.prototype.onRunnerEnd = function onRunnerEnd(runner) { |
| 1138 debug('MultiReporter#onRunnerEnd:', runner.name); |
| 1139 this.currentRunner = null; |
| 1140 this.flushPendingEvents(); |
| 1141 }; |
| 1142 |
| 1143 /** |
| 1144 * Flushes any buffered events and runs them through `proxyEvent`. This will |
| 1145 * loop until all buffered runners are complete, or we have run out of buffered |
| 1146 * events. |
| 1147 */ |
| 1148 MultiReporter.prototype.flushPendingEvents = function flushPendingEvents() { |
| 1149 var events = this.pendingEvents; |
| 1150 this.pendingEvents = []; |
| 1151 events.forEach(function(eventArgs) { |
| 1152 this.proxyEvent.apply(this, eventArgs); |
| 1153 }.bind(this)); |
| 1154 }; |
| 1155 |
| 1156 var ARC_OFFSET = 0; // start at the right. |
| 1157 var ARC_WIDTH = 6; |
| 1158 |
| 1159 /** |
| 1160 * A Mocha reporter that updates the document's title and favicon with |
| 1161 * at-a-glance stats. |
| 1162 * |
| 1163 * @param {!Mocha.Runner} runner The runner that is being reported on. |
| 1164 */ |
| 1165 function Title(runner) { |
| 1166 Mocha.reporters.Base.call(this, runner); |
| 1167 |
| 1168 runner.on('test end', this.report.bind(this)); |
| 1169 } |
| 1170 |
| 1171 /** Reports current stats via the page title and favicon. */ |
| 1172 Title.prototype.report = function report() { |
| 1173 this.updateTitle(); |
| 1174 this.updateFavicon(); |
| 1175 }; |
| 1176 |
| 1177 /** Updates the document title with a summary of current stats. */ |
| 1178 Title.prototype.updateTitle = function updateTitle() { |
| 1179 if (this.stats.failures > 0) { |
| 1180 document.title = pluralizedStat(this.stats.failures, 'failing'); |
| 1181 } else { |
| 1182 document.title = pluralizedStat(this.stats.passes, 'passing'); |
| 1183 } |
| 1184 }; |
| 1185 |
| 1186 /** |
| 1187 * Draws an arc for the favicon status, relative to the total number of tests. |
| 1188 * |
| 1189 * @param {!CanvasRenderingContext2D} context |
| 1190 * @param {number} total |
| 1191 * @param {number} start |
| 1192 * @param {number} length |
| 1193 * @param {string} color |
| 1194 */ |
| 1195 function drawFaviconArc(context, total, start, length, color) { |
| 1196 var arcStart = ARC_OFFSET + Math.PI * 2 * (start / total); |
| 1197 var arcEnd = ARC_OFFSET + Math.PI * 2 * ((start + length) / total); |
| 1198 |
| 1199 context.beginPath(); |
| 1200 context.strokeStyle = color; |
| 1201 context.lineWidth = ARC_WIDTH; |
| 1202 context.arc(16, 16, 16 - ARC_WIDTH / 2, arcStart, arcEnd); |
| 1203 context.stroke(); |
| 1204 } |
| 1205 |
| 1206 /** Updates the document's favicon w/ a summary of current stats. */ |
| 1207 Title.prototype.updateFavicon = function updateFavicon() { |
| 1208 var canvas = document.createElement('canvas'); |
| 1209 canvas.height = canvas.width = 32; |
| 1210 var context = canvas.getContext('2d'); |
| 1211 |
| 1212 var passing = this.stats.passes; |
| 1213 var pending = this.stats.pending; |
| 1214 var failing = this.stats.failures; |
| 1215 var total = Math.max(this.runner.total, passing + pending + failing); |
| 1216 drawFaviconArc(context, total, 0, passing, '#0e9c57'); |
| 1217 drawFaviconArc(context, total, passing, pending, '#f3b300'); |
| 1218 drawFaviconArc(context, total, pending + passing, failing, '#ff5621'); |
| 1219 |
| 1220 this.setFavicon(canvas.toDataURL()); |
| 1221 }; |
| 1222 |
| 1223 /** Sets the current favicon by URL. */ |
| 1224 Title.prototype.setFavicon = function setFavicon(url) { |
| 1225 var current = document.head.querySelector('link[rel="icon"]'); |
| 1226 if (current) { |
| 1227 document.head.removeChild(current); |
| 1228 } |
| 1229 |
| 1230 var link = document.createElement('link'); |
| 1231 link.rel = 'icon'; |
| 1232 link.type = 'image/x-icon'; |
| 1233 link.href = url; |
| 1234 link.setAttribute('sizes', '32x32'); |
| 1235 document.head.appendChild(link); |
| 1236 }; |
| 1237 |
| 1238 /** |
| 1239 * @param {CLISocket} socket The CLI socket, if present. |
| 1240 * @param {MultiReporter} parent The parent reporter, if present. |
| 1241 * @return {!Array.<!Mocha.reporters.Base} The reporters that should be used. |
| 1242 */ |
| 1243 function determineReporters(socket, parent) { |
| 1244 // Parents are greedy. |
| 1245 if (parent) { |
| 1246 return [parent.childReporter(window.location)]; |
| 1247 } |
| 1248 |
| 1249 // Otherwise, we get to run wild without any parental supervision! |
| 1250 var reporters = [Title, Console]; |
| 1251 |
| 1252 if (socket) { |
| 1253 reporters.push(function(runner) { |
| 1254 socket.observe(runner); |
| 1255 }); |
| 1256 } |
| 1257 |
| 1258 if (htmlSuites$1.length > 0 || jsSuites$1.length > 0) { |
| 1259 reporters.push(HTML); |
| 1260 } |
| 1261 |
| 1262 return reporters; |
| 1263 } |
| 1264 |
| 1265 /** |
| 1266 * Yeah, hideous, but this allows us to be loaded before Mocha, which is handy. |
| 1267 */ |
| 1268 function injectMocha(Mocha) { |
| 1269 _injectPrototype(Console, Mocha.reporters.Base.prototype); |
| 1270 _injectPrototype(HTML, Mocha.reporters.HTML.prototype); |
| 1271 // Mocha doesn't expose its `EventEmitter` shim directly, so: |
| 1272 _injectPrototype(MultiReporter, Object.getPrototypeOf(Mocha.Runner.prototype
)); |
| 1273 } |
| 1274 |
| 1275 function _injectPrototype(klass, prototype) { |
| 1276 var newPrototype = Object.create(prototype); |
| 1277 // Only support |
| 1278 Object.keys(klass.prototype).forEach(function(key) { |
| 1279 newPrototype[key] = klass.prototype[key]; |
| 1280 }); |
| 1281 |
| 1282 klass.prototype = newPrototype; |
| 1283 } |
| 1284 |
| 1285 /** |
| 1286 * Loads all environment scripts ...synchronously ...after us. |
| 1287 */ |
| 1288 function loadSync() { |
| 1289 debug('Loading environment scripts:'); |
| 1290 var a11ySuite = 'web-component-tester/data/a11ySuite.js'; |
| 1291 var scripts = get('environmentScripts'); |
| 1292 var a11ySuiteWillBeLoaded = window.__generatedByWct || scripts.indexOf(a11ySui
te) > -1; |
| 1293 if (!a11ySuiteWillBeLoaded) { |
| 1294 // wct is running as a bower dependency, load a11ySuite from data/ |
| 1295 scripts.push(a11ySuite); |
| 1296 } |
| 1297 scripts.forEach(function(path) { |
| 1298 var url = expandUrl(path, get('root')); |
| 1299 debug('Loading environment script:', url); |
| 1300 // Synchronous load. |
| 1301 document.write('<script src="' + encodeURI(url) + '"></script>'); // jshint
ignore:line |
| 1302 }); |
| 1303 debug('Environment scripts loaded'); |
| 1304 |
| 1305 var imports = get('environmentImports'); |
| 1306 imports.forEach(function(path) { |
| 1307 var url = expandUrl(path, get('root')); |
| 1308 debug('Loading environment import:', url); |
| 1309 // Synchronous load. |
| 1310 document.write('<link rel="import" href="' + encodeURI(url) + '">'); // jshi
nt ignore:line |
| 1311 }); |
| 1312 debug('Environment imports loaded'); |
| 1313 } |
| 1314 |
| 1315 /** |
| 1316 * We have some hard dependencies on things that should be loaded via |
| 1317 * `environmentScripts`, so we assert that they're present here; and do any |
| 1318 * post-facto setup. |
| 1319 */ |
| 1320 function ensureDependenciesPresent() { |
| 1321 _ensureMocha(); |
| 1322 _checkChai(); |
| 1323 } |
| 1324 |
| 1325 function _ensureMocha() { |
| 1326 var Mocha = window.Mocha; |
| 1327 if (!Mocha) { |
| 1328 throw new Error('WCT requires Mocha. Please ensure that it is present in WCT
.environmentScripts, or that you load it before loading web-component-tester/bro
wser.js'); |
| 1329 } |
| 1330 injectMocha(Mocha); |
| 1331 // Magic loading of mocha's stylesheet |
| 1332 var mochaPrefix = scriptPrefix('mocha.js'); |
| 1333 // only load mocha stylesheet for the test runner output |
| 1334 // Not the end of the world, if it doesn't load. |
| 1335 if (mochaPrefix && window.top === window.self) { |
| 1336 loadStyle(mochaPrefix + 'mocha.css'); |
| 1337 } |
| 1338 } |
| 1339 |
| 1340 function _checkChai() { |
| 1341 if (!window.chai) { |
| 1342 debug('Chai not present; not registering shorthands'); |
| 1343 return; |
| 1344 } |
| 1345 |
| 1346 window.assert = window.chai.assert; |
| 1347 window.expect = window.chai.expect; |
| 1348 } |
| 1349 |
| 1350 // We may encounter errors during initialization (for example, syntax errors in |
| 1351 // a test file). Hang onto those (and more) until we are ready to report them. |
| 1352 var globalErrors = []; |
| 1353 |
| 1354 /** |
| 1355 * Hook the environment to pick up on global errors. |
| 1356 */ |
| 1357 function listenForErrors() { |
| 1358 window.addEventListener('error', function(event) { |
| 1359 globalErrors.push(event.error); |
| 1360 }); |
| 1361 |
| 1362 // Also, we treat `console.error` as a test failure. Unless you prefer not. |
| 1363 var origConsole = console; |
| 1364 var origError = console.error; |
| 1365 console.error = function wctShimmedError() { |
| 1366 origError.apply(origConsole, arguments); |
| 1367 if (get('trackConsoleError')) { |
| 1368 throw 'console.error: ' + Array.prototype.join.call(arguments, ' '); |
| 1369 } |
| 1370 }; |
| 1371 } |
| 1372 |
| 1373 var interfaceExtensions = []; |
| 1374 |
| 1375 /** |
| 1376 * Registers an extension that extends the global `Mocha` implementation |
| 1377 * with new helper methods. These helper methods will be added to the `window` |
| 1378 * when tests run for both BDD and TDD interfaces. |
| 1379 */ |
| 1380 function extendInterfaces(helperName, helperFactory) { |
| 1381 interfaceExtensions.push(function() { |
| 1382 var Mocha = window.Mocha; |
| 1383 // For all Mocha interfaces (probably just TDD and BDD): |
| 1384 Object.keys(Mocha.interfaces).forEach(function(interfaceName) { |
| 1385 // This is the original callback that defines the interface (TDD or BDD): |
| 1386 var originalInterface = Mocha.interfaces[interfaceName]; |
| 1387 // This is the name of the "teardown" or "afterEach" property for the |
| 1388 // current interface: |
| 1389 var teardownProperty = interfaceName === 'tdd' ? 'teardown' : 'afterEach'; |
| 1390 // The original callback is monkey patched with a new one that appends to |
| 1391 // the global context however we want it to: |
| 1392 Mocha.interfaces[interfaceName] = function(suite) { |
| 1393 // Call back to the original callback so that we get the base interface: |
| 1394 originalInterface.apply(this, arguments); |
| 1395 // Register a listener so that we can further extend the base interface: |
| 1396 suite.on('pre-require', function(context, file, mocha) { |
| 1397 // Capture a bound reference to the teardown function as a convenience
: |
| 1398 var teardown = context[teardownProperty].bind(context); |
| 1399 // Add our new helper to the testing context. The helper is generated |
| 1400 // by a factory method that receives the context, the teardown functio
n |
| 1401 // and the interface name and returns the new method to be added to |
| 1402 // that context: |
| 1403 context[helperName] = helperFactory(context, teardown, interfaceName); |
| 1404 }); |
| 1405 }; |
| 1406 }); |
| 1407 }); |
| 1408 } |
| 1409 |
| 1410 /** |
| 1411 * Applies any registered interface extensions. The extensions will be applied |
| 1412 * as many times as this function is called, so don't call it more than once. |
| 1413 */ |
| 1414 function applyExtensions() { |
| 1415 interfaceExtensions.forEach(function(applyExtension) { |
| 1416 applyExtension(); |
| 1417 }); |
| 1418 } |
| 1419 |
| 1420 extendInterfaces('fixture', function (context, teardown) { |
| 1421 |
| 1422 // Return context.fixture if it is already a thing, for backwards |
| 1423 // compatibility with `test-fixture-mocha.js`: |
| 1424 return context.fixture || function fixture(fixtureId, model) { |
| 1425 |
| 1426 // Automatically register a teardown callback that will restore the |
| 1427 // test-fixture: |
| 1428 teardown(function () { |
| 1429 document.getElementById(fixtureId).restore(); |
| 1430 }); |
| 1431 |
| 1432 // Find the test-fixture with the provided ID and create it, returning |
| 1433 // the results: |
| 1434 return document.getElementById(fixtureId).create(model); |
| 1435 }; |
| 1436 }); |
| 1437 |
| 1438 /** |
| 1439 * stub |
| 1440 * |
| 1441 * The stub addon allows the tester to partially replace the implementation of |
| 1442 * an element with some custom implementation. Usage example: |
| 1443 * |
| 1444 * beforeEach(function() { |
| 1445 * stub('x-foo', { |
| 1446 * attached: function() { |
| 1447 * // Custom implementation of the `attached` method of element `x-foo`.. |
| 1448 * }, |
| 1449 * otherMethod: function() { |
| 1450 * // More custom implementation.. |
| 1451 * }, |
| 1452 * getterSetterProperty: { |
| 1453 * get: function() { |
| 1454 * // Custom getter implementation.. |
| 1455 * }, |
| 1456 * set: function() { |
| 1457 * // Custom setter implementation.. |
| 1458 * } |
| 1459 * }, |
| 1460 * // etc.. |
| 1461 * }); |
| 1462 * }); |
| 1463 */ |
| 1464 extendInterfaces('stub', function(context, teardown) { |
| 1465 |
| 1466 return function stub(tagName, implementation) { |
| 1467 // Find the prototype of the element being stubbed: |
| 1468 var proto = document.createElement(tagName).constructor.prototype; |
| 1469 |
| 1470 // For all keys in the implementation to stub with.. |
| 1471 var stubs = Object.keys(implementation).map(function(key) { |
| 1472 // Stub the method on the element prototype with Sinon: |
| 1473 return sinon.stub(proto, key, implementation[key]); |
| 1474 }); |
| 1475 |
| 1476 // After all tests.. |
| 1477 teardown(function() { |
| 1478 stubs.forEach(function(stub) { |
| 1479 stub.restore(); |
| 1480 }); |
| 1481 }); |
| 1482 }; |
| 1483 }); |
| 1484 |
| 1485 // replacement map stores what should be |
| 1486 var replacements = {}; |
| 1487 var replaceTeardownAttached = false; |
| 1488 |
| 1489 /** |
| 1490 * replace |
| 1491 * |
| 1492 * The replace addon allows the tester to replace all usages of one element with |
| 1493 * another element within all Polymer elements created within the time span of |
| 1494 * the test. Usage example: |
| 1495 * |
| 1496 * beforeEach(function() { |
| 1497 * replace('x-foo').with('x-fake-foo'); |
| 1498 * }); |
| 1499 * |
| 1500 * All annotations and attributes will be set on the placement element the way |
| 1501 * they were set for the original element. |
| 1502 */ |
| 1503 extendInterfaces('replace', function (context, teardown) { |
| 1504 return function replace(oldTagName) { |
| 1505 return { |
| 1506 with: function (tagName) { |
| 1507 // Standardizes our replacements map |
| 1508 oldTagName = oldTagName.toLowerCase(); |
| 1509 tagName = tagName.toLowerCase(); |
| 1510 |
| 1511 replacements[oldTagName] = tagName; |
| 1512 |
| 1513 // If the function is already a stub, restore it to original |
| 1514 if (document.importNode.isSinonProxy) { |
| 1515 return; |
| 1516 } |
| 1517 |
| 1518 if (!Polymer.Element) { |
| 1519 Polymer.Element = function () { }; |
| 1520 Polymer.Element.prototype._stampTemplate = function () { }; |
| 1521 } |
| 1522 |
| 1523 // Keep a reference to the original `document.importNode` |
| 1524 // implementation for later: |
| 1525 var originalImportNode = document.importNode; |
| 1526 |
| 1527 // Use Sinon to stub `document.ImportNode`: |
| 1528 sinon.stub(document, 'importNode', function (origContent, deep) { |
| 1529 var templateClone = document.createElement('template'); |
| 1530 var content = templateClone.content; |
| 1531 var inertDoc = content.ownerDocument; |
| 1532 |
| 1533 // imports node from inertDoc which holds inert nodes. |
| 1534 templateClone.content.appendChild(inertDoc.importNode(origContent, tru
e)); |
| 1535 |
| 1536 // optional arguments are not optional on IE. |
| 1537 var nodeIterator = document.createNodeIterator(content, |
| 1538 NodeFilter.SHOW_ELEMENT, null, true); |
| 1539 var node; |
| 1540 |
| 1541 // Traverses the tree. A recently-replaced node will be put next, so |
| 1542 // if a node is replaced, it will be checked if it needs to be |
| 1543 // replaced again. |
| 1544 while (node = nodeIterator.nextNode()) { |
| 1545 var currentTagName = node.tagName.toLowerCase(); |
| 1546 |
| 1547 if (replacements.hasOwnProperty(currentTagName)) { |
| 1548 currentTagName = replacements[currentTagName]; |
| 1549 |
| 1550 // find the final tag name. |
| 1551 while (replacements[currentTagName]) { |
| 1552 currentTagName = replacements[currentTagName]; |
| 1553 } |
| 1554 |
| 1555 // Create a replacement: |
| 1556 var replacement = document.createElement(currentTagName); |
| 1557 |
| 1558 // For all attributes in the original node.. |
| 1559 for (var index = 0; index < node.attributes.length; ++index) { |
| 1560 // Set that attribute on the replacement: |
| 1561 replacement.setAttribute( |
| 1562 node.attributes[index].name, node.attributes[index].value); |
| 1563 } |
| 1564 |
| 1565 // Replace the original node with the replacement node: |
| 1566 node.parentNode.replaceChild(replacement, node); |
| 1567 } |
| 1568 } |
| 1569 |
| 1570 return originalImportNode.call(this, content, deep); |
| 1571 }); |
| 1572 |
| 1573 if (!replaceTeardownAttached) { |
| 1574 // After each test... |
| 1575 teardown(function () { |
| 1576 replaceTeardownAttached = true; |
| 1577 // Restore the stubbed version of `document.importNode`: |
| 1578 if (document.importNode.isSinonProxy) { |
| 1579 document.importNode.restore(); |
| 1580 } |
| 1581 |
| 1582 // Empty the replacement map |
| 1583 replacements = {}; |
| 1584 }); |
| 1585 } |
| 1586 } |
| 1587 }; |
| 1588 }; |
| 1589 }); |
| 1590 |
| 1591 // Mocha global helpers, broken out by testing method. |
| 1592 // |
| 1593 // Keys are the method for a particular interface; values are their analog in |
| 1594 // the opposite interface. |
| 1595 var MOCHA_EXPORTS = { |
| 1596 // https://github.com/visionmedia/mocha/blob/master/lib/interfaces/tdd.js |
| 1597 tdd: { |
| 1598 'setup': '"before"', |
| 1599 'teardown': '"after"', |
| 1600 'suiteSetup': '"beforeEach"', |
| 1601 'suiteTeardown': '"afterEach"', |
| 1602 'suite': '"describe" or "context"', |
| 1603 'test': '"it" or "specify"', |
| 1604 }, |
| 1605 // https://github.com/visionmedia/mocha/blob/master/lib/interfaces/bdd.js |
| 1606 bdd: { |
| 1607 'before': '"setup"', |
| 1608 'after': '"teardown"', |
| 1609 'beforeEach': '"suiteSetup"', |
| 1610 'afterEach': '"suiteTeardown"', |
| 1611 'describe': '"suite"', |
| 1612 'context': '"suite"', |
| 1613 'xdescribe': '"suite.skip"', |
| 1614 'xcontext': '"suite.skip"', |
| 1615 'it': '"test"', |
| 1616 'xit': '"test.skip"', |
| 1617 'specify': '"test"', |
| 1618 'xspecify': '"test.skip"', |
| 1619 }, |
| 1620 }; |
| 1621 |
| 1622 /** |
| 1623 * Exposes all Mocha methods up front, configuring and running mocha |
| 1624 * automatically when you call them. |
| 1625 * |
| 1626 * The assumption is that it is a one-off (sub-)suite of tests being run. |
| 1627 */ |
| 1628 function stubInterfaces() { |
| 1629 Object.keys(MOCHA_EXPORTS).forEach(function (ui) { |
| 1630 Object.keys(MOCHA_EXPORTS[ui]).forEach(function (key) { |
| 1631 window[key] = function wrappedMochaFunction() { |
| 1632 _setupMocha(ui, key, MOCHA_EXPORTS[ui][key]); |
| 1633 if (!window[key] || window[key] === wrappedMochaFunction) { |
| 1634 throw new Error('Expected mocha.setup to define ' + key); |
| 1635 } |
| 1636 window[key].apply(window, arguments); |
| 1637 }; |
| 1638 }); |
| 1639 }); |
| 1640 } |
| 1641 |
| 1642 // Whether we've called `mocha.setup` |
| 1643 var _mochaIsSetup = false; |
| 1644 |
| 1645 /** |
| 1646 * @param {string} ui Sets up mocha to run `ui`-style tests. |
| 1647 * @param {string} key The method called that triggered this. |
| 1648 * @param {string} alternate The matching method in the opposite interface. |
| 1649 */ |
| 1650 function _setupMocha(ui, key, alternate) { |
| 1651 var mochaOptions = get('mochaOptions'); |
| 1652 if (mochaOptions.ui && mochaOptions.ui !== ui) { |
| 1653 var message = 'Mixing ' + mochaOptions.ui + ' and ' + ui + ' Mocha styles is
not supported. ' + |
| 1654 'You called "' + key + '". Did you mean ' + alternate + '?'; |
| 1655 throw new Error(message); |
| 1656 } |
| 1657 if (_mochaIsSetup) return; |
| 1658 |
| 1659 applyExtensions(); |
| 1660 mochaOptions.ui = ui; |
| 1661 mocha.setup(mochaOptions); // Note that the reporter is configured in run.js. |
| 1662 } |
| 1663 |
| 1664 var SOCKETIO_ENDPOINT = window.location.protocol + '//' + window.location.host; |
| 1665 var SOCKETIO_LIBRARY = SOCKETIO_ENDPOINT + '/socket.io/socket.io.js'; |
| 1666 |
| 1667 /** |
| 1668 * A socket for communication between the CLI and browser runners. |
| 1669 * |
| 1670 * @param {string} browserId An ID generated by the CLI runner. |
| 1671 * @param {!io.Socket} socket The socket.io `Socket` to communicate over. |
| 1672 */ |
| 1673 function CLISocket(browserId, socket) { |
| 1674 this.browserId = browserId; |
| 1675 this.socket = socket; |
| 1676 } |
| 1677 |
| 1678 /** |
| 1679 * @param {!Mocha.Runner} runner The Mocha `Runner` to observe, reporting |
| 1680 * interesting events back to the CLI runner. |
| 1681 */ |
| 1682 CLISocket.prototype.observe = function observe(runner) { |
| 1683 this.emitEvent('browser-start', { |
| 1684 url: window.location.toString(), |
| 1685 }); |
| 1686 |
| 1687 // We only emit a subset of events that we care about, and follow a more |
| 1688 // general event format that is hopefully applicable to test runners beyond |
| 1689 // mocha. |
| 1690 // |
| 1691 // For all possible mocha events, see: |
| 1692 // https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36 |
| 1693 runner.on('test', function(test) { |
| 1694 this.emitEvent('test-start', {test: getTitles(test)}); |
| 1695 }.bind(this)); |
| 1696 |
| 1697 runner.on('test end', function(test) { |
| 1698 this.emitEvent('test-end', { |
| 1699 state: getState(test), |
| 1700 test: getTitles(test), |
| 1701 duration: test.duration, |
| 1702 error: test.err, |
| 1703 }); |
| 1704 }.bind(this)); |
| 1705 |
| 1706 runner.on('fail', function(test, err) { |
| 1707 // fail the test run if we catch errors outside of a test function |
| 1708 if (test.type !== 'test') { |
| 1709 this.emitEvent('browser-fail', 'Error thrown outside of test function: ' +
err.stack); |
| 1710 } |
| 1711 }.bind(this)); |
| 1712 |
| 1713 runner.on('childRunner start', function(childRunner) { |
| 1714 this.emitEvent('sub-suite-start', childRunner.share); |
| 1715 }.bind(this)); |
| 1716 |
| 1717 runner.on('childRunner end', function(childRunner) { |
| 1718 this.emitEvent('sub-suite-end', childRunner.share); |
| 1719 }.bind(this)); |
| 1720 |
| 1721 runner.on('end', function() { |
| 1722 this.emitEvent('browser-end'); |
| 1723 }.bind(this)); |
| 1724 }; |
| 1725 |
| 1726 /** |
| 1727 * @param {string} event The name of the event to fire. |
| 1728 * @param {*} data Additional data to pass with the event. |
| 1729 */ |
| 1730 CLISocket.prototype.emitEvent = function emitEvent(event, data) { |
| 1731 this.socket.emit('client-event', { |
| 1732 browserId: this.browserId, |
| 1733 event: event, |
| 1734 data: data, |
| 1735 }); |
| 1736 }; |
| 1737 |
| 1738 /** |
| 1739 * Builds a `CLISocket` if we are within a CLI-run environment; short-circuits |
| 1740 * otherwise. |
| 1741 * |
| 1742 * @param {function(*, CLISocket)} done Node-style callback. |
| 1743 */ |
| 1744 CLISocket.init = function init(done) { |
| 1745 var browserId = getParam('cli_browser_id'); |
| 1746 if (!browserId) return done(); |
| 1747 // Only fire up the socket for root runners. |
| 1748 if (ChildRunner.current()) return done(); |
| 1749 |
| 1750 loadScript(SOCKETIO_LIBRARY, function(error) { |
| 1751 if (error) return done(error); |
| 1752 |
| 1753 var socket = io(SOCKETIO_ENDPOINT); |
| 1754 socket.on('error', function(error) { |
| 1755 socket.off(); |
| 1756 done(error); |
| 1757 }); |
| 1758 |
| 1759 socket.on('connect', function() { |
| 1760 socket.off(); |
| 1761 done(null, new CLISocket(browserId, socket)); |
| 1762 }); |
| 1763 }); |
| 1764 }; |
| 1765 |
| 1766 // Misc Utility |
| 1767 |
| 1768 /** |
| 1769 * @param {!Mocha.Runnable} runnable The test or suite to extract titles from. |
| 1770 * @return {!Array.<string>} The titles of the runnable and its parents. |
| 1771 */ |
| 1772 function getTitles(runnable) { |
| 1773 var titles = []; |
| 1774 while (runnable && !runnable.root && runnable.title) { |
| 1775 titles.unshift(runnable.title); |
| 1776 runnable = runnable.parent; |
| 1777 } |
| 1778 return titles; |
| 1779 } |
| 1780 |
| 1781 /** |
| 1782 * @param {!Mocha.Runnable} runnable |
| 1783 * @return {string} |
| 1784 */ |
| 1785 function getState(runnable) { |
| 1786 if (runnable.state === 'passed') { |
| 1787 return 'passing'; |
| 1788 } else if (runnable.state == 'failed') { |
| 1789 return 'failing'; |
| 1790 } else if (runnable.pending) { |
| 1791 return 'pending'; |
| 1792 } else { |
| 1793 return 'unknown'; |
| 1794 } |
| 1795 } |
| 1796 |
| 1797 /** |
| 1798 * @license |
| 1799 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| 1800 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt |
| 1801 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt |
| 1802 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt |
| 1803 * Code distributed by Google as part of the polymer project is also |
| 1804 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt |
| 1805 */ |
| 1806 |
| 1807 // Make sure that we use native timers, in case they're being stubbed out. |
| 1808 var setInterval = window.setInterval; // jshint ignore:line |
| 1809 var setTimeout$1 = window.setTimeout; // jshint ignore:lin
e |
| 1810 var requestAnimationFrame = window.requestAnimationFrame; // jshint ignore:line |
| 1811 |
| 1812 /** |
| 1813 * Runs `stepFn`, catching any error and passing it to `callback` (Node-style). |
| 1814 * Otherwise, calls `callback` with no arguments on success. |
| 1815 * |
| 1816 * @param {function()} callback |
| 1817 * @param {function()} stepFn |
| 1818 */ |
| 1819 window.safeStep = function safeStep(callback, stepFn) { |
| 1820 var err; |
| 1821 try { |
| 1822 stepFn(); |
| 1823 } catch (error) { |
| 1824 err = error; |
| 1825 } |
| 1826 callback(err); |
| 1827 }; |
| 1828 |
| 1829 /** |
| 1830 * Runs your test at declaration time (before Mocha has begun tests). Handy for |
| 1831 * when you need to test document initialization. |
| 1832 * |
| 1833 * Be aware that any errors thrown asynchronously cannot be tied to your test. |
| 1834 * You may want to catch them and pass them to the done event, instead. See |
| 1835 * `safeStep`. |
| 1836 * |
| 1837 * @param {string} name The name of the test. |
| 1838 * @param {function(?function())} testFn The test function. If an argument is |
| 1839 * accepted, the test will be treated as async, just like Mocha tests. |
| 1840 */ |
| 1841 window.testImmediate = function testImmediate(name, testFn) { |
| 1842 if (testFn.length > 0) { |
| 1843 return testImmediateAsync(name, testFn); |
| 1844 } |
| 1845 |
| 1846 var err; |
| 1847 try { |
| 1848 testFn(); |
| 1849 } catch (error) { |
| 1850 err = error; |
| 1851 } |
| 1852 |
| 1853 test(name, function(done) { |
| 1854 done(err); |
| 1855 }); |
| 1856 }; |
| 1857 |
| 1858 /** |
| 1859 * An async-only variant of `testImmediate`. |
| 1860 * |
| 1861 * @param {string} name |
| 1862 * @param {function(?function())} testFn |
| 1863 */ |
| 1864 window.testImmediateAsync = function testImmediateAsync(name, testFn) { |
| 1865 var testComplete = false; |
| 1866 var err; |
| 1867 |
| 1868 test(name, function(done) { |
| 1869 var intervalId = setInterval(function() { |
| 1870 if (!testComplete) return; |
| 1871 clearInterval(intervalId); |
| 1872 done(err); |
| 1873 }, 10); |
| 1874 }); |
| 1875 |
| 1876 try { |
| 1877 testFn(function(error) { |
| 1878 if (error) err = error; |
| 1879 testComplete = true; |
| 1880 }); |
| 1881 } catch (error) { |
| 1882 err = error; |
| 1883 testComplete = true; |
| 1884 } |
| 1885 }; |
| 1886 |
| 1887 /** |
| 1888 * Triggers a flush of any pending events, observations, etc and calls you back |
| 1889 * after they have been processed. |
| 1890 * |
| 1891 * @param {function()} callback |
| 1892 */ |
| 1893 window.flush = function flush(callback) { |
| 1894 // Ideally, this function would be a call to Polymer.dom.flush, but that doesn
't |
| 1895 // support a callback yet (https://github.com/Polymer/polymer-dev/issues/851), |
| 1896 // ...and there's cross-browser flakiness to deal with. |
| 1897 |
| 1898 // Make sure that we're invoking the callback with no arguments so that the |
| 1899 // caller can pass Mocha callbacks, etc. |
| 1900 var done = function done() { callback(); }; |
| 1901 |
| 1902 // Because endOfMicrotask is flaky for IE, we perform microtask checkpoints |
| 1903 // ourselves (https://github.com/Polymer/polymer-dev/issues/114): |
| 1904 var isIE = navigator.appName == 'Microsoft Internet Explorer'; |
| 1905 if (isIE && window.Platform && window.Platform.performMicrotaskCheckpoint) { |
| 1906 var reallyDone = done; |
| 1907 done = function doneIE() { |
| 1908 Platform.performMicrotaskCheckpoint(); |
| 1909 setTimeout$1(reallyDone, 0); |
| 1910 }; |
| 1911 } |
| 1912 |
| 1913 // Everyone else gets a regular flush. |
| 1914 var scope; |
| 1915 if (window.Polymer && window.Polymer.dom && window.Polymer.dom.flush) { |
| 1916 scope = window.Polymer.dom; |
| 1917 } else if (window.Polymer && window.Polymer.flush) { |
| 1918 scope = window.Polymer; |
| 1919 } else if (window.WebComponents && window.WebComponents.flush) { |
| 1920 scope = window.WebComponents; |
| 1921 } |
| 1922 if (scope) { |
| 1923 scope.flush(); |
| 1924 } |
| 1925 |
| 1926 // Ensure that we are creating a new _task_ to allow all active microtasks to |
| 1927 // finish (the code you're testing may be using endOfMicrotask, too). |
| 1928 setTimeout$1(done, 0); |
| 1929 }; |
| 1930 |
| 1931 /** |
| 1932 * Advances a single animation frame. |
| 1933 * |
| 1934 * Calls `flush`, `requestAnimationFrame`, `flush`, and `callback` sequentially |
| 1935 * @param {function()} callback |
| 1936 */ |
| 1937 window.animationFrameFlush = function animationFrameFlush(callback) { |
| 1938 flush(function() { |
| 1939 requestAnimationFrame(function() { |
| 1940 flush(callback); |
| 1941 }); |
| 1942 }); |
| 1943 }; |
| 1944 |
| 1945 /** |
| 1946 * DEPRECATED: Use `flush`. |
| 1947 * @param {function} callback |
| 1948 */ |
| 1949 window.asyncPlatformFlush = function asyncPlatformFlush(callback) { |
| 1950 console.warn('asyncPlatformFlush is deprecated in favor of the more terse flus
h()'); |
| 1951 return window.flush(callback); |
| 1952 }; |
| 1953 |
| 1954 /** |
| 1955 * |
| 1956 */ |
| 1957 window.waitFor = function waitFor(fn, next, intervalOrMutationEl, timeout, timeo
utTime) { |
| 1958 timeoutTime = timeoutTime || Date.now() + (timeout || 1000); |
| 1959 intervalOrMutationEl = intervalOrMutationEl || 32; |
| 1960 try { |
| 1961 fn(); |
| 1962 } catch (e) { |
| 1963 if (Date.now() > timeoutTime) { |
| 1964 throw e; |
| 1965 } else { |
| 1966 if (isNaN(intervalOrMutationEl)) { |
| 1967 intervalOrMutationEl.onMutation(intervalOrMutationEl, function() { |
| 1968 waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime); |
| 1969 }); |
| 1970 } else { |
| 1971 setTimeout$1(function() { |
| 1972 waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime); |
| 1973 }, intervalOrMutationEl); |
| 1974 } |
| 1975 return; |
| 1976 } |
| 1977 } |
| 1978 next(); |
| 1979 }; |
| 1980 |
| 1981 // You can configure WCT before it has loaded by assigning your custom |
| 1982 // configuration to the global `WCT`. |
| 1983 setup(window.WCT); |
| 1984 |
| 1985 // Maybe some day we'll expose WCT as a module to whatever module registry you |
| 1986 // are using (aka the UMD approach), or as an es6 module. |
| 1987 var WCT = window.WCT = {}; |
| 1988 // A generic place to hang data about the current suite. This object is reported |
| 1989 // back via the `sub-suite-start` and `sub-suite-end` events. |
| 1990 WCT.share = {}; |
| 1991 // Until then, we get to rely on it to expose parent runners to their children. |
| 1992 WCT._ChildRunner = ChildRunner; |
| 1993 WCT._config = _config; |
| 1994 |
| 1995 |
| 1996 // Public Interface |
| 1997 |
| 1998 /** |
| 1999 * Loads suites of tests, supporting both `.js` and `.html` files. |
| 2000 * |
| 2001 * @param {!Array.<string>} files The files to load. |
| 2002 */ |
| 2003 WCT.loadSuites = loadSuites; |
| 2004 |
| 2005 |
| 2006 // Load Process |
| 2007 |
| 2008 listenForErrors(); |
| 2009 stubInterfaces(); |
| 2010 loadSync(); |
| 2011 |
| 2012 // Give any scripts on the page a chance to declare tests and muck with things. |
| 2013 document.addEventListener('DOMContentLoaded', function() { |
| 2014 debug('DOMContentLoaded'); |
| 2015 |
| 2016 ensureDependenciesPresent(); |
| 2017 |
| 2018 // We need the socket built prior to building its reporter. |
| 2019 CLISocket.init(function(error, socket) { |
| 2020 if (error) throw error; |
| 2021 |
| 2022 // Are we a child of another run? |
| 2023 var current = ChildRunner.current(); |
| 2024 var parent = current && current.parentScope.WCT._reporter; |
| 2025 debug('parentReporter:', parent); |
| 2026 |
| 2027 var childSuites = activeChildSuites(); |
| 2028 var reportersToUse = determineReporters(socket, parent); |
| 2029 // +1 for any local tests. |
| 2030 var reporter = new MultiReporter(childSuites.length + 1, reportersToUse, par
ent); |
| 2031 WCT._reporter = reporter; // For environment/compatibility.js |
| 2032 |
| 2033 // We need the reporter so that we can report errors during load. |
| 2034 loadJsSuites(reporter, function(error) { |
| 2035 // Let our parent know that we're about to start the tests. |
| 2036 if (current) current.ready(error); |
| 2037 if (error) throw error; |
| 2038 |
| 2039 // Emit any errors we've encountered up til now |
| 2040 globalErrors.forEach(function onError(error) { |
| 2041 reporter.emitOutOfBandTest('Test Suite Initialization', error); |
| 2042 }); |
| 2043 |
| 2044 runSuites(reporter, childSuites, function(error) { |
| 2045 // Make sure to let our parent know that we're done. |
| 2046 if (current) current.done(); |
| 2047 if (error) throw error; |
| 2048 }); |
| 2049 }); |
| 2050 }); |
| 2051 }); |
| 2052 |
| 2053 }()); |
| 2054 //# sourceMappingURL=browser.js.map |
| OLD | NEW |