Repository: Polymer/web-component-tester
Branch: master
Commit: e68b2cb5842f
Files: 167
Total size: 409.9 KB
Directory structure:
gitextract_nxebno92/
├── .clang-format
├── .github/
│ └── PULL_REQUEST_TEMPLATE
├── .gitignore
├── .npmignore
├── .travis.yml
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│ ├── wct
│ └── wct-st
├── bower.json
├── browser/
│ ├── childrunner.ts
│ ├── clisocket.ts
│ ├── config.ts
│ ├── declarations.ts
│ ├── environment/
│ │ ├── compatability.ts
│ │ ├── errors.ts
│ │ └── helpers.ts
│ ├── environment.ts
│ ├── index.ts
│ ├── mocha/
│ │ ├── extend.ts
│ │ ├── fixture.ts
│ │ ├── replace.ts
│ │ └── stub.ts
│ ├── mocha.ts
│ ├── more-declarations.ts
│ ├── reporters/
│ │ ├── console.ts
│ │ ├── html.ts
│ │ ├── multi.ts
│ │ └── title.ts
│ ├── reporters.ts
│ ├── suites.ts
│ ├── tsconfig.json
│ └── util.ts
├── browser-js-header.txt
├── browser.js
├── custom_typings/
│ ├── bower-config.d.ts
│ ├── findup-sync.d.ts
│ ├── promisify-node.d.ts
│ ├── send.d.ts
│ ├── server-destroy.d.ts
│ ├── stacky.d.ts
│ └── wd.d.ts
├── data/
│ ├── a11ySuite-npm-header.txt
│ ├── a11ySuite.js
│ └── index.html
├── gulpfile.js
├── package.json
├── runner/
│ ├── browserrunner.ts
│ ├── cli.ts
│ ├── clireporter.ts
│ ├── config.ts
│ ├── context.ts
│ ├── gulp.ts
│ ├── httpbin.ts
│ ├── paths.ts
│ ├── plugin.ts
│ ├── port-scanner.ts
│ ├── steps.ts
│ ├── test.ts
│ └── webserver.ts
├── runner.js
├── tasks/
│ └── test.js
├── test/
│ ├── fixtures/
│ │ ├── cli/
│ │ │ ├── conf/
│ │ │ │ ├── branch/
│ │ │ │ │ └── leaf/
│ │ │ │ │ └── thing.js
│ │ │ │ ├── json/
│ │ │ │ │ ├── wct.conf.js
│ │ │ │ │ └── wct.conf.json
│ │ │ │ ├── rooted/
│ │ │ │ │ └── wct.conf.js
│ │ │ │ ├── test/
│ │ │ │ │ └── foo.js
│ │ │ │ └── wct.conf.js
│ │ │ └── standard/
│ │ │ ├── test/
│ │ │ │ ├── a.html
│ │ │ │ └── b.js
│ │ │ └── x-foo.html
│ │ ├── early-failure/
│ │ │ ├── bower_components/
│ │ │ │ └── web-component-tester/
│ │ │ │ └── package.json
│ │ │ └── test/
│ │ │ └── index.html
│ │ ├── fake-packages/
│ │ │ ├── duplicated-dep/
│ │ │ │ └── package.json
│ │ │ └── singleton-dep/
│ │ │ └── package.json
│ │ ├── integration/
│ │ │ ├── compilation/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ └── index.html
│ │ │ ├── components_dir/
│ │ │ │ ├── bower_components/
│ │ │ │ │ └── foo-element/
│ │ │ │ │ └── foo-element.js
│ │ │ │ ├── golden.json
│ │ │ │ ├── test/
│ │ │ │ │ └── index.html
│ │ │ │ └── wct.conf.json
│ │ │ ├── custom-components_dir/
│ │ │ │ ├── .bowerrc
│ │ │ │ ├── golden.json
│ │ │ │ ├── my_components/
│ │ │ │ │ └── bar-element/
│ │ │ │ │ └── bar-element.js
│ │ │ │ └── test/
│ │ │ │ └── index.html
│ │ │ ├── custom-multiple-component_dirs/
│ │ │ │ ├── .bowerrc
│ │ │ │ ├── golden.json
│ │ │ │ ├── my_components/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ ├── my_components-bar/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ ├── my_components-foo/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ └── test/
│ │ │ │ └── index.html
│ │ │ ├── define-webserver-hook/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ ├── index.html
│ │ │ │ └── tests.html
│ │ │ ├── failing/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ ├── index.html
│ │ │ │ ├── tests.html
│ │ │ │ └── tests.js
│ │ │ ├── missing/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ └── missing.html
│ │ │ ├── mixed-suites/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ ├── index.html
│ │ │ │ ├── one.html
│ │ │ │ ├── one.js
│ │ │ │ ├── two.html
│ │ │ │ └── two.js
│ │ │ ├── multiple-component_dirs/
│ │ │ │ ├── bower_components/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ ├── bower_components-bar/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ ├── bower_components-foo/
│ │ │ │ │ └── package/
│ │ │ │ │ └── index.js
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ └── index.html
│ │ │ ├── multiple-replace/
│ │ │ │ ├── dom-if-element.html
│ │ │ │ ├── dom-repeat-fixture.html
│ │ │ │ ├── exception-element.html
│ │ │ │ ├── exception-fixture.html
│ │ │ │ ├── golden.json
│ │ │ │ ├── normal-element.html
│ │ │ │ ├── projection-element-2.html
│ │ │ │ ├── projection-element-3.html
│ │ │ │ ├── projection-element-4.html
│ │ │ │ ├── projection-element.html
│ │ │ │ └── test/
│ │ │ │ ├── index.html
│ │ │ │ └── tests.html
│ │ │ ├── nested/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ ├── index.html
│ │ │ │ ├── leaf.html
│ │ │ │ ├── leaf.js
│ │ │ │ ├── one/
│ │ │ │ │ ├── index.html
│ │ │ │ │ └── tests.html
│ │ │ │ └── two/
│ │ │ │ └── index.html
│ │ │ ├── no-tests/
│ │ │ │ ├── golden.json
│ │ │ │ └── test/
│ │ │ │ └── index.html
│ │ │ └── query-string/
│ │ │ ├── golden.json
│ │ │ └── test/
│ │ │ ├── index.html
│ │ │ ├── tests.html
│ │ │ └── tests.js
│ │ └── paths/
│ │ ├── bar/
│ │ │ ├── a.js
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ ├── baz/
│ │ │ ├── a/
│ │ │ │ └── fizz.html
│ │ │ ├── a.html
│ │ │ ├── b/
│ │ │ │ ├── deep/
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── stuff.html
│ │ │ │ │ └── stuff.js
│ │ │ │ ├── index.html
│ │ │ │ └── one.js
│ │ │ └── b.js
│ │ ├── foo/
│ │ │ ├── one.js
│ │ │ ├── three.css
│ │ │ └── two.html
│ │ ├── foo.html
│ │ └── foo.js
│ ├── integration/
│ │ ├── browser.ts
│ │ └── setup_test_dir.ts
│ └── unit/
│ ├── cli.ts
│ ├── config.ts
│ ├── context.ts
│ ├── grunt.ts
│ ├── gulp.ts
│ └── paths.ts
├── tsconfig.json
├── tslint.json
└── wct-browser-legacy/
├── a11ySuite.js
├── browser.js
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
BasedOnStyle: Google
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE
================================================
- [ ] CHANGELOG.md has been updated
================================================
FILE: .gitignore
================================================
# Update .npmignore whenever you update this file!
.todo
/bower_components
/node_modules
package-lock.json
npm-debug.log
typings/
runner/*.js
runner/*.d.ts
runner/*.js.map
test/unit/*.js
test/unit/*.d.ts
test/unit/*.js.map
test/integration/*.js
test/integration/*.d.ts
test/integration/*.js.map
test/fixtures/integration/temp
browser/*.js
browser/*.js.map
browser/environment/*.js*
browser/mocha/*.js*
browser/reporters/*.js*
================================================
FILE: .npmignore
================================================
# Update .gitignore whenever you update this file!
.todo
bower_components
node_modules
npm-debug.log
typings/
# Don't ignore runner/*.js
================================================
FILE: .travis.yml
================================================
sudo: required
dist: trusty
addons:
firefox: latest
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
sauce_connect: true
language: node_js
node_js:
- '6'
- '8'
script:
- xvfb-run npm test
env:
global:
- MOCHA_TIMEOUT=300000
- secure: WqREPJCj0auiij7P86MfTpD5+buf7vlAuRiUILmcq0+rhVFvsgqG82NlKIjcQuCzM9WFMqBHxDyV0/iyfE5qiQsEXYd7g3SwMVFIAS3R8sKnMz/EMA0kCqFCGn14iwbakNduSuyjQhxHje72iczm/3S4nKFceOzDGFlnvGLxpTA=
- secure: D5P88V2PQF6mq/bPHlpzW1DgPSz4rKaoXJ1SNIsdeqUnd8xN+tmVUBO0/skunwIPIdws0y7cLfZOThEJuRMoDMUal0Fh81UcWna+yfM5N5NYziyCsOcf60UCZLpWdvS8QNEL0zGff6apO/TEaUkgYx7IMdzi0Bu+56v/iybwM3Q=
================================================
FILE: .vscode/settings.json
================================================
// Place your settings in this file to overwrite default and user settings.
{
"clang-format.style": "file",
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.exclude": {
"runner/*.js": true,
"runner/*.d.ts": true,
"runner/*.js.map": true,
"test/unit/*.js": true,
"test/unit/*.d.ts": true,
"test/unit/*.js.map": true,
"test/integration/*.js": true,
"test/integration/*.d.ts": true,
"test/integration/*.js.map": true,
"browser.js": true,
"browser.js.map": true,
"browser/**/*.js": true,
"browser/**/*.js.map": true
},
"typescript.tsdk": "./node_modules/typescript/lib"
}
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## 6.6.0-pre.5 - 2018-04-12
* Restore missing dependency 'findup-sync'.
## 6.6.0-pre.4 - 2018-04-11
* Upgrade polyserve to pick up recent fixes.
## 6.6.0-pre.3 - 2018-03-21
* Upgrade polyserve to ^0.25.2 and add the --module-resolution flag
* Fix #488 - Support .bowerrc directory name override of bower_components, including variants
* Injected libraries are now resolved to correct paths while using the runner. Note: simply serving the test file and running it will attempt to find the files, but they may load the wrong versions (e.g. lodash 4 might load instead of 3)
* Not published: 6.6.0-pre.1 & 6.6.0-pre.2
## 6.5.0 - 2018-01-17
* Upgrade wct-local to 2.1.0 to get support for headless Chrome.
## 6.4.3 - 2018-01-11
* web-component-tester: no longer injects `a11ySuite.js` script in `--npm` mode.
* wct-browser-legacy: `a11ySuite.js` now exports a `a11ySuite` reference. Import this reference direction to use `a11ySuite()` in npm.
## 6.4.2 - 2018-01-09
* Upgrade wct-sauce to 2.0.0 to get updated browsers lists to include Safari 11 and Edge 15.
* Fixed #523 WCT ignores the webserver hostname
* Remove `overflow-y: auto` from test runner styling to increase performance.
## 6.4.1 - 2017-11-20
* Ensure that WCT is installed with compatible versions of wct-local and wct-sauce. This fixes a bug where – if incompatible versions are installed – they aren't able to coordinate shutdown, so WCT hangs after successfully completing a test run.
## 6.4.0 - 2017-10-31 🎃
* Updated package.json:
* Upgraded dependencies async, chai, cleankill, findup-sync, sinon, and socket.io.
* Upgraded devDependencies update-notifier
* Added `define:webserver` hook to enable substitution of the generated express app for the webserver through a callback, to support use cases where a plugin might want to inject handlers or middleware in front of polyserve.
* Added support for `proxy: {path: string, target: string}` config which is forwarded to `polyserve`.
## 6.3.0 - 2017-10-02
* Updated wct-browser-legacy to use a module version of a11ySuite to get access to Polymer.dom.flush.
* Updated generated index for webserver to use a11ySuite as a module.
* Updated polyserve to get support for on-the-fly module compilation and ``).
* Browser options can be specified in `wct.conf.js` via the `clientOptions` key.
* WCT now always generates an index when run via the command line.
* `wct.conf.json` can be used as an alternative to `wct.conf.js`.
## 3.0.0-3.0.6
Yanked. See `3.0.7` for rolled up notes.
# 2.x
There were changes made, and @nevir failed to annotate them. What a slacker.
# 1.x
What are you, an archaeologist?
================================================
FILE: LICENSE
================================================
Copyright (c) 2015 The Polymer Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
## 🚨 Moved to [`Polymer/tools/packages/web-component-tester`][1] 🚨
The [`Polymer/web-component-tester`][2] repo has been migrated to [`packages/web-component-tester`][1] folder of the [`Polymer/tools`][3] 🚝 *monorepo*.
We are *actively* working on migrating open Issues and PRs to the new repo. New Issues and PRs should be filed at [`Polymer/tools`][3].
[1]: https://github.com/Polymer/tools/tree/master/packages/web-component-tester
[2]: https://github.com/Polymer/web-component-tester
[3]: https://github.com/Polymer/tools
================================================
FILE: bin/wct
================================================
#!/usr/bin/env node
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
var resolve = require('resolve');
process.title = 'wct';
resolve('web-component-tester', {basedir: process.cwd()}, function(error, path) {
var wct = path ? require(path) : require('..');
var promise = wct.cli.run(process.env, process.argv.slice(2), process.stdout, function (error) {
process.exit(error ? 1 : 0);
});
if (promise) {
promise.then(() => process.exit(0), () => process.exit(1));
}
});
================================================
FILE: bin/wct-st
================================================
#!/usr/bin/env node
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
var resolve = require('resolve');
process.title = 'wct-st';
resolve('web-component-tester', {basedir: process.cwd()}, function(error, path) {
var wct = path ? require(path) : require('..');
var promise = wct.cli.runSauceTunnel(process.env, process.argv.slice(2), process.stdout, function (error) {
process.exit(error ? 1 : 0);
});
if (promise) {
promise.then(() => process.exit(0), () => process.exit(1));
}
});
================================================
FILE: bower.json
================================================
{
"name": "web-component-tester",
"description": "web-component-tester makes testing your web components a breeze!",
"version": "6.0.0",
"main": [
"browser.js"
],
"license": "http://polymer.github.io/LICENSE.txt",
"ignore": [
"*",
"!/data/*",
"!/browser.js",
"!/browser.js.map",
"!/package.json",
"!/bower.json"
],
"keywords": [
"browser",
"grunt",
"gruntplugin",
"gulp",
"polymer",
"test",
"testing",
"web component",
"web"
],
"dependencies": {
"accessibility-developer-tools": "^2.10.0",
"async": "^1.5.0",
"chai": "^3.2.0",
"lodash": "^3.7.0",
"mocha": "^3.1.2",
"sinon-chai": "^2.7.0",
"sinonjs": "^1.14.1",
"stacky": "^1.3.0",
"test-fixture": "^3.0.0"
},
"devDependencies": {
"polymer": "Polymer/polymer#^1.5.0",
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.22"
}
}
================================================
FILE: browser/childrunner.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as util from './util.js';
// TODO(thedeeno): Consider renaming subsuite. IIRC, childRunner is entirely
// distinct from mocha suite, which tripped me up badly when trying to add
// plugin support. Perhaps something like 'batch', or 'bundle'. Something that
// has no mocha correlate. This may also eliminate the need for root/non-root
// suite distinctions.
export interface SharedState {}
/**
* A Mocha suite (or suites) run within a child iframe, but reported as if they
* are part of the current context.
*/
export default class ChildRunner {
private url: string;
parentScope: Window;
private state: 'initializing'|'loading'|'complete';
private iframe?: HTMLIFrameElement;
private onRunComplete: (error?: any) => void;
private timeoutId: undefined|number;
private share: SharedState;
constructor(url: string, parentScope: Window) {
const urlBits = util.parseUrl(url);
util.mergeParams(
urlBits.params, util.getParams(parentScope.location.search));
delete urlBits.params.cli_browser_id;
this.url = urlBits.base + util.paramsToQuery(urlBits.params);
this.parentScope = parentScope;
this.state = 'initializing';
}
// ChildRunners get a pretty generous load timeout by default.
static loadTimeout = 60000;
// We can't maintain properties on iframe elements in Firefox/Safari/???, so
// we track childRunners by URL.
static _byUrl: {[url: string]: undefined|ChildRunner} = {};
/**
* @return {ChildRunner} The `ChildRunner` that was registered for this
* window.
*/
static current(): ChildRunner {
return ChildRunner.get(window);
}
/**
* @param {!Window} target A window to find the ChildRunner of.
* @param {boolean} traversal Whether this is a traversal from a child window.
* @return {ChildRunner} The `ChildRunner` that was registered for `target`.
*/
static get(target: Window, traversal?: boolean): ChildRunner {
const childRunner = ChildRunner._byUrl[target.location.href];
if (childRunner) {
return childRunner;
}
if (window.parent === window) { // Top window.
if (traversal) {
console.warn(
'Subsuite loaded but was never registered. This most likely is due to wonky history behavior. Reloading...');
window.location.reload();
}
return null;
}
// Otherwise, traverse.
return window.parent.WCT._ChildRunner.get(target, true);
}
/**
* Loads and runs the subsuite.
*
* @param {function} done Node-style callback.
*/
run(done: (error?: any) => void) {
util.debug('ChildRunner#run', this.url);
this.state = 'loading';
this.onRunComplete = done;
this.iframe = document.createElement('iframe');
this.iframe.src = this.url;
this.iframe.classList.add('subsuite');
let container = document.getElementById('subsuites');
if (!container) {
container = document.createElement('div');
container.id = 'subsuites';
document.body.appendChild(container);
}
container.appendChild(this.iframe);
// let the iframe expand the URL for us.
this.url = this.iframe.src;
ChildRunner._byUrl[this.url] = this;
this.timeoutId = setTimeout(
this.loaded.bind(this, new Error('Timed out loading ' + this.url)),
ChildRunner.loadTimeout);
this.iframe.addEventListener(
'error',
this.loaded.bind(
this, new Error('Failed to load document ' + this.url)));
this.iframe.contentWindow.addEventListener(
'DOMContentLoaded', this.loaded.bind(this, null));
}
/**
* Called when the sub suite's iframe has loaded (or errored during load).
*
* @param {*} error The error that occured, if any.
*/
loaded(error: any) {
util.debug('ChildRunner#loaded', this.url, error);
if (this.iframe.contentWindow == null && error) {
this.signalRunComplete(error);
this.done();
return;
}
// Not all targets have WCT loaded (compatiblity mode)
if (this.iframe.contentWindow.WCT) {
this.share = this.iframe.contentWindow.WCT.share;
}
if (error) {
this.signalRunComplete(error);
this.done();
}
}
/**
* Called in mocha/run.js when all dependencies have loaded, and the child is
* ready to start running tests
*
* @param {*} error The error that occured, if any.
*/
ready(error?: any) {
util.debug('ChildRunner#ready', this.url, error);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (error) {
this.signalRunComplete(error);
this.done();
}
}
/**
* Called when the sub suite's tests are complete, so that it can clean up.
*/
done() {
util.debug('ChildRunner#done', this.url, arguments);
// make sure to clear that timeout
this.ready();
this.signalRunComplete();
if (!this.iframe)
return;
// Be safe and avoid potential browser crashes when logic attempts to
// interact with the removed iframe.
setTimeout(function() {
this.iframe.parentNode.removeChild(this.iframe);
this.iframe = null;
this.share = null;
}.bind(this), 1);
}
signalRunComplete(error?: any) {
if (!this.onRunComplete)
return;
this.state = 'complete';
this.onRunComplete(error);
this.onRunComplete = null;
}
}
================================================
FILE: browser/clisocket.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import ChildRunner from './childrunner.js';
import * as util from './util.js';
const SOCKETIO_ENDPOINT =
window.location.protocol + '//' + window.location.host;
const SOCKETIO_LIBRARY = SOCKETIO_ENDPOINT + '/socket.io/socket.io.js';
/**
* A socket for communication between the CLI and browser runners.
*
* @param {string} browserId An ID generated by the CLI runner.
* @param {!io.Socket} socket The socket.io `Socket` to communicate over.
*/
export default class CLISocket {
private readonly socket: SocketIO.Socket;
private readonly browserId: string;
constructor(browserId: string, socket: SocketIO.Socket) {
this.browserId = browserId;
this.socket = socket;
}
/**
* @param {!Mocha.Runner} runner The Mocha `Runner` to observe, reporting
* interesting events back to the CLI runner.
*/
observe(runner: Mocha.IRunner) {
this.emitEvent('browser-start', {
url: window.location.toString(),
});
// We only emit a subset of events that we care about, and follow a more
// general event format that is hopefully applicable to test runners beyond
// mocha.
//
// For all possible mocha events, see:
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36
runner.on('test', (test: Mocha.IRunnable) => {
this.emitEvent('test-start', {test: getTitles(test)});
});
runner.on('test end', (test: Mocha.IRunnable) => {
this.emitEvent('test-end', {
state: getState(test),
test: getTitles(test),
duration: (test as any).duration,
error: (test as any).err,
});
});
runner.on('fail', (test, err) => {
// fail the test run if we catch errors outside of a test function
if (test.type !== 'test') {
this.emitEvent(
'browser-fail',
'Error thrown outside of test function: ' + err.stack);
}
});
runner.on('childRunner start', (childRunner) => {
this.emitEvent('sub-suite-start', childRunner.share);
});
runner.on('childRunner end', (childRunner) => {
this.emitEvent('sub-suite-end', childRunner.share);
});
runner.on('end', () => {
this.emitEvent('browser-end');
});
}
/**
* @param {string} event The name of the event to fire.
* @param {*} data Additional data to pass with the event.
*/
emitEvent(event: string, data?: any) {
this.socket.emit('client-event', {
browserId: this.browserId,
event: event,
data: data,
});
}
/**
* Builds a `CLISocket` if we are within a CLI-run environment; short-circuits
* otherwise.
*
* @param {function(*, CLISocket)} done Node-style callback.
*/
static init(done: (error?: any, socket?: CLISocket) => void) {
const browserId = util.getParam('cli_browser_id');
if (!browserId)
return done();
// Only fire up the socket for root runners.
if (ChildRunner.current())
return done();
util.loadScript(SOCKETIO_LIBRARY, function(error: any) {
if (error)
return done(error);
const socket = io(SOCKETIO_ENDPOINT);
socket.on('error', function(error?: any) {
socket.off();
done(error);
});
socket.on('connect', function() {
socket.off();
done(null, new CLISocket(browserId, socket as any));
});
});
}
}
// Misc Utility
/**
* @param {!Mocha.Runnable} runnable The test or suite to extract titles from.
* @return {!Array.} The titles of the runnable and its parents.
*/
function getTitles(runnable: Mocha.IRunnable) {
const titles = [];
while (runnable && !runnable.root && runnable.title) {
titles.unshift(runnable.title);
runnable = runnable.parent as any;
}
return titles;
}
/**
* @param {!Mocha.Runnable} runnable
* @return {string}
*/
function getState(runnable: Mocha.IRunnable) {
if (runnable.state === 'passed') {
return 'passing';
} else if (runnable.state === 'failed') {
return 'failing';
} else if (runnable.pending) {
return 'pending';
} else {
return 'unknown';
}
}
================================================
FILE: browser/config.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import ChildRunner from './childrunner.js';
import * as util from './util.js';
export interface Config {
/**
* `.js` scripts to be loaded (synchronously) before WCT starts in earnest.
*
* Paths are relative to `scriptPrefix`.
*/
environmentScripts: string[];
environmentImports: string[];
/** Absolute root for client scripts. Detected in `setup()` if not set. */
root: null|string;
/** By default, we wait for any web component frameworks to load. */
waitForFrameworks: boolean;
/**
* Alternate callback for waiting for tests.
* `this` for the callback will be the window currently running tests.
*/
waitFor: null|Function;
/** How many `.html` suites that can be concurrently loaded & run. */
numConcurrentSuites: number;
/** Whether `console.error` should be treated as a test failure. */
trackConsoleError: boolean;
/** Configuration passed to mocha.setup. */
mochaOptions: MochaSetupOptions;
/** Whether WCT should emit (extremely verbose) debugging log messages. */
verbose: boolean;
}
/**
* The global configuration state for WCT's browser client.
*/
export let _config: Config = {
environmentScripts: !!window.__wctUseNpm ?
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/index.js',
'mocha/mocha.js', 'chai/chai.js', '@polymer/sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js',
'@polymer/test-fixture/test-fixture.js'
] :
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/lodash.js',
'mocha/mocha.js', 'chai/chai.js', 'sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js'
],
environmentImports: !!window.__wctUseNpm ? [] :
['test-fixture/test-fixture.html'],
root: null as null | string,
waitForFrameworks: true,
waitFor: null as null | Function,
numConcurrentSuites: 1,
trackConsoleError: true,
mochaOptions: {timeout: 10 * 1000},
verbose: false,
};
/**
* Merges initial `options` into WCT's global configuration.
*
* @param {Object} options The options to merge. See `browser/config.js` for a
* reference.
*/
export function setup(options: Config) {
const childRunner = ChildRunner.current();
if (childRunner) {
_deepMerge(_config, childRunner.parentScope.WCT._config);
// But do not force the mocha UI
delete _config.mochaOptions.ui;
}
if (options && typeof options === 'object') {
_deepMerge(_config, options);
}
if (!_config.root) {
// Sibling dependencies.
const root = util.scriptPrefix('browser.js');
_config.root = util.basePath(root.substr(0, root.length - 1));
if (!_config.root) {
throw new Error(
'Unable to detect root URL for WCT sources. Please set WCT.root before including browser.js');
}
}
}
/**
* Retrieves a configuration value.
*/
export function get(key: K): Config[K] {
return _config[key];
}
// Internal
function _deepMerge(target: Partial, source: Config) {
Object.keys(source).forEach(function(key) {
if (target[key] !== null && typeof target[key] === 'object' &&
!Array.isArray(target[key])) {
_deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
================================================
FILE: browser/declarations.ts
================================================
import * as ChaiStatic from 'chai';
import * as SinonStatic from 'sinon';
import * as SocketIOStatic from 'socket.io';
import * as StackyStatic from 'stacky';
import {default as ChildRunner, SharedState} from './childrunner.js';
import {Config} from './config.js';
import MultiReporter from './reporters/multi.js';
import * as suites from './suites.js';
type loadSuitesType = (typeof suites.loadSuites);
declare global {
interface Window {
__wctUseNpm?: boolean;
WebComponents?: WebComponentsStatic;
Platform?: PlatformStatic;
Polymer?: PolymerStatic;
WCT: {
readonly _ChildRunner: typeof ChildRunner; //
readonly share: SharedState; //
readonly _config: Config; //
readonly loadSuites: loadSuitesType;
_reporter: MultiReporter;
};
mocha: typeof mocha;
Mocha: typeof Mocha;
__generatedByWct?: boolean;
chai: typeof ChaiStatic;
assert: typeof ChaiStatic.assert;
expect: typeof ChaiStatic.expect;
}
interface WebComponentsStatic {
ready?(): void;
flush?(): void;
}
interface PlatformStatic {
performMicrotaskCheckpoint(): void;
}
interface PolymerElement {
_stampTemplate?(): void;
}
interface PolymerElementConstructor {
prototype: PolymerElement;
}
interface PolymerStatic {
flush(): void;
dom: {flush(): void};
Element: PolymerElementConstructor;
}
interface Element {
isConnected?: boolean;
}
interface Mocha {
suite: Mocha.ISuite;
}
var Stacky: typeof StackyStatic;
var io: typeof SocketIOStatic;
var Platform: PlatformStatic;
var sinon: typeof SinonStatic;
}
================================================
FILE: browser/environment/compatability.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import ChildRunner from '../childrunner.js';
// polymer-test-tools (and Polymer/tools) support HTML tests where each file is
// expected to call `done()`, which posts a message to the parent window.
window.addEventListener('message', function(event) {
if (!event.data || (event.data !== 'ok' && !event.data.error)) {
return;
}
const childRunner = ChildRunner.get(event.source);
if (!childRunner) {
return;
}
childRunner.ready();
// The name of the suite as exposed to the user.
const reporter = childRunner.parentScope.WCT._reporter;
const title = reporter.suiteTitle(event.source.location);
reporter.emitOutOfBandTest(
'page-wide tests via global done()', event.data.error, title, true);
childRunner.done();
});
// Attempt to ensure that we complete a test suite if it is interrupted by a
// document unload.
window.addEventListener('unload', function(_event: BeforeUnloadEvent) {
// Mocha's hook queue is asynchronous; but we want synchronous behavior if
// we've gotten to the point of unloading the document.
Mocha.Runner.immediately = function(callback: () => void) {
callback();
};
});
================================================
FILE: browser/environment/errors.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as config from '../config.js';
// We may encounter errors during initialization (for example, syntax errors in
// a test file). Hang onto those (and more) until we are ready to report them.
export let globalErrors: any[] = [];
/**
* Hook the environment to pick up on global errors.
*/
export function listenForErrors() {
window.addEventListener('error', function(event) {
globalErrors.push(event.error);
});
// Also, we treat `console.error` as a test failure. Unless you prefer not.
const origConsole = console;
const origError = console.error;
console.error = function wctShimmedError() {
origError.apply(origConsole, arguments);
if (config.get('trackConsoleError')) {
throw 'console.error: ' + Array.prototype.join.call(arguments, ' ');
}
};
}
================================================
FILE: browser/environment/helpers.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
export {};
// Make sure that we use native timers, in case they're being stubbed out.
const nativeSetInterval = window.setInterval;
const nativeSetTimeout = window.setTimeout;
const nativeRequestAnimationFrame = window.requestAnimationFrame;
/**
* Runs `stepFn`, catching any error and passing it to `callback` (Node-style).
* Otherwise, calls `callback` with no arguments on success.
*
* @param {function()} callback
* @param {function()} stepFn
*/
function safeStep(callback: (error?: any) => void, stepFn: () => void) {
let err;
try {
stepFn();
} catch (error) {
err = error;
}
callback(err);
}
/**
* Runs your test at declaration time (before Mocha has begun tests). Handy for
* when you need to test document initialization.
*
* Be aware that any errors thrown asynchronously cannot be tied to your test.
* You may want to catch them and pass them to the done event, instead. See
* `safeStep`.
*
* @param {string} name The name of the test.
* @param {function(?function())} testFn The test function. If an argument is
* accepted, the test will be treated as async, just like Mocha tests.
*/
function testImmediate(name: string, testFn: Function) {
if (testFn.length > 0) {
return testImmediateAsync(name, testFn);
}
let err: any;
try {
testFn();
} catch (error) {
err = error;
}
test(name, function(done) {
done(err);
});
}
/**
* An async-only variant of `testImmediate`.
*
* @param {string} name
* @param {function(?function())} testFn
*/
function testImmediateAsync(name: string, testFn: Function) {
let testComplete = false;
let err: any;
test(name, function(done) {
const intervalId = nativeSetInterval(function() {
if (!testComplete)
return;
clearInterval(intervalId);
done(err);
}, 10);
});
try {
testFn(function(error: any) {
if (error)
err = error;
testComplete = true;
});
} catch (error) {
err = error;
testComplete = true;
}
}
/**
* Triggers a flush of any pending events, observations, etc and calls you back
* after they have been processed.
*
* @param {function()} callback
*/
function flush(callback: () => void) {
// Ideally, this function would be a call to Polymer.dom.flush, but that
// doesn't support a callback yet
// (https://github.com/Polymer/polymer-dev/issues/851),
// ...and there's cross-browser flakiness to deal with.
// Make sure that we're invoking the callback with no arguments so that the
// caller can pass Mocha callbacks, etc.
let done = function done() {
callback();
};
// Because endOfMicrotask is flaky for IE, we perform microtask checkpoints
// ourselves (https://github.com/Polymer/polymer-dev/issues/114):
const isIE = navigator.appName === 'Microsoft Internet Explorer';
if (isIE && window.Platform && window.Platform.performMicrotaskCheckpoint) {
const reallyDone = done;
done = function doneIE() {
Platform.performMicrotaskCheckpoint();
nativeSetTimeout(reallyDone, 0);
};
}
// Everyone else gets a regular flush.
let scope;
if (window.Polymer && window.Polymer.dom && window.Polymer.dom.flush) {
scope = window.Polymer.dom;
} else if (window.Polymer && window.Polymer.flush) {
scope = window.Polymer;
} else if (window.WebComponents && window.WebComponents.flush) {
scope = window.WebComponents;
}
if (scope) {
scope.flush();
}
// Ensure that we are creating a new _task_ to allow all active microtasks to
// finish (the code you're testing may be using endOfMicrotask, too).
nativeSetTimeout(done, 0);
}
/**
* Advances a single animation frame.
*
* Calls `flush`, `requestAnimationFrame`, `flush`, and `callback` sequentially
* @param {function()} callback
*/
function animationFrameFlush(callback: () => void) {
flush(function() {
nativeRequestAnimationFrame(function() {
flush(callback);
});
});
}
/**
* DEPRECATED: Use `flush`.
* @param {function} callback
*/
function asyncPlatformFlush(callback: () => void) {
console.warn(
'asyncPlatformFlush is deprecated in favor of the more terse flush()');
return window.flush(callback);
}
export interface MutationEl {
onMutation(mutationEl: this, cb: () => void): void;
}
/**
*
*/
function waitFor(
fn: () => void, next: () => void, intervalOrMutationEl: number|MutationEl,
timeout: number, timeoutTime: number) {
timeoutTime = timeoutTime || Date.now() + (timeout || 1000);
intervalOrMutationEl = intervalOrMutationEl || 32;
try {
fn();
} catch (e) {
if (Date.now() > timeoutTime) {
throw e;
} else {
if (typeof intervalOrMutationEl !== 'number') {
intervalOrMutationEl.onMutation(intervalOrMutationEl, function() {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
});
} else {
nativeSetTimeout(function() {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
}, intervalOrMutationEl);
}
return;
}
}
next();
}
declare global {
interface Window {
safeStep: typeof safeStep;
testImmediate: typeof testImmediate;
testImmediateAsync: typeof testImmediateAsync;
flush: typeof flush;
animationFrameFlush: typeof animationFrameFlush;
asyncPlatformFlush: typeof asyncPlatformFlush;
waitFor: typeof waitFor;
}
}
window.safeStep = safeStep;
window.testImmediate = testImmediate;
window.testImmediateAsync = testImmediateAsync;
window.flush = flush;
window.animationFrameFlush = animationFrameFlush;
window.asyncPlatformFlush = asyncPlatformFlush;
window.waitFor = waitFor;
================================================
FILE: browser/environment.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as config from './config.js';
import * as reporters from './reporters.js';
import * as util from './util.js';
/**
* Loads all environment scripts ...synchronously ...after us.
*/
export function loadSync() {
util.debug('Loading environment scripts:');
const a11ySuiteScriptPath = 'web-component-tester/data/a11ySuite.js';
const scripts = config.get('environmentScripts');
const a11ySuiteWillBeLoaded =
window.__generatedByWct || scripts.indexOf(a11ySuiteScriptPath) > -1;
// We can't inject a11ySuite when running the npm version because it is a
// module-based script that needs `'); // jshint ignore:line
});
util.debug('Environment scripts loaded');
const imports = config.get('environmentImports');
imports.forEach(function(path) {
const url = util.expandUrl(path, config.get('root'));
util.debug('Loading environment import:', url);
// Synchronous load.
document.write(
''); // jshint ignore:line
});
util.debug('Environment imports loaded');
}
/**
* We have some hard dependencies on things that should be loaded via
* `environmentScripts`, so we assert that they're present here; and do any
* post-facto setup.
*/
export function ensureDependenciesPresent() {
_ensureMocha();
_checkChai();
}
function _ensureMocha() {
const Mocha = window.Mocha;
if (!Mocha) {
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/browser.js');
}
reporters.injectMocha(Mocha);
// Magic loading of mocha's stylesheet
const mochaPrefix = util.scriptPrefix('mocha.js');
// only load mocha stylesheet for the test runner output
// Not the end of the world, if it doesn't load.
if (mochaPrefix && window.top === window.self) {
util.loadStyle(mochaPrefix + 'mocha.css');
}
}
function _checkChai() {
if (!window.chai) {
util.debug('Chai not present; not registering shorthands');
return;
}
window.assert = window.chai.assert;
window.expect = window.chai.expect;
}
================================================
FILE: browser/index.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* This file is the entry point into `web-component-tester`'s browser client.
*/
// Registers a bunch of globals:
import './environment/helpers.js';
import ChildRunner from './childrunner.js';
import CLISocket from './clisocket.js';
import * as config from './config.js';
import * as environment from './environment.js';
import * as errors from './environment/errors.js';
import * as mocha from './mocha.js';
import * as reporters from './reporters.js';
import MultiReporter from './reporters/multi.js';
import * as suites from './suites.js';
import * as util from './util.js';
// You can configure WCT before it has loaded by assigning your custom
// configuration to the global `WCT`.
config.setup(window.WCT as any as config.Config);
// Maybe some day we'll expose WCT as a module to whatever module registry you
// are using (aka the UMD approach), or as an es6 module.
const WCT = window.WCT = {
// A generic place to hang data about the current suite. This object is
// reported
// back via the `sub-suite-start` and `sub-suite-end` events.
share: {},
// Until then, we get to rely on it to expose parent runners to their
// children.
_ChildRunner: ChildRunner,
_reporter: undefined as any, // assigned below
_config: config._config,
// Public API
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param {!Array.} files The files to load.
*/
loadSuites: suites.loadSuites,
};
// Load Process
errors.listenForErrors();
mocha.stubInterfaces();
environment.loadSync();
// Give any scripts on the page a chance to declare tests and muck with things.
document.addEventListener('DOMContentLoaded', function() {
util.debug('DOMContentLoaded');
environment.ensureDependenciesPresent();
// We need the socket built prior to building its reporter.
CLISocket.init(function(error, socket) {
if (error)
throw error;
// Are we a child of another run?
const current = ChildRunner.current();
const parent = current && current.parentScope.WCT._reporter;
util.debug('parentReporter:', parent);
const childSuites = suites.activeChildSuites();
const reportersToUse = reporters.determineReporters(socket, parent);
// +1 for any local tests.
const reporter =
new MultiReporter(childSuites.length + 1, reportersToUse, parent);
WCT._reporter = reporter; // For environment/compatibility.js
// We need the reporter so that we can report errors during load.
suites.loadJsSuites(reporter, function(error) {
// Let our parent know that we're about to start the tests.
if (current)
current.ready(error);
if (error)
throw error;
// Emit any errors we've encountered up til now
errors.globalErrors.forEach(function onError(error) {
reporter.emitOutOfBandTest('Test Suite Initialization', error);
});
suites.runSuites(reporter, childSuites, function(error) {
// Make sure to let our parent know that we're done.
if (current)
current.done();
if (error)
throw error;
});
});
});
});
================================================
FILE: browser/mocha/extend.ts
================================================
const interfaceExtensions: Array<() => void> = [];
/**
* Registers an extension that extends the global `Mocha` implementation
* with new helper methods. These helper methods will be added to the `window`
* when tests run for both BDD and TDD interfaces.
*/
export function extendInterfaces(
helperName: string,
helperFactory: (
context: any, teardown: (cb: () => void) => void,
interfaceName: 'tdd'|'bdd') => void) {
interfaceExtensions.push(function() {
const Mocha = window.Mocha;
// For all Mocha interfaces (probably just TDD and BDD):
Object.keys((Mocha as any).interfaces)
.forEach(function(interfaceName: 'tdd'|'bdd') {
// This is the original callback that defines the interface (TDD or
// BDD):
const originalInterface = (Mocha as any).interfaces[interfaceName];
// This is the name of the "teardown" or "afterEach" property for the
// current interface:
const teardownProperty =
interfaceName === 'tdd' ? 'teardown' : 'afterEach';
// The original callback is monkey patched with a new one that appends
// to the global context however we want it to:
(Mocha as any).interfaces[interfaceName] = function(suite: any) {
// Call back to the original callback so that we get the base
// interface:
originalInterface.apply(this, arguments);
// Register a listener so that we can further extend the base
// interface:
suite.on(
'pre-require',
function(context: any, _file: string, _mocha: any) {
// Capture a bound reference to the teardown function as a
// convenience:
const teardown = context[teardownProperty].bind(context);
// Add our new helper to the testing context. The helper is
// generated by a factory method that receives the context,
// the teardown function and the interface name and returns
// the new method to be added to that context:
context[helperName] =
helperFactory(context, teardown, interfaceName);
});
};
});
});
}
/**
* Applies any registered interface extensions. The extensions will be applied
* as many times as this function is called, so don't call it more than once.
*/
export function applyExtensions() {
interfaceExtensions.forEach(function(applyExtension) {
applyExtension();
});
}
================================================
FILE: browser/mocha/fixture.ts
================================================
import {extendInterfaces} from './extend.js';
interface TestFixture extends HTMLElement {
create(model: object): HTMLElement;
restore(): void;
}
extendInterfaces('fixture', function(context, teardown) {
// Return context.fixture if it is already a thing, for backwards
// compatibility with `test-fixture-mocha.js`:
return context.fixture || function fixture(fixtureId: string, model: object) {
// Automatically register a teardown callback that will restore the
// test-fixture:
teardown(function() {
(document.getElementById(fixtureId) as TestFixture).restore();
});
// Find the test-fixture with the provided ID and create it, returning
// the results:
return (document.getElementById(fixtureId) as TestFixture).create(model);
};
});
================================================
FILE: browser/mocha/replace.ts
================================================
import {extendInterfaces} from './extend.js';
// replacement map stores what should be
let replacements = {};
let replaceTeardownAttached = false;
/**
* replace
*
* The replace addon allows the tester to replace all usages of one element with
* another element within all Polymer elements created within the time span of
* the test. Usage example:
*
* beforeEach(function() {
* replace('x-foo').with('x-fake-foo');
* });
*
* All annotations and attributes will be set on the placement element the way
* they were set for the original element.
*/
extendInterfaces('replace', function(_context, teardown) {
return function replace(oldTagName: string) {
return {
with: function(tagName: string) {
// Standardizes our replacements map
oldTagName = oldTagName.toLowerCase();
tagName = tagName.toLowerCase();
replacements[oldTagName] = tagName;
// If the function is already a stub, restore it to original
if ((document.importNode as any).isSinonProxy) {
return;
}
if (!window.Polymer.Element) {
window.Polymer.Element = function() {};
window.Polymer.Element.prototype._stampTemplate = function() {};
}
// Keep a reference to the original `document.importNode`
// implementation for later:
const originalImportNode = document.importNode;
// Use Sinon to stub `document.ImportNode`:
sinon.stub(
document, 'importNode', function(origContent: any, deep: boolean) {
const templateClone = document.createElement('template');
const content = templateClone.content;
const inertDoc = content.ownerDocument;
// imports node from inertDoc which holds inert nodes.
templateClone.content.appendChild(
inertDoc.importNode(origContent, true));
// optional arguments are not optional on IE.
const nodeIterator = document.createNodeIterator(
content, NodeFilter.SHOW_ELEMENT, null, true);
let node;
// Traverses the tree. A recently-replaced node will be put next,
// so if a node is replaced, it will be checked if it needs to be
// replaced again.
while (node = nodeIterator.nextNode() as Element) {
let currentTagName = node.tagName.toLowerCase();
if (replacements.hasOwnProperty(currentTagName)) {
currentTagName = replacements[currentTagName];
// find the final tag name.
while (replacements[currentTagName]) {
currentTagName = replacements[currentTagName];
}
// Create a replacement:
const replacement = document.createElement(currentTagName);
// For all attributes in the original node..
for (let index = 0; index < node.attributes.length; ++index) {
// Set that attribute on the replacement:
replacement.setAttribute(
node.attributes[index].name,
node.attributes[index].value);
}
// Replace the original node with the replacement node:
node.parentNode.replaceChild(replacement, node);
}
}
return originalImportNode.call(this, content, deep);
});
if (!replaceTeardownAttached) {
// After each test...
teardown(function() {
replaceTeardownAttached = true;
// Restore the stubbed version of `document.importNode`:
const documentImportNode = document.importNode as any;
if (documentImportNode.isSinonProxy) {
documentImportNode.restore();
}
// Empty the replacement map
replacements = {};
});
}
}
};
};
});
================================================
FILE: browser/mocha/stub.ts
================================================
import {extendInterfaces} from './extend.js';
/**
* stub
*
* The stub addon allows the tester to partially replace the implementation of
* an element with some custom implementation. Usage example:
*
* beforeEach(function() {
* stub('x-foo', {
* attached: function() {
* // Custom implementation of the `attached` method of element `x-foo`..
* },
* otherMethod: function() {
* // More custom implementation..
* },
* getterSetterProperty: {
* get: function() {
* // Custom getter implementation..
* },
* set: function() {
* // Custom setter implementation..
* }
* },
* // etc..
* });
* });
*/
extendInterfaces('stub', function(_context, teardown) {
return function stub(tagName: string, implementation: object) {
// Find the prototype of the element being stubbed:
const proto = document.createElement(tagName).constructor.prototype;
// For all keys in the implementation to stub with..
const stubs = Object.keys(implementation).map(function(key) {
// Stub the method on the element prototype with Sinon:
return sinon.stub(proto, key, implementation[key]);
});
// After all tests..
teardown(function() {
stubs.forEach(function(stub) {
stub.restore();
});
});
};
});
================================================
FILE: browser/mocha.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import './mocha/fixture.js';
import './mocha/stub.js';
import './mocha/replace.js';
import * as config from './config.js';
import {applyExtensions} from './mocha/extend.js';
// Mocha global helpers, broken out by testing method.
//
// Keys are the method for a particular interface; values are their analog in
// the opposite interface.
const MOCHA_EXPORTS = {
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/tdd.js
tdd: {
'setup': '"before"',
'teardown': '"after"',
'suiteSetup': '"beforeEach"',
'suiteTeardown': '"afterEach"',
'suite': '"describe" or "context"',
'test': '"it" or "specify"',
},
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/bdd.js
bdd: {
'before': '"setup"',
'after': '"teardown"',
'beforeEach': '"suiteSetup"',
'afterEach': '"suiteTeardown"',
'describe': '"suite"',
'context': '"suite"',
'xdescribe': '"suite.skip"',
'xcontext': '"suite.skip"',
'it': '"test"',
'xit': '"test.skip"',
'specify': '"test"',
'xspecify': '"test.skip"',
},
};
/**
* Exposes all Mocha methods up front, configuring and running mocha
* automatically when you call them.
*
* The assumption is that it is a one-off (sub-)suite of tests being run.
*/
export function stubInterfaces() {
const keys = Object.keys(MOCHA_EXPORTS) as Array;
keys.forEach(function(ui) {
Object.keys(MOCHA_EXPORTS[ui]).forEach(function(key) {
window[key] = function wrappedMochaFunction() {
_setupMocha(ui, key, MOCHA_EXPORTS[ui][key]);
if (!window[key] || window[key] === wrappedMochaFunction) {
throw new Error('Expected mocha.setup to define ' + key);
}
window[key].apply(window, arguments);
};
});
});
}
// Whether we've called `mocha.setup`
const _mochaIsSetup = false;
/**
* @param {string} ui Sets up mocha to run `ui`-style tests.
* @param {string} key The method called that triggered this.
* @param {string} alternate The matching method in the opposite interface.
*/
function _setupMocha(ui: 'tdd'|'bdd', key: string, alternate: 'string') {
const mochaOptions = config.get('mochaOptions');
if (mochaOptions.ui && mochaOptions.ui !== ui) {
const message = 'Mixing ' + mochaOptions.ui + ' and ' + ui +
' Mocha styles is not supported. ' +
'You called "' + key + '". Did you mean ' + alternate + '?';
throw new Error(message);
}
if (_mochaIsSetup) {
return;
}
applyExtensions();
mochaOptions.ui = ui;
mocha.setup(mochaOptions); // Note that the reporter is configured in run.js.
}
================================================
FILE: browser/more-declarations.ts
================================================
declare namespace Mocha {
interface UtilsStatic {
highlightTags(somethingSomething: string): void;
}
let utils: UtilsStatic;
interface IRunner extends NodeJS.EventEmitter {
name?: string;
total: number;
}
interface IRunnable {
parent: ISuite;
root: boolean;
state: 'passed'|'failed'|undefined;
pending: boolean;
}
interface ISuite {
root: boolean;
}
let Runner: {prototype: IRunner; immediately(callback: () => void): void};
}
declare namespace SocketIO {
interface Server {
off(): void;
}
}
================================================
FILE: browser/reporters/console.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as util from '../util.js';
// We capture console events when running tests; so make sure we have a
// reference to the original one.
const console = window.console;
const FONT =
';font: normal 13px "Roboto", "Helvetica Neue", "Helvetica", sans-serif;';
const STYLES = {
plain: FONT,
suite: 'color: #5c6bc0' + FONT,
test: FONT,
passing: 'color: #259b24' + FONT,
pending: 'color: #e65100' + FONT,
failing: 'color: #c41411' + FONT,
stack: 'color: #c41411',
results: FONT + 'font-size: 16px',
};
// I don't think we can feature detect this one...
const userAgent = navigator.userAgent.toLowerCase();
const CAN_STYLE_LOG = userAgent.match('firefox') || userAgent.match('webkit');
const CAN_STYLE_GROUP = userAgent.match('webkit');
// Track the indent for faked `console.group`
let logIndent = '';
function log(text: string, style?: keyof typeof STYLES) {
text = text.split('\n')
.map(function(l) {
return logIndent + l;
})
.join('\n');
if (CAN_STYLE_LOG) {
console.log('%c' + text, STYLES[style] || STYLES.plain);
} else {
console.log(text);
}
}
function logGroup(text: string, style?: keyof typeof STYLES) {
if (CAN_STYLE_GROUP) {
console.group('%c' + text, STYLES[style] || STYLES.plain);
} else if (console.group) {
console.group(text);
} else {
logIndent = logIndent + ' ';
log(text, style);
}
}
function logGroupEnd() {
if (console.groupEnd) {
console.groupEnd();
} else {
logIndent = logIndent.substr(0, logIndent.length - 2);
}
}
function logException(error: Error) {
log(error.stack || error.message || (error + ''), 'stack');
}
/**
* A Mocha reporter that logs results out to the web `console`.
*/
export default class Console {
/**
* @param runner The runner that is being reported on.
*/
constructor(runner: Mocha.IRunner) {
Mocha.reporters.Base.call(this, runner);
runner.on('suite', function(suite: Mocha.ISuite) {
if (suite.root) {
return;
}
logGroup(suite.title, 'suite');
}.bind(this));
runner.on('suite end', function(suite: Mocha.ISuite) {
if (suite.root) {
return;
}
logGroupEnd();
}.bind(this));
runner.on('test', function(test: Mocha.ITest) {
logGroup(test.title, 'test');
}.bind(this));
runner.on('pending', function(test: Mocha.ITest) {
logGroup(test.title, 'pending');
}.bind(this));
runner.on('fail', function(_test: Mocha.ITest, error: any) {
logException(error);
}.bind(this));
runner.on('test end', function(_test: Mocha.ITest) {
logGroupEnd();
}.bind(this));
runner.on('end', this.logSummary.bind(this));
}
/** Prints out a final summary of test results. */
logSummary() {
logGroup('Test Results', 'results');
if (this.stats.failures > 0) {
log(util.pluralizedStat(this.stats.failures, 'failing'), 'failing');
}
if (this.stats.pending > 0) {
log(util.pluralizedStat(this.stats.pending, 'pending'), 'pending');
}
log(util.pluralizedStat(this.stats.passes, 'passing'));
if (!this.stats.failures) {
log('test suite passed', 'passing');
}
log('Evaluated ' + this.stats.tests + ' tests in ' +
(this.stats as any).duration + 'ms.');
logGroupEnd();
}
}
export default interface Console extends Mocha.reporters.Base {}
================================================
FILE: browser/reporters/html.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* WCT-specific behavior on top of Mocha's default HTML reporter.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
export default function HTML(runner: Mocha.IRunner) {
const output = document.createElement('div');
output.id = 'mocha';
document.body.appendChild(output);
runner.on('suite', function(_test: any) {
this.total = runner.total;
}.bind(this));
Mocha.reporters.HTML.call(this, runner);
}
// Woo! What a hack. This just saves us from adding a bunch of complexity around
// style loading.
const style = document.createElement('style');
style.textContent = `
html, body {
position: relative;
height: 100%;
width: 100%;
min-width: 900px;
}
#mocha, #subsuites {
height: 100%;
position: absolute;
top: 0;
}
#mocha {
box-sizing: border-box;
margin: 0 !important;
padding: 60px 20px;
right: 0;
left: 500px;
}
#subsuites {
-ms-flex-direction: column;
-webkit-flex-direction: column;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
left: 0;
width: 500px;
}
#subsuites .subsuite {
border: 0;
width: 100%;
height: 100%;
}
#mocha .test.pass .duration {
color: #555 !important;
}
`;
document.head.appendChild(style);
================================================
FILE: browser/reporters/multi.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as util from '../util.js';
const STACKY_CONFIG = {
indent: ' ',
locationStrip: [
/^https?:\/\/[^\/]+/,
/\?.*$/,
],
filter(line: {location: string}) {
return !!line.location.match(/\/web-component-tester\/[^\/]+(\?.*)?$/);
},
};
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36-46
const MOCHA_EVENTS = [
'start', 'end', 'suite', 'suite end', 'test', 'test end', 'hook', 'hook end',
'pass', 'fail', 'pending', 'childRunner end'
];
// Until a suite has loaded, we assume this many tests in it.
const ESTIMATED_TESTS_PER_SUITE = 3;
export interface Reporter {}
export interface ReporterFactory { new(parent: MultiReporter): Reporter; }
interface ExtendedTest extends Mocha.ITest {
err: any;
}
/**
* A Mocha-like reporter that combines the output of multiple Mocha suites.
*/
export default class MultiReporter implements Reporter {
private readonly reporters: ReadonlyArray;
private readonly parent: MultiReporter|undefined;
private readonly basePath: string;
total: number;
private currentRunner: null|Mocha.IRunner;
/** Arguments that would be called on emit(). */
private pendingEvents: Array;
private complete: boolean|undefined;
/**
* @param numSuites The number of suites that will be run, in order to
* estimate the total number of tests that will be performed.
* @param reporters The set of reporters that
* should receive the unified event stream.
* @param parent The parent reporter, if present.
*/
constructor(
numSuites: number, reporters: ReporterFactory[],
parent: MultiReporter|undefined) {
this.reporters = reporters.map((reporter) => {
return new reporter(this);
});
this.parent = parent;
this.basePath = parent && parent.basePath || util.basePath(window.location);
this.total = numSuites * ESTIMATED_TESTS_PER_SUITE;
// Mocha reporters assume a stream of events, so we have to be careful to
// only report on one runner at a time...
this.currentRunner = null;
// ...while we buffer events for any other active runners.
this.pendingEvents = [];
this.emit('start');
}
/**
* @param location The location this reporter represents.
* @return A reporter-like "class" for each child suite
* that should be passed to `mocha.run`.
*/
childReporter(location: Location|string): ReporterFactory {
const name = this.suiteTitle(location);
// The reporter is used as a constructor, so we can't depend on `this` being
// properly bound.
const self = this;
return class ChildReporter {
constructor(runner: Mocha.IRunner) {
runner.name = window.name;
self.bindChildRunner(runner);
}
static title = window.name;
};
}
/** Must be called once all runners have finished. */
done() {
this.complete = true;
this.flushPendingEvents();
this.emit('end');
}
/**
* Emit a top level test that is not part of any suite managed by this
* reporter.
*
* Helpful for reporting on global errors, loading issues, etc.
*
* @param title The title of the test.
* @param error An error associated with this test. If falsy, test is
* considered to be passing.
* @param suiteTitle Title for the suite that's wrapping the test.
* @param estimated If this test was included in the original
* estimate of `numSuites`.
*/
emitOutOfBandTest(
title: string, error?: any, suiteTitle?: string, estimated?: boolean) {
util.debug('MultiReporter#emitOutOfBandTest(', arguments, ')');
const root: Mocha.ISuite = new (Mocha as any).Suite(suiteTitle || '');
const test: ExtendedTest = new (Mocha as any).Test(title, function() {});
test.parent = root;
test.state = error ? 'failed' : 'passed';
test.err = error;
if (!estimated) {
this.total = this.total + ESTIMATED_TESTS_PER_SUITE;
}
const runner = {total: 1} as Mocha.IRunner;
this.proxyEvent('start', runner);
this.proxyEvent('suite', runner, root);
this.proxyEvent('test', runner, test);
if (error) {
this.proxyEvent('fail', runner, test, error);
} else {
this.proxyEvent('pass', runner, test);
}
this.proxyEvent('test end', runner, test);
this.proxyEvent('suite end', runner, root);
this.proxyEvent('end', runner);
}
/**
* @param {!Location|string} location
* @return {string}
*/
suiteTitle(location: Location|string) {
let path = util.relativeLocation(location, this.basePath);
path = util.cleanLocation(path);
return path;
}
// Internal Interface
/** @param {!Mocha.runners.Base} runner The runner to listen to events for. */
private bindChildRunner(runner: Mocha.IRunner) {
MOCHA_EVENTS.forEach((eventName) => {
runner.on(eventName, this.proxyEvent.bind(this, eventName, runner));
});
}
/**
* Evaluates an event fired by `runner`, proxying it forward or buffering it.
*
* @param {string} eventName
* @param {!Mocha.runners.Base} runner The runner that emitted this event.
* @param {...*} var_args Any additional data passed as part of the event.
*/
private proxyEvent(
eventName: string, runner: Mocha.IRunner, ..._args: any[]) {
const extraArgs = Array.prototype.slice.call(arguments, 2);
if (this.complete) {
console.warn(
'out of order Mocha event for ' + runner.name + ':', eventName,
extraArgs);
return;
}
if (this.currentRunner && runner !== this.currentRunner) {
this.pendingEvents.push(Array.prototype.slice.call(arguments));
return;
}
util.debug('MultiReporter#proxyEvent(', arguments, ')');
// This appears to be a Mocha bug: Tests failed by passing an error to their
// done function don't set `err` properly.
//
// TODO(nevir): Track down.
if (eventName === 'fail' && !extraArgs[0].err) {
extraArgs[0].err = extraArgs[1];
}
if (eventName === 'start') {
this.onRunnerStart(runner);
} else if (eventName === 'end') {
this.onRunnerEnd(runner);
} else {
this.cleanEvent(eventName, runner, extraArgs);
this.emit.apply(this, [eventName].concat(extraArgs));
}
}
/**
* Cleans or modifies an event if needed.
*
* @param eventName
* @param runner The runner that emitted this event.
* @param extraArgs
*/
private cleanEvent(
eventName: string, _runner: Mocha.IRunner, extraArgs: any[]) {
// Suite hierarchy
if (extraArgs[0]) {
extraArgs[0] = this.showRootSuite(extraArgs[0]);
}
// Normalize errors
if (eventName === 'fail') {
extraArgs[1] = Stacky.normalize(extraArgs[1], STACKY_CONFIG);
}
if (extraArgs[0] && extraArgs[0].err) {
extraArgs[0].err = Stacky.normalize(extraArgs[0].err, STACKY_CONFIG);
}
}
/**
* We like to show the root suite's title, which requires a little bit of
* trickery in the suite hierarchy.
*
* @param {!Mocha.Runnable} node
*/
private showRootSuite(node: Mocha.IRunnable) {
const leaf = node = Object.create(node);
while (node && node.parent) {
const wrappedParent = Object.create(node.parent);
node.parent = wrappedParent;
node = wrappedParent;
}
node.root = false;
return leaf;
}
/** @param {!Mocha.runners.Base} runner */
private onRunnerStart(runner: Mocha.IRunner) {
util.debug('MultiReporter#onRunnerStart:', runner.name);
this.total = this.total - ESTIMATED_TESTS_PER_SUITE + runner.total;
this.currentRunner = runner;
}
/** @param {!Mocha.runners.Base} runner */
private onRunnerEnd(runner: Mocha.IRunner) {
util.debug('MultiReporter#onRunnerEnd:', runner.name);
this.currentRunner = null;
this.flushPendingEvents();
}
/**
* Flushes any buffered events and runs them through `proxyEvent`. This will
* loop until all buffered runners are complete, or we have run out of
* buffered events.
*/
private flushPendingEvents() {
const events = this.pendingEvents;
this.pendingEvents = [];
events.forEach((eventArgs) => {
this.proxyEvent.apply(this, eventArgs);
});
}
}
export default interface MultiReporter extends Mocha.IRunner,
NodeJS.EventEmitter {}
================================================
FILE: browser/reporters/title.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as util from '../util.js';
const ARC_OFFSET = 0; // start at the right.
const ARC_WIDTH = 6;
/**
* A Mocha reporter that updates the document's title and favicon with
* at-a-glance stats.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
export default class Title {
runner: Mocha.IRunner;
constructor(runner: Mocha.IRunner) {
Mocha.reporters.Base.call(this, runner);
runner.on('test end', this.report.bind(this));
}
/** Reports current stats via the page title and favicon. */
report() {
this.updateTitle();
this.updateFavicon();
}
/** Updates the document title with a summary of current stats. */
updateTitle() {
if (this.stats.failures > 0) {
document.title = util.pluralizedStat(this.stats.failures, 'failing');
} else {
document.title = util.pluralizedStat(this.stats.passes, 'passing');
}
}
/** Updates the document's favicon w/ a summary of current stats. */
updateFavicon() {
const canvas = document.createElement('canvas');
canvas.height = canvas.width = 32;
const context = canvas.getContext('2d');
const passing = this.stats.passes;
const pending = this.stats.pending;
const failing = this.stats.failures;
const total = Math.max(this.runner.total, passing + pending + failing);
drawFaviconArc(context, total, 0, passing, '#0e9c57');
drawFaviconArc(context, total, passing, pending, '#f3b300');
drawFaviconArc(context, total, pending + passing, failing, '#ff5621');
this.setFavicon(canvas.toDataURL());
}
/** Sets the current favicon by URL. */
setFavicon(url: string) {
const current = document.head.querySelector('link[rel="icon"]');
if (current) {
document.head.removeChild(current);
}
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = url;
link.setAttribute('sizes', '32x32');
document.head.appendChild(link);
}
}
/**
* Draws an arc for the favicon status, relative to the total number of tests.
*/
function drawFaviconArc(
context: CanvasRenderingContext2D, total: number, start: number,
length: number, color: string) {
const arcStart = ARC_OFFSET + Math.PI * 2 * (start / total);
const arcEnd = ARC_OFFSET + Math.PI * 2 * ((start + length) / total);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = ARC_WIDTH;
context.arc(16, 16, 16 - ARC_WIDTH / 2, arcStart, arcEnd);
context.stroke();
}
export default interface Title extends Mocha.reporters.Base {}
================================================
FILE: browser/reporters.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import CLISocket from './clisocket.js';
import ConsoleReporter from './reporters/console.js';
import HTMLReporter from './reporters/html.js';
import MultiReporter, {ReporterFactory} from './reporters/multi.js';
import TitleReporter from './reporters/title.js';
import * as suites from './suites.js';
export let htmlSuites: Array = [];
export let jsSuites: Array = [];
/**
* @param {CLISocket} socket The CLI socket, if present.
* @param {MultiReporter} parent The parent reporter, if present.
* @return {!Array. = [TitleReporter, ConsoleReporter];
if (socket) {
reporters.push(function(runner: MultiReporter) {
socket.observe(runner);
} as any);
}
if (suites.htmlSuites.length > 0 || suites.jsSuites.length > 0) {
reporters.push(HTMLReporter as any);
}
return reporters;
}
export type MochaStatic = typeof Mocha;
/**
* Yeah, hideous, but this allows us to be loaded before Mocha, which is handy.
*/
export function injectMocha(Mocha: MochaStatic) {
_injectPrototype(ConsoleReporter, Mocha.reporters.Base.prototype);
_injectPrototype(HTMLReporter, Mocha.reporters.HTML.prototype);
// Mocha doesn't expose its `EventEmitter` shim directly, so:
_injectPrototype(
MultiReporter, Object.getPrototypeOf(Mocha.Runner.prototype));
}
function _injectPrototype(klass: any, prototype: any) {
const newPrototype = Object.create(prototype);
// Only support
Object.keys(klass.prototype).forEach(function(key) {
newPrototype[key] = klass.prototype[key];
});
klass.prototype = newPrototype;
}
================================================
FILE: browser/suites.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import ChildRunner from './childrunner.js';
import * as config from './config.js';
import MultiReporter from './reporters/multi.js';
import * as util from './util.js';
export let htmlSuites: string[] = [];
export let jsSuites: string[] = [];
// We process grep ourselves to avoid loading suites that will be filtered.
let GREP = util.getParam('grep');
// work around mocha bug (https://github.com/mochajs/mocha/issues/2070)
if (GREP) {
GREP = GREP.replace(/\\\./g, '.');
}
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param files The files to load.
*/
export function loadSuites(files: string[]) {
files.forEach(function(file) {
if (/\.js(\?.*)?$/.test(file)) {
jsSuites.push(file);
} else if (/\.html(\?.*)?$/.test(file)) {
htmlSuites.push(file);
} else {
throw new Error('Unknown resource type: ' + file);
}
});
}
/**
* @return The child suites that should be loaded, ignoring
* those that would not match `GREP`.
*/
export function activeChildSuites(): string[] {
let subsuites = htmlSuites;
if (GREP) {
const cleanSubsuites = [];
for (let i = 0, subsuite; subsuite = subsuites[i]; i++) {
if (GREP.indexOf(util.cleanLocation(subsuite)) !== -1) {
cleanSubsuites.push(subsuite);
}
}
subsuites = cleanSubsuites;
}
return subsuites;
}
/**
* Loads all `.js` sources requested by the current suite.
*/
export function loadJsSuites(
_reporter: MultiReporter, done: (error: Error) => void) {
util.debug('loadJsSuites', jsSuites);
const loaders = jsSuites.map(function(file) {
// We only support `.js` dependencies for now.
return util.loadScript.bind(util, file);
});
util.parallel(loaders, done);
}
export function runSuites(
reporter: MultiReporter, childSuites: string[],
done: (error?: any) => void) {
util.debug('runSuites');
const suiteRunners: Array<(next: () => void) => void> = [
// Run the local tests (if any) first, not stopping on error;
_runMocha.bind(null, reporter),
];
// As well as any sub suites. Again, don't stop on error.
childSuites.forEach(function(file) {
suiteRunners.push(function(next) {
const childRunner = new ChildRunner(file, window);
reporter.emit('childRunner start', childRunner);
childRunner.run(function(error) {
reporter.emit('childRunner end', childRunner);
if (error)
reporter.emitOutOfBandTest(file, error);
next();
});
});
});
util.parallel(
suiteRunners, config.get('numConcurrentSuites'), function(error) {
reporter.done();
done(error);
});
}
/**
* Kicks off a mocha run, waiting for frameworks to load if necessary.
*
* @param {!MultiReporter} reporter Where to send Mocha's events.
* @param {function} done A callback fired, _no error is passed_.
*/
function _runMocha(reporter: MultiReporter, done: () => void, waited: boolean) {
if (config.get('waitForFrameworks') && !waited) {
const waitFor =
(config.get('waitFor') || util.whenFrameworksReady).bind(window);
waitFor(_runMocha.bind(null, reporter, done, true));
return;
}
util.debug('_runMocha');
const mocha = window.mocha;
const Mocha = window.Mocha;
mocha.reporter(reporter.childReporter(window.location));
mocha.suite.title = reporter.suiteTitle(window.location);
mocha.grep(GREP);
// We can't use `mocha.run` because it bashes over grep, invert, and friends.
// See https://github.com/visionmedia/mocha/blob/master/support/tail.js#L137
const runner = Mocha.prototype.run.call(mocha, function(_error: any) {
if (document.getElementById('mocha')) {
Mocha.utils.highlightTags('code');
}
done(); // We ignore the Mocha failure count.
});
// Mocha's default `onerror` handling strips the stack (to support really old
// browsers). We upgrade this to get better stacks for async errors.
//
// TODO(nevir): Can we expand support to other browsers?
if (navigator.userAgent.match(/chrome/i)) {
window.onerror = null;
window.addEventListener('error', function(event) {
if (!event.error)
return;
if (event.error.ignore)
return;
runner.uncaught(event.error);
});
}
}
================================================
FILE: browser/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"module": "es6",
"moduleResolution": "node",
"isolatedModules": false,
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noImplicitThis": false,
"strictNullChecks": false,
"removeComments": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": true,
"lib": [
"es2017",
"dom"
],
"sourceMap": true,
"pretty": true
},
"exclude": [
"node_modules"
],
"include": [
"*.ts",
"**/*.ts",
"../custom_typings/*.d.ts"
]
}
================================================
FILE: browser/util.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import * as config from './config.js';
/**
* @param {function()} callback A function to call when the active web component
* frameworks have loaded.
*/
export function whenFrameworksReady(callback: () => void) {
debug('whenFrameworksReady');
const done = function() {
debug('whenFrameworksReady done');
callback();
};
// If webcomponents script is in the document, wait for WebComponentsReady.
if (window.WebComponents && !window.WebComponents.ready) {
debug('WebComponentsReady?');
window.addEventListener('WebComponentsReady', function wcReady() {
window.removeEventListener('WebComponentsReady', wcReady);
debug('WebComponentsReady');
done();
});
} else {
done();
}
}
/**
* @return {string} ' tests' or ' test'.
*/
export function pluralizedStat(count: number, kind: string): string {
if (count === 1) {
return count + ' ' + kind + ' test';
} else {
return count + ' ' + kind + ' tests';
}
}
/**
* @param {string} path The URI of the script to load.
* @param {function} done
*/
export function loadScript(path: string, done: (error?: any) => void) {
const script = document.createElement('script');
script.src = path;
if (done) {
script.onload = done.bind(null, null);
script.onerror = done.bind(null, 'Failed to load script ' + script.src);
}
document.head.appendChild(script);
}
/**
* @param {string} path The URI of the stylesheet to load.
* @param {function} done
*/
export function loadStyle(path: string, done?: () => void) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = path;
if (done) {
link.onload = done.bind(null, null);
link.onerror = done.bind(null, 'Failed to load stylesheet ' + link.href);
}
document.head.appendChild(link);
}
/**
* @param {...*} var_args Logs values to the console when the `debug`
* configuration option is true.
*/
export function debug(...var_args: any[]) {
if (!config.get('verbose')) {
return;
}
const args = [window.location.pathname, ...var_args];
(console.debug || console.log).apply(console, args);
}
// URL Processing
/**
* @param {string} url
* @return {{base: string, params: string}}
*/
export function parseUrl(url: string) {
const parts = url.match(/^(.*?)(?:\?(.*))?$/);
return {
base: parts[1],
params: getParams(parts[2] || ''),
};
}
/**
* Expands a URL that may or may not be relative to `base`.
*
* @param {string} url
* @param {string} base
* @return {string}
*/
export function expandUrl(url: string, base: string) {
if (!base)
return url;
if (url.match(/^(\/|https?:\/\/)/))
return url;
if (base.substr(base.length - 1) !== '/') {
base = base + '/';
}
return base + url;
}
export interface Params { [param: string]: string[]; }
/**
* @param {string=} opt_query A query string to parse.
* @return {!Object>} All params on the URL's query.
*/
export function getParams(query?: string): Params {
query = typeof query === 'string' ? query : window.location.search;
if (query.substring(0, 1) === '?') {
query = query.substring(1);
}
// python's SimpleHTTPServer tacks a `/` on the end of query strings :(
if (query.slice(-1) === '/') {
query = query.substring(0, query.length - 1);
}
if (query === '')
return {};
const result: {[param: string]: string[]} = {};
query.split('&').forEach(function(part) {
const pair = part.split('=');
if (pair.length !== 2) {
console.warn('Invalid URL query part:', part);
return;
}
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
});
return result;
}
/**
* Merges params from `source` into `target` (mutating `target`).
*
* @param {!Object>} target
* @param {!Object>} source
*/
export function mergeParams(target: Params, source: Params) {
Object.keys(source).forEach(function(key) {
if (!(key in target)) {
target[key] = [];
}
target[key] = target[key].concat(source[key]);
});
}
/**
* @param {string} param The param to return a value for.
* @return {?string} The first value for `param`, if found.
*/
export function getParam(param: string): string|null {
const params = getParams();
return params[param] ? params[param][0] : null;
}
/**
* @param {!Object>} params
* @return {string} `params` encoded as a URI query.
*/
export function paramsToQuery(params: Params): string {
const pairs: string[] = [];
Object.keys(params).forEach(function(key) {
params[key].forEach(function(value) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
});
return (pairs.length > 0) ? ('?' + pairs.join('&')) : '';
}
function getPathName(location: Location|string): string {
return typeof location === 'string' ? location : location.pathname;
}
export function basePath(location: Location|string) {
return getPathName(location).match(/^.*\//)[0];
}
export function relativeLocation(location: Location|string, basePath: string) {
let path = getPathName(location);
if (path.indexOf(basePath) === 0) {
path = path.substring(basePath.length);
}
return path;
}
export function cleanLocation(location: Location|string) {
let path = getPathName(location);
if (path.slice(-11) === '/index.html') {
path = path.slice(0, path.length - 10);
}
return path;
}
export type Runner = (f: Function) => void;
/**
* Like `async.parallelLimit`, but our own so that we don't force a dependency
* on downstream code.
*
* @param runners Runners that call their given
* Node-style callback when done.
* @param {number|function(*)} limit Maximum number of concurrent runners.
* (optional).
* @param {?function(*)} done Callback that should be triggered once all runners
* have completed, or encountered an error.
*/
export function parallel(runners: Runner[], done: (error?: any) => void): void;
export function parallel(
runners: Runner[], limit: number, done: (error?: any) => void): void;
export function parallel(
runners: Runner[], maybeLimit: number|((error?: any) => void),
done?: (error?: any) => void) {
let limit: number;
if (typeof maybeLimit !== 'number') {
done = maybeLimit;
limit = 0;
} else {
limit = maybeLimit;
}
if (!runners.length) {
return done();
}
let called = false;
const total = runners.length;
let numActive = 0;
let numDone = 0;
function runnerDone(error: any) {
if (called) {
return;
}
numDone = numDone + 1;
numActive = numActive - 1;
if (error || numDone >= total) {
called = true;
done(error);
} else {
runOne();
}
}
function runOne() {
if (limit && numActive >= limit) {
return;
}
if (!runners.length) {
return;
}
numActive = numActive + 1;
runners.shift()(runnerDone);
}
runners.forEach(runOne);
}
/**
* Finds the directory that a loaded script is hosted on.
*
* @param {string} filename
* @return {string?}
*/
export function scriptPrefix(filename: string): string|null {
const scripts =
document.querySelectorAll('script[src*="' + filename + '"]') as
NodeListOf;
if (scripts.length !== 1) {
return null;
}
const script = scripts[0].src;
return script.substring(0, script.indexOf(filename));
}
================================================
FILE: browser-js-header.txt
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* THIS FILE IS AUTOMATICALLY GENERATED!
* To make changes to browser.js, please edit the source files in the repo's `browser/` directory!
*/
================================================
FILE: browser.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* THIS FILE IS AUTOMATICALLY GENERATED!
* To make changes to browser.js, please edit the source files in the repo's `browser/` directory!
*/
(function () {
'use strict';
window.__wctUseNpm = false;
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
// Make sure that we use native timers, in case they're being stubbed out.
var nativeSetInterval = window.setInterval;
var nativeSetTimeout = window.setTimeout;
var nativeRequestAnimationFrame = window.requestAnimationFrame;
/**
* Runs `stepFn`, catching any error and passing it to `callback` (Node-style).
* Otherwise, calls `callback` with no arguments on success.
*
* @param {function()} callback
* @param {function()} stepFn
*/
function safeStep(callback, stepFn) {
var err;
try {
stepFn();
}
catch (error) {
err = error;
}
callback(err);
}
/**
* Runs your test at declaration time (before Mocha has begun tests). Handy for
* when you need to test document initialization.
*
* Be aware that any errors thrown asynchronously cannot be tied to your test.
* You may want to catch them and pass them to the done event, instead. See
* `safeStep`.
*
* @param {string} name The name of the test.
* @param {function(?function())} testFn The test function. If an argument is
* accepted, the test will be treated as async, just like Mocha tests.
*/
function testImmediate(name, testFn) {
if (testFn.length > 0) {
return testImmediateAsync(name, testFn);
}
var err;
try {
testFn();
}
catch (error) {
err = error;
}
test(name, function (done) {
done(err);
});
}
/**
* An async-only variant of `testImmediate`.
*
* @param {string} name
* @param {function(?function())} testFn
*/
function testImmediateAsync(name, testFn) {
var testComplete = false;
var err;
test(name, function (done) {
var intervalId = nativeSetInterval(function () {
if (!testComplete)
return;
clearInterval(intervalId);
done(err);
}, 10);
});
try {
testFn(function (error) {
if (error)
err = error;
testComplete = true;
});
}
catch (error) {
err = error;
testComplete = true;
}
}
/**
* Triggers a flush of any pending events, observations, etc and calls you back
* after they have been processed.
*
* @param {function()} callback
*/
function flush(callback) {
// Ideally, this function would be a call to Polymer.dom.flush, but that
// doesn't support a callback yet
// (https://github.com/Polymer/polymer-dev/issues/851),
// ...and there's cross-browser flakiness to deal with.
// Make sure that we're invoking the callback with no arguments so that the
// caller can pass Mocha callbacks, etc.
var done = function done() {
callback();
};
// Because endOfMicrotask is flaky for IE, we perform microtask checkpoints
// ourselves (https://github.com/Polymer/polymer-dev/issues/114):
var isIE = navigator.appName === 'Microsoft Internet Explorer';
if (isIE && window.Platform && window.Platform.performMicrotaskCheckpoint) {
var reallyDone_1 = done;
done = function doneIE() {
Platform.performMicrotaskCheckpoint();
nativeSetTimeout(reallyDone_1, 0);
};
}
// Everyone else gets a regular flush.
var scope;
if (window.Polymer && window.Polymer.dom && window.Polymer.dom.flush) {
scope = window.Polymer.dom;
}
else if (window.Polymer && window.Polymer.flush) {
scope = window.Polymer;
}
else if (window.WebComponents && window.WebComponents.flush) {
scope = window.WebComponents;
}
if (scope) {
scope.flush();
}
// Ensure that we are creating a new _task_ to allow all active microtasks to
// finish (the code you're testing may be using endOfMicrotask, too).
nativeSetTimeout(done, 0);
}
/**
* Advances a single animation frame.
*
* Calls `flush`, `requestAnimationFrame`, `flush`, and `callback` sequentially
* @param {function()} callback
*/
function animationFrameFlush(callback) {
flush(function () {
nativeRequestAnimationFrame(function () {
flush(callback);
});
});
}
/**
* DEPRECATED: Use `flush`.
* @param {function} callback
*/
function asyncPlatformFlush(callback) {
console.warn('asyncPlatformFlush is deprecated in favor of the more terse flush()');
return window.flush(callback);
}
/**
*
*/
function waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime) {
timeoutTime = timeoutTime || Date.now() + (timeout || 1000);
intervalOrMutationEl = intervalOrMutationEl || 32;
try {
fn();
}
catch (e) {
if (Date.now() > timeoutTime) {
throw e;
}
else {
if (typeof intervalOrMutationEl !== 'number') {
intervalOrMutationEl.onMutation(intervalOrMutationEl, function () {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
});
}
else {
nativeSetTimeout(function () {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
}, intervalOrMutationEl);
}
return;
}
}
next();
}
window.safeStep = safeStep;
window.testImmediate = testImmediate;
window.testImmediateAsync = testImmediateAsync;
window.flush = flush;
window.animationFrameFlush = animationFrameFlush;
window.asyncPlatformFlush = asyncPlatformFlush;
window.waitFor = waitFor;
/**
* The global configuration state for WCT's browser client.
*/
var _config = {
environmentScripts: !!window.__wctUseNpm ?
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/index.js',
'mocha/mocha.js', 'chai/chai.js', '@polymer/sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js',
'@polymer/test-fixture/test-fixture.js'
] :
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/lodash.js',
'mocha/mocha.js', 'chai/chai.js', 'sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js'
],
environmentImports: !!window.__wctUseNpm ? [] :
['test-fixture/test-fixture.html'],
root: null,
waitForFrameworks: true,
waitFor: null,
numConcurrentSuites: 1,
trackConsoleError: true,
mochaOptions: { timeout: 10 * 1000 },
verbose: false,
};
/**
* Merges initial `options` into WCT's global configuration.
*
* @param {Object} options The options to merge. See `browser/config.js` for a
* reference.
*/
function setup(options) {
var childRunner = ChildRunner.current();
if (childRunner) {
_deepMerge(_config, childRunner.parentScope.WCT._config);
// But do not force the mocha UI
delete _config.mochaOptions.ui;
}
if (options && typeof options === 'object') {
_deepMerge(_config, options);
}
if (!_config.root) {
// Sibling dependencies.
var root = scriptPrefix('browser.js');
_config.root = basePath(root.substr(0, root.length - 1));
if (!_config.root) {
throw new Error('Unable to detect root URL for WCT sources. Please set WCT.root before including browser.js');
}
}
}
/**
* Retrieves a configuration value.
*/
function get(key) {
return _config[key];
}
// Internal
function _deepMerge(target, source) {
Object.keys(source).forEach(function (key) {
if (target[key] !== null && typeof target[key] === 'object' &&
!Array.isArray(target[key])) {
_deepMerge(target[key], source[key]);
}
else {
target[key] = source[key];
}
});
}
/**
* @param {function()} callback A function to call when the active web component
* frameworks have loaded.
*/
function whenFrameworksReady(callback) {
debug('whenFrameworksReady');
var done = function () {
debug('whenFrameworksReady done');
callback();
};
// If webcomponents script is in the document, wait for WebComponentsReady.
if (window.WebComponents && !window.WebComponents.ready) {
debug('WebComponentsReady?');
window.addEventListener('WebComponentsReady', function wcReady() {
window.removeEventListener('WebComponentsReady', wcReady);
debug('WebComponentsReady');
done();
});
}
else {
done();
}
}
/**
* @return {string} ' tests' or ' test'.
*/
function pluralizedStat(count, kind) {
if (count === 1) {
return count + ' ' + kind + ' test';
}
else {
return count + ' ' + kind + ' tests';
}
}
/**
* @param {string} path The URI of the script to load.
* @param {function} done
*/
function loadScript(path, done) {
var script = document.createElement('script');
script.src = path;
if (done) {
script.onload = done.bind(null, null);
script.onerror = done.bind(null, 'Failed to load script ' + script.src);
}
document.head.appendChild(script);
}
/**
* @param {string} path The URI of the stylesheet to load.
* @param {function} done
*/
function loadStyle(path, done) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = path;
if (done) {
link.onload = done.bind(null, null);
link.onerror = done.bind(null, 'Failed to load stylesheet ' + link.href);
}
document.head.appendChild(link);
}
/**
* @param {...*} var_args Logs values to the console when the `debug`
* configuration option is true.
*/
function debug() {
var var_args = [];
for (var _i = 0; _i < arguments.length; _i++) {
var_args[_i] = arguments[_i];
}
if (!get('verbose')) {
return;
}
var args = [window.location.pathname].concat(var_args);
(console.debug || console.log).apply(console, args);
}
// URL Processing
/**
* @param {string} url
* @return {{base: string, params: string}}
*/
function parseUrl(url) {
var parts = url.match(/^(.*?)(?:\?(.*))?$/);
return {
base: parts[1],
params: getParams(parts[2] || ''),
};
}
/**
* Expands a URL that may or may not be relative to `base`.
*
* @param {string} url
* @param {string} base
* @return {string}
*/
function expandUrl(url, base) {
if (!base)
return url;
if (url.match(/^(\/|https?:\/\/)/))
return url;
if (base.substr(base.length - 1) !== '/') {
base = base + '/';
}
return base + url;
}
/**
* @param {string=} opt_query A query string to parse.
* @return {!Object>} All params on the URL's query.
*/
function getParams(query) {
query = typeof query === 'string' ? query : window.location.search;
if (query.substring(0, 1) === '?') {
query = query.substring(1);
}
// python's SimpleHTTPServer tacks a `/` on the end of query strings :(
if (query.slice(-1) === '/') {
query = query.substring(0, query.length - 1);
}
if (query === '')
return {};
var result = {};
query.split('&').forEach(function (part) {
var pair = part.split('=');
if (pair.length !== 2) {
console.warn('Invalid URL query part:', part);
return;
}
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
});
return result;
}
/**
* Merges params from `source` into `target` (mutating `target`).
*
* @param {!Object>} target
* @param {!Object>} source
*/
function mergeParams(target, source) {
Object.keys(source).forEach(function (key) {
if (!(key in target)) {
target[key] = [];
}
target[key] = target[key].concat(source[key]);
});
}
/**
* @param {string} param The param to return a value for.
* @return {?string} The first value for `param`, if found.
*/
function getParam(param) {
var params = getParams();
return params[param] ? params[param][0] : null;
}
/**
* @param {!Object>} params
* @return {string} `params` encoded as a URI query.
*/
function paramsToQuery(params) {
var pairs = [];
Object.keys(params).forEach(function (key) {
params[key].forEach(function (value) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
});
return (pairs.length > 0) ? ('?' + pairs.join('&')) : '';
}
function getPathName(location) {
return typeof location === 'string' ? location : location.pathname;
}
function basePath(location) {
return getPathName(location).match(/^.*\//)[0];
}
function relativeLocation(location, basePath) {
var path = getPathName(location);
if (path.indexOf(basePath) === 0) {
path = path.substring(basePath.length);
}
return path;
}
function cleanLocation(location) {
var path = getPathName(location);
if (path.slice(-11) === '/index.html') {
path = path.slice(0, path.length - 10);
}
return path;
}
function parallel(runners, maybeLimit, done) {
var limit;
if (typeof maybeLimit !== 'number') {
done = maybeLimit;
limit = 0;
}
else {
limit = maybeLimit;
}
if (!runners.length) {
return done();
}
var called = false;
var total = runners.length;
var numActive = 0;
var numDone = 0;
function runnerDone(error) {
if (called) {
return;
}
numDone = numDone + 1;
numActive = numActive - 1;
if (error || numDone >= total) {
called = true;
done(error);
}
else {
runOne();
}
}
function runOne() {
if (limit && numActive >= limit) {
return;
}
if (!runners.length) {
return;
}
numActive = numActive + 1;
runners.shift()(runnerDone);
}
runners.forEach(runOne);
}
/**
* Finds the directory that a loaded script is hosted on.
*
* @param {string} filename
* @return {string?}
*/
function scriptPrefix(filename) {
var scripts = document.querySelectorAll('script[src*="' + filename + '"]');
if (scripts.length !== 1) {
return null;
}
var script = scripts[0].src;
return script.substring(0, script.indexOf(filename));
}
var util = Object.freeze({
whenFrameworksReady: whenFrameworksReady,
pluralizedStat: pluralizedStat,
loadScript: loadScript,
loadStyle: loadStyle,
debug: debug,
parseUrl: parseUrl,
expandUrl: expandUrl,
getParams: getParams,
mergeParams: mergeParams,
getParam: getParam,
paramsToQuery: paramsToQuery,
basePath: basePath,
relativeLocation: relativeLocation,
cleanLocation: cleanLocation,
parallel: parallel,
scriptPrefix: scriptPrefix
});
/**
* A Mocha suite (or suites) run within a child iframe, but reported as if they
* are part of the current context.
*/
var ChildRunner = /** @class */ (function () {
function ChildRunner(url, parentScope) {
var urlBits = parseUrl(url);
mergeParams(urlBits.params, getParams(parentScope.location.search));
delete urlBits.params.cli_browser_id;
this.url = urlBits.base + paramsToQuery(urlBits.params);
this.parentScope = parentScope;
this.state = 'initializing';
}
/**
* @return {ChildRunner} The `ChildRunner` that was registered for this
* window.
*/
ChildRunner.current = function () {
return ChildRunner.get(window);
};
/**
* @param {!Window} target A window to find the ChildRunner of.
* @param {boolean} traversal Whether this is a traversal from a child window.
* @return {ChildRunner} The `ChildRunner` that was registered for `target`.
*/
ChildRunner.get = function (target, traversal) {
var childRunner = ChildRunner._byUrl[target.location.href];
if (childRunner) {
return childRunner;
}
if (window.parent === window) { // Top window.
if (traversal) {
console.warn('Subsuite loaded but was never registered. This most likely is due to wonky history behavior. Reloading...');
window.location.reload();
}
return null;
}
// Otherwise, traverse.
return window.parent.WCT._ChildRunner.get(target, true);
};
/**
* Loads and runs the subsuite.
*
* @param {function} done Node-style callback.
*/
ChildRunner.prototype.run = function (done) {
debug('ChildRunner#run', this.url);
this.state = 'loading';
this.onRunComplete = done;
this.iframe = document.createElement('iframe');
this.iframe.src = this.url;
this.iframe.classList.add('subsuite');
var container = document.getElementById('subsuites');
if (!container) {
container = document.createElement('div');
container.id = 'subsuites';
document.body.appendChild(container);
}
container.appendChild(this.iframe);
// let the iframe expand the URL for us.
this.url = this.iframe.src;
ChildRunner._byUrl[this.url] = this;
this.timeoutId = setTimeout(this.loaded.bind(this, new Error('Timed out loading ' + this.url)), ChildRunner.loadTimeout);
this.iframe.addEventListener('error', this.loaded.bind(this, new Error('Failed to load document ' + this.url)));
this.iframe.contentWindow.addEventListener('DOMContentLoaded', this.loaded.bind(this, null));
};
/**
* Called when the sub suite's iframe has loaded (or errored during load).
*
* @param {*} error The error that occured, if any.
*/
ChildRunner.prototype.loaded = function (error) {
debug('ChildRunner#loaded', this.url, error);
if (this.iframe.contentWindow == null && error) {
this.signalRunComplete(error);
this.done();
return;
}
// Not all targets have WCT loaded (compatiblity mode)
if (this.iframe.contentWindow.WCT) {
this.share = this.iframe.contentWindow.WCT.share;
}
if (error) {
this.signalRunComplete(error);
this.done();
}
};
/**
* Called in mocha/run.js when all dependencies have loaded, and the child is
* ready to start running tests
*
* @param {*} error The error that occured, if any.
*/
ChildRunner.prototype.ready = function (error) {
debug('ChildRunner#ready', this.url, error);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (error) {
this.signalRunComplete(error);
this.done();
}
};
/**
* Called when the sub suite's tests are complete, so that it can clean up.
*/
ChildRunner.prototype.done = function () {
debug('ChildRunner#done', this.url, arguments);
// make sure to clear that timeout
this.ready();
this.signalRunComplete();
if (!this.iframe)
return;
// Be safe and avoid potential browser crashes when logic attempts to
// interact with the removed iframe.
setTimeout(function () {
this.iframe.parentNode.removeChild(this.iframe);
this.iframe = null;
this.share = null;
}.bind(this), 1);
};
ChildRunner.prototype.signalRunComplete = function (error) {
if (!this.onRunComplete)
return;
this.state = 'complete';
this.onRunComplete(error);
this.onRunComplete = null;
};
// ChildRunners get a pretty generous load timeout by default.
ChildRunner.loadTimeout = 60000;
// We can't maintain properties on iframe elements in Firefox/Safari/???, so
// we track childRunners by URL.
ChildRunner._byUrl = {};
return ChildRunner;
}());
var SOCKETIO_ENDPOINT = window.location.protocol + '//' + window.location.host;
var SOCKETIO_LIBRARY = SOCKETIO_ENDPOINT + '/socket.io/socket.io.js';
/**
* A socket for communication between the CLI and browser runners.
*
* @param {string} browserId An ID generated by the CLI runner.
* @param {!io.Socket} socket The socket.io `Socket` to communicate over.
*/
var CLISocket = /** @class */ (function () {
function CLISocket(browserId, socket) {
this.browserId = browserId;
this.socket = socket;
}
/**
* @param {!Mocha.Runner} runner The Mocha `Runner` to observe, reporting
* interesting events back to the CLI runner.
*/
CLISocket.prototype.observe = function (runner) {
var _this = this;
this.emitEvent('browser-start', {
url: window.location.toString(),
});
// We only emit a subset of events that we care about, and follow a more
// general event format that is hopefully applicable to test runners beyond
// mocha.
//
// For all possible mocha events, see:
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36
runner.on('test', function (test) {
_this.emitEvent('test-start', { test: getTitles(test) });
});
runner.on('test end', function (test) {
_this.emitEvent('test-end', {
state: getState(test),
test: getTitles(test),
duration: test.duration,
error: test.err,
});
});
runner.on('fail', function (test, err) {
// fail the test run if we catch errors outside of a test function
if (test.type !== 'test') {
_this.emitEvent('browser-fail', 'Error thrown outside of test function: ' + err.stack);
}
});
runner.on('childRunner start', function (childRunner) {
_this.emitEvent('sub-suite-start', childRunner.share);
});
runner.on('childRunner end', function (childRunner) {
_this.emitEvent('sub-suite-end', childRunner.share);
});
runner.on('end', function () {
_this.emitEvent('browser-end');
});
};
/**
* @param {string} event The name of the event to fire.
* @param {*} data Additional data to pass with the event.
*/
CLISocket.prototype.emitEvent = function (event, data) {
this.socket.emit('client-event', {
browserId: this.browserId,
event: event,
data: data,
});
};
/**
* Builds a `CLISocket` if we are within a CLI-run environment; short-circuits
* otherwise.
*
* @param {function(*, CLISocket)} done Node-style callback.
*/
CLISocket.init = function (done) {
var browserId = getParam('cli_browser_id');
if (!browserId)
return done();
// Only fire up the socket for root runners.
if (ChildRunner.current())
return done();
loadScript(SOCKETIO_LIBRARY, function (error) {
if (error)
return done(error);
var socket = io(SOCKETIO_ENDPOINT);
socket.on('error', function (error) {
socket.off();
done(error);
});
socket.on('connect', function () {
socket.off();
done(null, new CLISocket(browserId, socket));
});
});
};
return CLISocket;
}());
// Misc Utility
/**
* @param {!Mocha.Runnable} runnable The test or suite to extract titles from.
* @return {!Array.} The titles of the runnable and its parents.
*/
function getTitles(runnable) {
var titles = [];
while (runnable && !runnable.root && runnable.title) {
titles.unshift(runnable.title);
runnable = runnable.parent;
}
return titles;
}
/**
* @param {!Mocha.Runnable} runnable
* @return {string}
*/
function getState(runnable) {
if (runnable.state === 'passed') {
return 'passing';
}
else if (runnable.state === 'failed') {
return 'failing';
}
else if (runnable.pending) {
return 'pending';
}
else {
return 'unknown';
}
}
// We capture console events when running tests; so make sure we have a
// reference to the original one.
var console$1 = window.console;
var FONT = ';font: normal 13px "Roboto", "Helvetica Neue", "Helvetica", sans-serif;';
var STYLES = {
plain: FONT,
suite: 'color: #5c6bc0' + FONT,
test: FONT,
passing: 'color: #259b24' + FONT,
pending: 'color: #e65100' + FONT,
failing: 'color: #c41411' + FONT,
stack: 'color: #c41411',
results: FONT + 'font-size: 16px',
};
// I don't think we can feature detect this one...
var userAgent = navigator.userAgent.toLowerCase();
var CAN_STYLE_LOG = userAgent.match('firefox') || userAgent.match('webkit');
var CAN_STYLE_GROUP = userAgent.match('webkit');
// Track the indent for faked `console.group`
var logIndent = '';
function log(text, style) {
text = text.split('\n')
.map(function (l) {
return logIndent + l;
})
.join('\n');
if (CAN_STYLE_LOG) {
console$1.log('%c' + text, STYLES[style] || STYLES.plain);
}
else {
console$1.log(text);
}
}
function logGroup(text, style) {
if (CAN_STYLE_GROUP) {
console$1.group('%c' + text, STYLES[style] || STYLES.plain);
}
else if (console$1.group) {
console$1.group(text);
}
else {
logIndent = logIndent + ' ';
log(text, style);
}
}
function logGroupEnd() {
if (console$1.groupEnd) {
console$1.groupEnd();
}
else {
logIndent = logIndent.substr(0, logIndent.length - 2);
}
}
function logException(error) {
log(error.stack || error.message || (error + ''), 'stack');
}
/**
* A Mocha reporter that logs results out to the web `console`.
*/
var Console = /** @class */ (function () {
/**
* @param runner The runner that is being reported on.
*/
function Console(runner) {
Mocha.reporters.Base.call(this, runner);
runner.on('suite', function (suite) {
if (suite.root) {
return;
}
logGroup(suite.title, 'suite');
}.bind(this));
runner.on('suite end', function (suite) {
if (suite.root) {
return;
}
logGroupEnd();
}.bind(this));
runner.on('test', function (test) {
logGroup(test.title, 'test');
}.bind(this));
runner.on('pending', function (test) {
logGroup(test.title, 'pending');
}.bind(this));
runner.on('fail', function (_test, error) {
logException(error);
}.bind(this));
runner.on('test end', function (_test) {
logGroupEnd();
}.bind(this));
runner.on('end', this.logSummary.bind(this));
}
/** Prints out a final summary of test results. */
Console.prototype.logSummary = function () {
logGroup('Test Results', 'results');
if (this.stats.failures > 0) {
log(pluralizedStat(this.stats.failures, 'failing'), 'failing');
}
if (this.stats.pending > 0) {
log(pluralizedStat(this.stats.pending, 'pending'), 'pending');
}
log(pluralizedStat(this.stats.passes, 'passing'));
if (!this.stats.failures) {
log('test suite passed', 'passing');
}
log('Evaluated ' + this.stats.tests + ' tests in ' +
this.stats.duration + 'ms.');
logGroupEnd();
};
return Console;
}());
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* WCT-specific behavior on top of Mocha's default HTML reporter.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
function HTML(runner) {
var output = document.createElement('div');
output.id = 'mocha';
document.body.appendChild(output);
runner.on('suite', function (_test) {
this.total = runner.total;
}.bind(this));
Mocha.reporters.HTML.call(this, runner);
}
// Woo! What a hack. This just saves us from adding a bunch of complexity around
// style loading.
var style = document.createElement('style');
style.textContent = "\n html, body {\n position: relative;\n height: 100%;\n width: 100%;\n min-width: 900px;\n }\n #mocha, #subsuites {\n height: 100%;\n position: absolute;\n top: 0;\n }\n #mocha {\n box-sizing: border-box;\n margin: 0 !important;\n padding: 60px 20px;\n right: 0;\n left: 500px;\n }\n #subsuites {\n -ms-flex-direction: column;\n -webkit-flex-direction: column;\n display: -ms-flexbox;\n display: -webkit-flex;\n display: flex;\n flex-direction: column;\n left: 0;\n width: 500px;\n }\n #subsuites .subsuite {\n border: 0;\n width: 100%;\n height: 100%;\n }\n #mocha .test.pass .duration {\n color: #555 !important;\n }\n";
document.head.appendChild(style);
var STACKY_CONFIG = {
indent: ' ',
locationStrip: [
/^https?:\/\/[^\/]+/,
/\?.*$/,
],
filter: function (line) {
return !!line.location.match(/\/web-component-tester\/[^\/]+(\?.*)?$/);
},
};
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36-46
var MOCHA_EVENTS = [
'start', 'end', 'suite', 'suite end', 'test', 'test end', 'hook', 'hook end',
'pass', 'fail', 'pending', 'childRunner end'
];
// Until a suite has loaded, we assume this many tests in it.
var ESTIMATED_TESTS_PER_SUITE = 3;
/**
* A Mocha-like reporter that combines the output of multiple Mocha suites.
*/
var MultiReporter = /** @class */ (function () {
/**
* @param numSuites The number of suites that will be run, in order to
* estimate the total number of tests that will be performed.
* @param reporters The set of reporters that
* should receive the unified event stream.
* @param parent The parent reporter, if present.
*/
function MultiReporter(numSuites, reporters, parent) {
var _this = this;
this.reporters = reporters.map(function (reporter) {
return new reporter(_this);
});
this.parent = parent;
this.basePath = parent && parent.basePath || basePath(window.location);
this.total = numSuites * ESTIMATED_TESTS_PER_SUITE;
// Mocha reporters assume a stream of events, so we have to be careful to
// only report on one runner at a time...
this.currentRunner = null;
// ...while we buffer events for any other active runners.
this.pendingEvents = [];
this.emit('start');
}
/**
* @param location The location this reporter represents.
* @return A reporter-like "class" for each child suite
* that should be passed to `mocha.run`.
*/
MultiReporter.prototype.childReporter = function (location) {
var name = this.suiteTitle(location);
// The reporter is used as a constructor, so we can't depend on `this` being
// properly bound.
var self = this;
return _a = /** @class */ (function () {
function ChildReporter(runner) {
runner.name = window.name;
self.bindChildRunner(runner);
}
return ChildReporter;
}()),
_a.title = window.name,
_a;
var _a;
};
/** Must be called once all runners have finished. */
MultiReporter.prototype.done = function () {
this.complete = true;
this.flushPendingEvents();
this.emit('end');
};
/**
* Emit a top level test that is not part of any suite managed by this
* reporter.
*
* Helpful for reporting on global errors, loading issues, etc.
*
* @param title The title of the test.
* @param error An error associated with this test. If falsy, test is
* considered to be passing.
* @param suiteTitle Title for the suite that's wrapping the test.
* @param estimated If this test was included in the original
* estimate of `numSuites`.
*/
MultiReporter.prototype.emitOutOfBandTest = function (title, error, suiteTitle, estimated) {
debug('MultiReporter#emitOutOfBandTest(', arguments, ')');
var root = new Mocha.Suite(suiteTitle || '');
var test = new Mocha.Test(title, function () { });
test.parent = root;
test.state = error ? 'failed' : 'passed';
test.err = error;
if (!estimated) {
this.total = this.total + ESTIMATED_TESTS_PER_SUITE;
}
var runner = { total: 1 };
this.proxyEvent('start', runner);
this.proxyEvent('suite', runner, root);
this.proxyEvent('test', runner, test);
if (error) {
this.proxyEvent('fail', runner, test, error);
}
else {
this.proxyEvent('pass', runner, test);
}
this.proxyEvent('test end', runner, test);
this.proxyEvent('suite end', runner, root);
this.proxyEvent('end', runner);
};
/**
* @param {!Location|string} location
* @return {string}
*/
MultiReporter.prototype.suiteTitle = function (location) {
var path = relativeLocation(location, this.basePath);
path = cleanLocation(path);
return path;
};
// Internal Interface
/** @param {!Mocha.runners.Base} runner The runner to listen to events for. */
MultiReporter.prototype.bindChildRunner = function (runner) {
var _this = this;
MOCHA_EVENTS.forEach(function (eventName) {
runner.on(eventName, _this.proxyEvent.bind(_this, eventName, runner));
});
};
/**
* Evaluates an event fired by `runner`, proxying it forward or buffering it.
*
* @param {string} eventName
* @param {!Mocha.runners.Base} runner The runner that emitted this event.
* @param {...*} var_args Any additional data passed as part of the event.
*/
MultiReporter.prototype.proxyEvent = function (eventName, runner) {
var _args = [];
for (var _i = 2; _i < arguments.length; _i++) {
_args[_i - 2] = arguments[_i];
}
var extraArgs = Array.prototype.slice.call(arguments, 2);
if (this.complete) {
console.warn('out of order Mocha event for ' + runner.name + ':', eventName, extraArgs);
return;
}
if (this.currentRunner && runner !== this.currentRunner) {
this.pendingEvents.push(Array.prototype.slice.call(arguments));
return;
}
debug('MultiReporter#proxyEvent(', arguments, ')');
// This appears to be a Mocha bug: Tests failed by passing an error to their
// done function don't set `err` properly.
//
// TODO(nevir): Track down.
if (eventName === 'fail' && !extraArgs[0].err) {
extraArgs[0].err = extraArgs[1];
}
if (eventName === 'start') {
this.onRunnerStart(runner);
}
else if (eventName === 'end') {
this.onRunnerEnd(runner);
}
else {
this.cleanEvent(eventName, runner, extraArgs);
this.emit.apply(this, [eventName].concat(extraArgs));
}
};
/**
* Cleans or modifies an event if needed.
*
* @param eventName
* @param runner The runner that emitted this event.
* @param extraArgs
*/
MultiReporter.prototype.cleanEvent = function (eventName, _runner, extraArgs) {
// Suite hierarchy
if (extraArgs[0]) {
extraArgs[0] = this.showRootSuite(extraArgs[0]);
}
// Normalize errors
if (eventName === 'fail') {
extraArgs[1] = Stacky.normalize(extraArgs[1], STACKY_CONFIG);
}
if (extraArgs[0] && extraArgs[0].err) {
extraArgs[0].err = Stacky.normalize(extraArgs[0].err, STACKY_CONFIG);
}
};
/**
* We like to show the root suite's title, which requires a little bit of
* trickery in the suite hierarchy.
*
* @param {!Mocha.Runnable} node
*/
MultiReporter.prototype.showRootSuite = function (node) {
var leaf = node = Object.create(node);
while (node && node.parent) {
var wrappedParent = Object.create(node.parent);
node.parent = wrappedParent;
node = wrappedParent;
}
node.root = false;
return leaf;
};
/** @param {!Mocha.runners.Base} runner */
MultiReporter.prototype.onRunnerStart = function (runner) {
debug('MultiReporter#onRunnerStart:', runner.name);
this.total = this.total - ESTIMATED_TESTS_PER_SUITE + runner.total;
this.currentRunner = runner;
};
/** @param {!Mocha.runners.Base} runner */
MultiReporter.prototype.onRunnerEnd = function (runner) {
debug('MultiReporter#onRunnerEnd:', runner.name);
this.currentRunner = null;
this.flushPendingEvents();
};
/**
* Flushes any buffered events and runs them through `proxyEvent`. This will
* loop until all buffered runners are complete, or we have run out of
* buffered events.
*/
MultiReporter.prototype.flushPendingEvents = function () {
var _this = this;
var events = this.pendingEvents;
this.pendingEvents = [];
events.forEach(function (eventArgs) {
_this.proxyEvent.apply(_this, eventArgs);
});
};
return MultiReporter;
}());
var ARC_OFFSET = 0; // start at the right.
var ARC_WIDTH = 6;
/**
* A Mocha reporter that updates the document's title and favicon with
* at-a-glance stats.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
var Title = /** @class */ (function () {
function Title(runner) {
Mocha.reporters.Base.call(this, runner);
runner.on('test end', this.report.bind(this));
}
/** Reports current stats via the page title and favicon. */
Title.prototype.report = function () {
this.updateTitle();
this.updateFavicon();
};
/** Updates the document title with a summary of current stats. */
Title.prototype.updateTitle = function () {
if (this.stats.failures > 0) {
document.title = pluralizedStat(this.stats.failures, 'failing');
}
else {
document.title = pluralizedStat(this.stats.passes, 'passing');
}
};
/** Updates the document's favicon w/ a summary of current stats. */
Title.prototype.updateFavicon = function () {
var canvas = document.createElement('canvas');
canvas.height = canvas.width = 32;
var context = canvas.getContext('2d');
var passing = this.stats.passes;
var pending = this.stats.pending;
var failing = this.stats.failures;
var total = Math.max(this.runner.total, passing + pending + failing);
drawFaviconArc(context, total, 0, passing, '#0e9c57');
drawFaviconArc(context, total, passing, pending, '#f3b300');
drawFaviconArc(context, total, pending + passing, failing, '#ff5621');
this.setFavicon(canvas.toDataURL());
};
/** Sets the current favicon by URL. */
Title.prototype.setFavicon = function (url) {
var current = document.head.querySelector('link[rel="icon"]');
if (current) {
document.head.removeChild(current);
}
var link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = url;
link.setAttribute('sizes', '32x32');
document.head.appendChild(link);
};
return Title;
}());
/**
* Draws an arc for the favicon status, relative to the total number of tests.
*/
function drawFaviconArc(context, total, start, length, color) {
var arcStart = ARC_OFFSET + Math.PI * 2 * (start / total);
var arcEnd = ARC_OFFSET + Math.PI * 2 * ((start + length) / total);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = ARC_WIDTH;
context.arc(16, 16, 16 - ARC_WIDTH / 2, arcStart, arcEnd);
context.stroke();
}
var htmlSuites$1 = [];
var jsSuites$1 = [];
// We process grep ourselves to avoid loading suites that will be filtered.
var GREP = getParam('grep');
// work around mocha bug (https://github.com/mochajs/mocha/issues/2070)
if (GREP) {
GREP = GREP.replace(/\\\./g, '.');
}
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param files The files to load.
*/
function loadSuites(files) {
files.forEach(function (file) {
if (/\.js(\?.*)?$/.test(file)) {
jsSuites$1.push(file);
}
else if (/\.html(\?.*)?$/.test(file)) {
htmlSuites$1.push(file);
}
else {
throw new Error('Unknown resource type: ' + file);
}
});
}
/**
* @return The child suites that should be loaded, ignoring
* those that would not match `GREP`.
*/
function activeChildSuites() {
var subsuites = htmlSuites$1;
if (GREP) {
var cleanSubsuites = [];
for (var i = 0, subsuite = void 0; subsuite = subsuites[i]; i++) {
if (GREP.indexOf(cleanLocation(subsuite)) !== -1) {
cleanSubsuites.push(subsuite);
}
}
subsuites = cleanSubsuites;
}
return subsuites;
}
/**
* Loads all `.js` sources requested by the current suite.
*/
function loadJsSuites(_reporter, done) {
debug('loadJsSuites', jsSuites$1);
var loaders = jsSuites$1.map(function (file) {
// We only support `.js` dependencies for now.
return loadScript.bind(util, file);
});
parallel(loaders, done);
}
function runSuites(reporter, childSuites, done) {
debug('runSuites');
var suiteRunners = [
// Run the local tests (if any) first, not stopping on error;
_runMocha.bind(null, reporter),
];
// As well as any sub suites. Again, don't stop on error.
childSuites.forEach(function (file) {
suiteRunners.push(function (next) {
var childRunner = new ChildRunner(file, window);
reporter.emit('childRunner start', childRunner);
childRunner.run(function (error) {
reporter.emit('childRunner end', childRunner);
if (error)
reporter.emitOutOfBandTest(file, error);
next();
});
});
});
parallel(suiteRunners, get('numConcurrentSuites'), function (error) {
reporter.done();
done(error);
});
}
/**
* Kicks off a mocha run, waiting for frameworks to load if necessary.
*
* @param {!MultiReporter} reporter Where to send Mocha's events.
* @param {function} done A callback fired, _no error is passed_.
*/
function _runMocha(reporter, done, waited) {
if (get('waitForFrameworks') && !waited) {
var waitFor = (get('waitFor') || whenFrameworksReady).bind(window);
waitFor(_runMocha.bind(null, reporter, done, true));
return;
}
debug('_runMocha');
var mocha = window.mocha;
var Mocha = window.Mocha;
mocha.reporter(reporter.childReporter(window.location));
mocha.suite.title = reporter.suiteTitle(window.location);
mocha.grep(GREP);
// We can't use `mocha.run` because it bashes over grep, invert, and friends.
// See https://github.com/visionmedia/mocha/blob/master/support/tail.js#L137
var runner = Mocha.prototype.run.call(mocha, function (_error) {
if (document.getElementById('mocha')) {
Mocha.utils.highlightTags('code');
}
done(); // We ignore the Mocha failure count.
});
// Mocha's default `onerror` handling strips the stack (to support really old
// browsers). We upgrade this to get better stacks for async errors.
//
// TODO(nevir): Can we expand support to other browsers?
if (navigator.userAgent.match(/chrome/i)) {
window.onerror = null;
window.addEventListener('error', function (event) {
if (!event.error)
return;
if (event.error.ignore)
return;
runner.uncaught(event.error);
});
}
}
/**
* @param {CLISocket} socket The CLI socket, if present.
* @param {MultiReporter} parent The parent reporter, if present.
* @return {!Array. 0 || jsSuites$1.length > 0) {
reporters.push(HTML);
}
return reporters;
}
/**
* Yeah, hideous, but this allows us to be loaded before Mocha, which is handy.
*/
function injectMocha(Mocha) {
_injectPrototype(Console, Mocha.reporters.Base.prototype);
_injectPrototype(HTML, Mocha.reporters.HTML.prototype);
// Mocha doesn't expose its `EventEmitter` shim directly, so:
_injectPrototype(MultiReporter, Object.getPrototypeOf(Mocha.Runner.prototype));
}
function _injectPrototype(klass, prototype) {
var newPrototype = Object.create(prototype);
// Only support
Object.keys(klass.prototype).forEach(function (key) {
newPrototype[key] = klass.prototype[key];
});
klass.prototype = newPrototype;
}
/**
* Loads all environment scripts ...synchronously ...after us.
*/
function loadSync() {
debug('Loading environment scripts:');
var a11ySuiteScriptPath = 'web-component-tester/data/a11ySuite.js';
var scripts = get('environmentScripts');
var a11ySuiteWillBeLoaded = window.__generatedByWct || scripts.indexOf(a11ySuiteScriptPath) > -1;
// We can't inject a11ySuite when running the npm version because it is a
// module-based script that needs `'); // jshint ignore:line
});
debug('Environment scripts loaded');
var imports = get('environmentImports');
imports.forEach(function (path) {
var url = expandUrl(path, get('root'));
debug('Loading environment import:', url);
// Synchronous load.
document.write(''); // jshint ignore:line
});
debug('Environment imports loaded');
}
/**
* We have some hard dependencies on things that should be loaded via
* `environmentScripts`, so we assert that they're present here; and do any
* post-facto setup.
*/
function ensureDependenciesPresent() {
_ensureMocha();
_checkChai();
}
function _ensureMocha() {
var Mocha = window.Mocha;
if (!Mocha) {
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/browser.js');
}
injectMocha(Mocha);
// Magic loading of mocha's stylesheet
var mochaPrefix = scriptPrefix('mocha.js');
// only load mocha stylesheet for the test runner output
// Not the end of the world, if it doesn't load.
if (mochaPrefix && window.top === window.self) {
loadStyle(mochaPrefix + 'mocha.css');
}
}
function _checkChai() {
if (!window.chai) {
debug('Chai not present; not registering shorthands');
return;
}
window.assert = window.chai.assert;
window.expect = window.chai.expect;
}
// We may encounter errors during initialization (for example, syntax errors in
// a test file). Hang onto those (and more) until we are ready to report them.
var globalErrors = [];
/**
* Hook the environment to pick up on global errors.
*/
function listenForErrors() {
window.addEventListener('error', function (event) {
globalErrors.push(event.error);
});
// Also, we treat `console.error` as a test failure. Unless you prefer not.
var origConsole = console;
var origError = console.error;
console.error = function wctShimmedError() {
origError.apply(origConsole, arguments);
if (get('trackConsoleError')) {
throw 'console.error: ' + Array.prototype.join.call(arguments, ' ');
}
};
}
var interfaceExtensions = [];
/**
* Registers an extension that extends the global `Mocha` implementation
* with new helper methods. These helper methods will be added to the `window`
* when tests run for both BDD and TDD interfaces.
*/
function extendInterfaces(helperName, helperFactory) {
interfaceExtensions.push(function () {
var Mocha = window.Mocha;
// For all Mocha interfaces (probably just TDD and BDD):
Object.keys(Mocha.interfaces)
.forEach(function (interfaceName) {
// This is the original callback that defines the interface (TDD or
// BDD):
var originalInterface = Mocha.interfaces[interfaceName];
// This is the name of the "teardown" or "afterEach" property for the
// current interface:
var teardownProperty = interfaceName === 'tdd' ? 'teardown' : 'afterEach';
// The original callback is monkey patched with a new one that appends
// to the global context however we want it to:
Mocha.interfaces[interfaceName] = function (suite) {
// Call back to the original callback so that we get the base
// interface:
originalInterface.apply(this, arguments);
// Register a listener so that we can further extend the base
// interface:
suite.on('pre-require', function (context, _file, _mocha) {
// Capture a bound reference to the teardown function as a
// convenience:
var teardown = context[teardownProperty].bind(context);
// Add our new helper to the testing context. The helper is
// generated by a factory method that receives the context,
// the teardown function and the interface name and returns
// the new method to be added to that context:
context[helperName] =
helperFactory(context, teardown, interfaceName);
});
};
});
});
}
/**
* Applies any registered interface extensions. The extensions will be applied
* as many times as this function is called, so don't call it more than once.
*/
function applyExtensions() {
interfaceExtensions.forEach(function (applyExtension) {
applyExtension();
});
}
extendInterfaces('fixture', function (context, teardown) {
// Return context.fixture if it is already a thing, for backwards
// compatibility with `test-fixture-mocha.js`:
return context.fixture || function fixture(fixtureId, model) {
// Automatically register a teardown callback that will restore the
// test-fixture:
teardown(function () {
document.getElementById(fixtureId).restore();
});
// Find the test-fixture with the provided ID and create it, returning
// the results:
return document.getElementById(fixtureId).create(model);
};
});
/**
* stub
*
* The stub addon allows the tester to partially replace the implementation of
* an element with some custom implementation. Usage example:
*
* beforeEach(function() {
* stub('x-foo', {
* attached: function() {
* // Custom implementation of the `attached` method of element `x-foo`..
* },
* otherMethod: function() {
* // More custom implementation..
* },
* getterSetterProperty: {
* get: function() {
* // Custom getter implementation..
* },
* set: function() {
* // Custom setter implementation..
* }
* },
* // etc..
* });
* });
*/
extendInterfaces('stub', function (_context, teardown) {
return function stub(tagName, implementation) {
// Find the prototype of the element being stubbed:
var proto = document.createElement(tagName).constructor.prototype;
// For all keys in the implementation to stub with..
var stubs = Object.keys(implementation).map(function (key) {
// Stub the method on the element prototype with Sinon:
return sinon.stub(proto, key, implementation[key]);
});
// After all tests..
teardown(function () {
stubs.forEach(function (stub) {
stub.restore();
});
});
};
});
// replacement map stores what should be
var replacements = {};
var replaceTeardownAttached = false;
/**
* replace
*
* The replace addon allows the tester to replace all usages of one element with
* another element within all Polymer elements created within the time span of
* the test. Usage example:
*
* beforeEach(function() {
* replace('x-foo').with('x-fake-foo');
* });
*
* All annotations and attributes will be set on the placement element the way
* they were set for the original element.
*/
extendInterfaces('replace', function (_context, teardown) {
return function replace(oldTagName) {
return {
with: function (tagName) {
// Standardizes our replacements map
oldTagName = oldTagName.toLowerCase();
tagName = tagName.toLowerCase();
replacements[oldTagName] = tagName;
// If the function is already a stub, restore it to original
if (document.importNode.isSinonProxy) {
return;
}
if (!window.Polymer.Element) {
window.Polymer.Element = function () { };
window.Polymer.Element.prototype._stampTemplate = function () { };
}
// Keep a reference to the original `document.importNode`
// implementation for later:
var originalImportNode = document.importNode;
// Use Sinon to stub `document.ImportNode`:
sinon.stub(document, 'importNode', function (origContent, deep) {
var templateClone = document.createElement('template');
var content = templateClone.content;
var inertDoc = content.ownerDocument;
// imports node from inertDoc which holds inert nodes.
templateClone.content.appendChild(inertDoc.importNode(origContent, true));
// optional arguments are not optional on IE.
var nodeIterator = document.createNodeIterator(content, NodeFilter.SHOW_ELEMENT, null, true);
var node;
// Traverses the tree. A recently-replaced node will be put next,
// so if a node is replaced, it will be checked if it needs to be
// replaced again.
while (node = nodeIterator.nextNode()) {
var currentTagName = node.tagName.toLowerCase();
if (replacements.hasOwnProperty(currentTagName)) {
currentTagName = replacements[currentTagName];
// find the final tag name.
while (replacements[currentTagName]) {
currentTagName = replacements[currentTagName];
}
// Create a replacement:
var replacement = document.createElement(currentTagName);
// For all attributes in the original node..
for (var index = 0; index < node.attributes.length; ++index) {
// Set that attribute on the replacement:
replacement.setAttribute(node.attributes[index].name, node.attributes[index].value);
}
// Replace the original node with the replacement node:
node.parentNode.replaceChild(replacement, node);
}
}
return originalImportNode.call(this, content, deep);
});
if (!replaceTeardownAttached) {
// After each test...
teardown(function () {
replaceTeardownAttached = true;
// Restore the stubbed version of `document.importNode`:
var documentImportNode = document.importNode;
if (documentImportNode.isSinonProxy) {
documentImportNode.restore();
}
// Empty the replacement map
replacements = {};
});
}
}
};
};
});
// Mocha global helpers, broken out by testing method.
//
// Keys are the method for a particular interface; values are their analog in
// the opposite interface.
var MOCHA_EXPORTS = {
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/tdd.js
tdd: {
'setup': '"before"',
'teardown': '"after"',
'suiteSetup': '"beforeEach"',
'suiteTeardown': '"afterEach"',
'suite': '"describe" or "context"',
'test': '"it" or "specify"',
},
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/bdd.js
bdd: {
'before': '"setup"',
'after': '"teardown"',
'beforeEach': '"suiteSetup"',
'afterEach': '"suiteTeardown"',
'describe': '"suite"',
'context': '"suite"',
'xdescribe': '"suite.skip"',
'xcontext': '"suite.skip"',
'it': '"test"',
'xit': '"test.skip"',
'specify': '"test"',
'xspecify': '"test.skip"',
},
};
/**
* Exposes all Mocha methods up front, configuring and running mocha
* automatically when you call them.
*
* The assumption is that it is a one-off (sub-)suite of tests being run.
*/
function stubInterfaces() {
var keys = Object.keys(MOCHA_EXPORTS);
keys.forEach(function (ui) {
Object.keys(MOCHA_EXPORTS[ui]).forEach(function (key) {
window[key] = function wrappedMochaFunction() {
_setupMocha(ui, key, MOCHA_EXPORTS[ui][key]);
if (!window[key] || window[key] === wrappedMochaFunction) {
throw new Error('Expected mocha.setup to define ' + key);
}
window[key].apply(window, arguments);
};
});
});
}
// Whether we've called `mocha.setup`
var _mochaIsSetup = false;
/**
* @param {string} ui Sets up mocha to run `ui`-style tests.
* @param {string} key The method called that triggered this.
* @param {string} alternate The matching method in the opposite interface.
*/
function _setupMocha(ui, key, alternate) {
var mochaOptions = get('mochaOptions');
if (mochaOptions.ui && mochaOptions.ui !== ui) {
var message = 'Mixing ' + mochaOptions.ui + ' and ' + ui +
' Mocha styles is not supported. ' +
'You called "' + key + '". Did you mean ' + alternate + '?';
throw new Error(message);
}
if (_mochaIsSetup) {
return;
}
applyExtensions();
mochaOptions.ui = ui;
mocha.setup(mochaOptions); // Note that the reporter is configured in run.js.
}
// You can configure WCT before it has loaded by assigning your custom
// configuration to the global `WCT`.
setup(window.WCT);
// Maybe some day we'll expose WCT as a module to whatever module registry you
// are using (aka the UMD approach), or as an es6 module.
var WCT = window.WCT = {
// A generic place to hang data about the current suite. This object is
// reported
// back via the `sub-suite-start` and `sub-suite-end` events.
share: {},
// Until then, we get to rely on it to expose parent runners to their
// children.
_ChildRunner: ChildRunner,
_reporter: undefined,
_config: _config,
// Public API
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param {!Array.} files The files to load.
*/
loadSuites: loadSuites,
};
// Load Process
listenForErrors();
stubInterfaces();
loadSync();
// Give any scripts on the page a chance to declare tests and muck with things.
document.addEventListener('DOMContentLoaded', function () {
debug('DOMContentLoaded');
ensureDependenciesPresent();
// We need the socket built prior to building its reporter.
CLISocket.init(function (error, socket) {
if (error)
throw error;
// Are we a child of another run?
var current = ChildRunner.current();
var parent = current && current.parentScope.WCT._reporter;
debug('parentReporter:', parent);
var childSuites = activeChildSuites();
var reportersToUse = determineReporters(socket, parent);
// +1 for any local tests.
var reporter = new MultiReporter(childSuites.length + 1, reportersToUse, parent);
WCT._reporter = reporter; // For environment/compatibility.js
// We need the reporter so that we can report errors during load.
loadJsSuites(reporter, function (error) {
// Let our parent know that we're about to start the tests.
if (current)
current.ready(error);
if (error)
throw error;
// Emit any errors we've encountered up til now
globalErrors.forEach(function onError(error) {
reporter.emitOutOfBandTest('Test Suite Initialization', error);
});
runSuites(reporter, childSuites, function (error) {
// Make sure to let our parent know that we're done.
if (current)
current.done();
if (error)
throw error;
});
});
});
});
}());
//# sourceMappingURL=browser.js.map
================================================
FILE: custom_typings/bower-config.d.ts
================================================
declare module 'bower-config' {
interface Config {
directory: string;
}
function read(cwd: string, overrides?: boolean): Config;
}
================================================
FILE: custom_typings/findup-sync.d.ts
================================================
declare module 'findup-sync' {
import * as minimatch from 'minimatch';
interface IOptions extends minimatch.IOptions {
cwd?: string;
}
function mod(pattern: string[]|string, opts?: IOptions): string;
namespace mod {}
export = mod;
}
================================================
FILE: custom_typings/promisify-node.d.ts
================================================
declare module 'promisify-node' {
interface NodeCallback {
(err: any, value: T): void;
}
function promisify(f: (cb: NodeCallback) => void): () => Promise;
function promisify(f: (a: A1, cb: NodeCallback) => void): (a: A1) =>
Promise;
function promisify(
f: (a: A1, a2: A2, cb: NodeCallback) => void): (a: A1, a2: A2) =>
Promise;
function promisify(
f: (a: A1, a2: A2, a3: A3, cb: NodeCallback) =>
void): (a: A1, a2: A2, a3: A3) => Promise;
namespace promisify {}
export = promisify;
}
================================================
FILE: custom_typings/send.d.ts
================================================
declare module 'send' {
import * as http from 'http';
import * as events from 'events';
function send(req: http.IncomingMessage, path: string, options?: send.Options): send.SendStream;
namespace send {
export interface SendStream extends events.EventEmitter {
pipe(res: http.ServerResponse): void;
}
export interface Options {
dotfiles?: 'allow' | 'deny' | 'ignore';
end?: number;
etag?: boolean;
extensions?: string[];
index?: boolean|string|string[];
lastModified?: boolean;
maxAge?: number;
root?: string;
start?: number;
}
}
export = send;
}
================================================
FILE: custom_typings/server-destroy.d.ts
================================================
declare module 'server-destroy' {
import * as http from 'http';
/**
* Monkey-patches the destroy() method onto the given server.
*
* It only accepts DestroyableServers as parameters to remind the user
* to update their type annotations elsewhere, as we can't express the
* mutation in the type system directly.
*/
function enableDestroy(server: enableDestroy.DestroyableServer): void;
namespace enableDestroy {
interface DestroyableServer extends http.Server {
destroy(): void;
}
}
export = enableDestroy;
}
================================================
FILE: custom_typings/stacky.d.ts
================================================
declare module 'stacky' {
interface ParsedStackFrame {
method: string;
location: string;
line: number;
column: number;
}
type StyleFunction = (part: string) => string;
interface Options {
maxMethodPadding?: number;
indent?: string;
methodPlaceholder?: string;
locationStrip?: (string|RegExp)[];
unimportantLocation?: (string|RegExp)[];
filter?: (line: ParsedStackFrame) => boolean;
styles?: {
method?: StyleFunction;
location?: StyleFunction;
line?: StyleFunction;
column?: StyleFunction;
unimportant?: StyleFunction;
};
}
export function clean(lines: ParsedStackFrame[], options: Options): void;
export function pretty(
errorStack: string|ParsedStackFrame[], options: Options): string;
export function normalize(error: Error, config: Options): Error;
}
================================================
FILE: custom_typings/wd.d.ts
================================================
declare module 'wd' {
interface NodeCB {
(err: any, value: T): void;
}
export interface Browser {
configureHttp(options: {
retries: number
}): void;
attach(sessionId: string, callback: NodeCB): void;
init(capabilities: Capabilities, callback: NodeCB): void;
get(url: string, callback: NodeCB): void;
quit(callback: NodeCB): void;
on(eventName: string, handler: Function): void;
}
export interface Capabilities {
/** The name of the browser being used */
browserName: 'android'|'chrome'|'firefox'|'htmlunit'|'internet explorer'|'iPhone'|'iPad'|'opera'|'safari';
/** The browser version, or the empty string if unknown. */
version: string;
/** A key specifying which platform the browser should be running on. */
platform: 'WINDOWS'|'XP'|'VISTA'|'MAC'|'LINUX'|'UNIX'|'ANDROID'|'ANY';
/** Whether the session can interact with modal popups,
* such as window.alert and window.confirm. */
handlesAlerts: boolean;
/** Whether the session supports CSS selectors when searching for elements. */
cssSelectorsEnabled: boolean;
webdriver: {
remote: {
quietExceptions: boolean;
}
};
selenium: {
server: {
url: string;
}
};
}
export function remote(
hostnameOrUrl: string, port?: number,
username?: string, password?: string): Browser;
export function remote(
options: {hostname: string, port?: number,
auth?: string, path?: string, }
): Browser;
export function remote(
options: {host: string, port?: number,
username?: string, accesskey?: string, path?: string, }
): Browser;
}
================================================
FILE: data/a11ySuite-npm-header.txt
================================================
import * as polymerDom from '../@polymer/polymer/lib/legacy/polymer.dom.js';
const Polymer = { dom: polymerDom };
export {a11ySuiteExport as a11ySuite};
// wct-browser-legacy/a11ySuite.js is a generated file. Source is in web-component-tester/data/a11ySuite.js
================================================
FILE: data/a11ySuite.js
================================================
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
var a11ySuiteExport;
(function(Mocha, chai, axs) {
Object.keys(Mocha.interfaces).forEach(function(iface) {
var orig = Mocha.interfaces[iface];
Mocha.interfaces[iface] = function(suite) {
orig.apply(this, arguments);
var Suite = Mocha.Suite;
var Test = Mocha.Test;
suite.on('pre-require', function(context, file, mocha) {
/**
* Runs the Chrome Accessibility Developer Tools Audit against a test-fixture
*
* @param {String} fixtureId ID of the fixture element in the document to use
* @param {Array?} ignoredRules Array of rules to ignore for this suite
* @param {Function?} beforeEach Function to be called before each test to ensure proper setup
*/
a11ySuiteExport = context.a11ySuite = function(fixtureId, ignoredRules, beforeEach) {
// capture a reference to the fixture element early
var fixtureElement = document.getElementById(fixtureId);
if (!fixtureElement) {
return;
}
// build an audit config to disable certain ignorable tests
var axsConfig = new axs.AuditConfiguration();
axsConfig.scope = document.body;
axsConfig.showUnsupportedRulesWarning = false;
axsConfig.auditRulesToIgnore = ignoredRules;
// build mocha suite
var a11ySuite = Suite.create(suite, 'A11y Audit - Fixture: ' + fixtureId);
// override the `eachTest` function to hackily create the tests
//
// eachTest is called right before test runs to calculate the total
// number of tests
a11ySuite.eachTest = function() {
// instantiate fixture
fixtureElement.create();
// Make sure lazy-loaded dom is ready (eg )
Polymer.dom.flush();
// If we have a beforeEach function, call it
if (beforeEach) {
beforeEach();
}
// run audit
var auditResults = axs.Audit.run(axsConfig);
// create tests for audit results
auditResults.forEach(function(result, index) {
// only show applicable tests
if (result.result !== 'NA') {
var title = result.rule.heading;
// fail test if audit result is FAIL
var error = result.result === 'FAIL' ? axs.Audit.accessibilityErrorMessage(result) : null;
var test = new Test(title, function() {
if (error) {
throw new Error(error);
}
});
test.file = file;
a11ySuite.addTest(test);
}
});
// teardown fixture
fixtureElement.restore();
suite.eachTest.apply(a11ySuite, arguments);
this.eachTest = suite.eachTest;
};
return a11ySuite;
};
});
};
});
chai.use(function(chai, util) {
var Assertion = chai.Assertion;
// assert
chai.assert.a11yLabel = function(node, exp, msg){
new Assertion(node).to.have.a11yLabel(exp, msg);
};
// expect / should
Assertion.addMethod('a11yLabel', function(str, msg) {
if (msg) {
util.flag(this, 'message', msg);
}
var node = this._obj;
// obj must be a Node
new Assertion(node).to.be.instanceOf(Node);
// vind the text alternative with the help of accessibility dev tools
var textAlternative = axs.properties.findTextAlternatives(node, {});
this.assert(
textAlternative === str,
'expected #{this} to have text alternative #{exp} but got #{act}',
'expected #{this} to not have text alternative #{act}',
str,
textAlternative,
true
);
});
});
})(window.Mocha, window.chai, window.axs);
================================================
FILE: data/index.html
================================================
<% extraScripts.forEach(function(script) { %>
<% }); %>
<% if (typeof npm === 'undefined' || !npm) { %>
<% } %>
================================================
FILE: gulpfile.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
'use strict';
const concat = require('gulp-concat');
const depcheck = require('depcheck');
const fs = require('fs');
const glob = require('glob');
const gulp = require('gulp');
const bower = require('gulp-bower');
const mocha = require('gulp-spawn-mocha');
const tslint = require('gulp-tslint');
const ts = require('gulp-typescript');
const lazypipe = require('lazypipe');
const path = require('path');
const rollup = require('rollup');
const runSequence = require('run-sequence');
const typescript = require('typescript');
const mochaConfig = { reporter: 'spec' };
if (process.env.MOCHA_TIMEOUT) {
mochaConfig.timeout = parseInt(process.env.MOCHA_TIMEOUT, 10);
}
// const commonTools = require('tools-common/gulpfile');
const commonTools = {
depcheck: commonDepCheck
};
gulp.task('lint', ['tslint', 'depcheck']);
// Meta tasks
gulp.task('default', ['test']);
function removeFile(path) {
try {
fs.unlinkSync(path);
return;
} catch (e) {
try {
fs.statSync(path);
} catch (e) {
return;
}
throw new Error('Unable to remove file: ' + path);
}
}
gulp.task('clean', (done) => {
removeFile('browser.js');
removeFile('browser.js.map');
const patterns = ['runner/*.js', 'browser/**/*.js', 'browser/**/*.js.map'];
for (const pattern of patterns) {
glob(pattern, (err, files) => {
if (err) {
return done(err);
}
try {
for (const file of files) {
removeFile(file);
}
} catch (e) {
return done(e);
}
});
}
done();
});
gulp.task('test', function (done) {
runSequence(
'build:typescript-server',
'lint',
'test:unit',
'test:integration',
done);
});
gulp.task('build-all', (done) => {
runSequence('clean', 'lint', 'build', done);
});
gulp.task('build',
['build:typescript-server', 'build:browser', 'build:wct-browser-legacy']);
const tsProject = ts.createProject('tsconfig.json', { typescript });
gulp.task('build:typescript-server', function () {
// Ignore typescript errors, because gulp-typescript, like most things
// gulp, can't be trusted.
return tsProject.src().pipe(tsProject(ts.reporter.nullReporter())).js.pipe(gulp.dest('./'));
});
const browserTsProject = ts.createProject('browser/tsconfig.json', {
typescript
});
gulp.task('build:typescript-browser', function () {
return browserTsProject.src().pipe(
browserTsProject(ts.reporter.nullReporter())).js.pipe(gulp.dest('./browser/'));
});
// Specific tasks
gulp.task('build:browser', ['build:typescript-browser'], function (done) {
rollup.rollup({
entry: 'browser/index.js',
}).then(function (bundle) {
bundle.write({
indent: false,
format: 'iife',
banner: fs.readFileSync('browser-js-header.txt', 'utf-8'),
intro: 'window.__wctUseNpm = false;',
dest: 'browser.js',
sourceMap: true,
sourceMapFile: path.resolve('browser.js.map')
}).then(function () {
done();
});
}).catch(done);
});
gulp.task('build:wct-browser-legacy:a11ySuite', function () {
return gulp.src(['data/a11ySuite-npm-header.txt', 'data/a11ySuite.js'])
.pipe(concat('a11ySuite.js'))
.pipe(gulp.dest('wct-browser-legacy/'));
});
gulp.task('build:wct-browser-legacy:browser', ['build:typescript-browser'], function (done) {
rollup.rollup({
entry: 'browser/index.js',
}).then(function (bundle) {
bundle.write({
indent: false,
format: 'iife',
banner: fs.readFileSync('browser-js-header.txt', 'utf-8'),
intro: 'window.__wctUseNpm = true;',
dest: 'wct-browser-legacy/browser.js',
sourceMap: true,
sourceMapFile: path.resolve('browser.js.map')
}).then(function () {
done();
});
}).catch(done);
});
gulp.task('build:wct-browser-legacy', [
'build:wct-browser-legacy:a11ySuite',
'build:wct-browser-legacy:browser',
]);
gulp.task('test:unit', function () {
return gulp.src('test/unit/*.js', { read: false })
.pipe(mocha(mochaConfig));
});
gulp.task('bower', function () {
return bower();
});
gulp.task('test:integration', ['bower'], function () {
return gulp.src('test/integration/*.js', { read: false })
.pipe(mocha(mochaConfig));
});
gulp.task('tslint', () =>
gulp.src([
'runner/**/*.ts', '!runner/**/*.d.ts',
'test/**/*.ts', '!test/**/*.d.ts',
'custom_typings/*.d.ts', 'browser/**/*.ts', '!browser/**/*.ts'
])
.pipe(tslint())
.pipe(tslint.report({ formatter: 'verbose' })));
// Flows
commonTools.depcheck({
stickyDeps: new Set([
// Used in browser.js
'accessibility-developer-tools',
'mocha',
'test-fixture',
'@polymer/sinonjs',
'@polymer/test-fixture',
'@webcomponents/webcomponentsjs',
'async',
'findup-sync',
// Only included to satisfy peer dependency and suppress error on install
'sinon',
// Used in the wct binary
'resolve'
])
});
function commonDepCheck(options) {
const defaultOptions = { stickyDeps: new Set() };
options = Object.assign({}, defaultOptions, options);
gulp.task('depcheck', () => {
return new Promise((resolve, reject) => {
depcheck(
__dirname, { ignoreDirs: [], ignoreMatches: ['@types/*'] }, resolve);
}).then((result) => {
const invalidFiles = Object.keys(result.invalidFiles) || [];
const invalidJsFiles = invalidFiles.filter((f) => f.endsWith('.js'));
if (invalidJsFiles.length > 0) {
console.log('Invalid files:', result.invalidFiles);
throw new Error('Invalid files');
}
const unused = new Set(result.dependencies);
for (const falseUnused of options.stickyDeps) {
unused.delete(falseUnused);
}
if (unused.size > 0) {
console.log('Unused dependencies:', unused);
throw new Error('Unused dependencies');
}
});
});
}
gulp.task('prepublish', function (done) {
// We can't run the integration tests here because on travis we may not
// be running with an x instance when we do `npm install`. We can change
// this to just `test` from `test:unit` once all supported npm versions
// no longer run `prepublish` on install.
runSequence('build-all', 'test:unit', done);
});
================================================
FILE: package.json
================================================
{
"name": "web-component-tester",
"version": "6.6.0-pre.5",
"--private-wct--": {
"client-side-version-range": "4 - 6 || ^6.6.0-pre.1",
"wct-browser-legacy-version-range": "0.0.1-pre.1 || ^1.0.0"
},
"description": "web-component-tester makes testing your web components a breeze!",
"keywords": [
"browser",
"grunt",
"gruntplugin",
"gulp",
"polymer",
"test",
"testing",
"web component",
"web"
],
"homepage": "https://github.com/Polymer/web-component-tester",
"bugs": "https://github.com/Polymer/web-component-tester/issues",
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "https://github.com/Polymer/web-component-tester.git"
},
"main": "runner.js",
"bin": {
"wct": "./bin/wct",
"wct-st": "./bin/wct-st"
},
"files": [
"bin/",
"data/",
"runner/",
"scripts/",
"tasks/",
".bowerrc",
"bower.json",
"browser.js",
"browser.js.map",
"package.json",
"LICENSE",
"README.md",
"runner.js"
],
"scripts": {
"lint": "gulp lint",
"build": "tsc && gulp build",
"test": "tsc && gulp test",
"prepublishOnly": "gulp prepublish",
"test:watch": "watch 'gulp test:unit' runner/ browser/ bin/ test/ tasks/",
"format": "find runner test | grep '\\.js$\\|\\.ts$' | xargs ./node_modules/.bin/clang-format --style=file -i"
},
"dependencies": {
"@polymer/sinonjs": "^1.14.1",
"@polymer/test-fixture": "^0.0.3",
"@webcomponents/webcomponentsjs": "^1.0.7",
"accessibility-developer-tools": "^2.12.0",
"async": "^2.4.1",
"body-parser": "^1.17.2",
"bower-config": "^1.4.0",
"chai": "^4.0.2",
"chalk": "^1.1.3",
"cleankill": "^2.0.0",
"express": "^4.15.3",
"findup-sync": "^2.0.0",
"glob": "^7.1.2",
"lodash": "^3.10.1",
"mocha": "^3.4.2",
"multer": "^1.3.0",
"nomnom": "^1.8.1",
"polyserve": "^0.27.2",
"promisify-node": "^0.4.0",
"resolve": "^1.5.0",
"semver": "^5.3.0",
"send": "^0.11.1",
"server-destroy": "^1.0.1",
"sinon": "^2.3.5",
"sinon-chai": "^2.10.0",
"socket.io": "^2.0.3",
"stacky": "^1.3.1",
"wd": "^1.2.0"
},
"optionalDependencies": {
"update-notifier": "^2.2.0",
"wct-local": "^2.1.0",
"wct-sauce": "^2.0.0"
},
"devDependencies": {
"@types/body-parser": "0.0.33",
"@types/chai": "^3.4.34",
"@types/chalk": "^0.4.31",
"@types/express": "^4.0.33",
"@types/express-serve-static-core": "^4.0.39",
"@types/glob": "^5.0.30",
"@types/grunt": "^0.4.20",
"@types/gulp": "^3.8.8",
"@types/lodash": "^4.14.38",
"@types/mime": "0.0.29",
"@types/minimatch": "^2.0.29",
"@types/mocha": "^2.2.32",
"@types/multer": "0.0.32",
"@types/node": "^8.0.0",
"@types/nomnom": "0.0.28",
"@types/rimraf": "0.0.28",
"@types/semver": "^5.3.30",
"@types/sinon": "^1.16.31",
"@types/sinon-chai": "^2.7.27",
"@types/socket.io": "^1.4.27",
"bower": "^1.7.9",
"clang-format": "^1.0.43",
"depcheck": "^0.6.3",
"grunt": "^0.4.5",
"gulp": "^3.8.8",
"gulp-bower": "0.0.13",
"gulp-concat": "^2.6.1",
"gulp-spawn-mocha": "^3.1.0",
"gulp-tslint": "^8.1.2",
"gulp-typescript": "^3.1.2",
"lazypipe": "^1.0.1",
"rimraf": "^2.5.4",
"rollup": "^0.25.1",
"run-sequence": "^1.0.1",
"tslint": "^5.7.0",
"typescript": "^2.1.4",
"watch": "^0.18.0"
},
"engines": {
"node": ">= 6.0"
}
}
================================================
FILE: runner/browserrunner.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chalk from 'chalk';
import * as cleankill from 'cleankill';
import * as _ from 'lodash';
import * as wd from 'wd';
import {Config} from './config';
export interface Stats {
status: string;
passing?: number;
pending?: number;
failing?: number;
}
export interface BrowserDef extends wd.Capabilities {
id: number;
url: string;
sessionId: string;
deviceName?: string;
variant?: string;
}
// Browser abstraction, responsible for spinning up a browser instance via wd.js
// and executing runner.html test files passed in options.files
export class BrowserRunner {
timeout: number;
browser: wd.Browser;
stats: Stats;
sessionId: string;
timeoutId: NodeJS.Timer;
emitter: NodeJS.EventEmitter;
def: BrowserDef;
options: Config;
donePromise: Promise;
/**
* The url of the initial page to load in the browser when starting tests.
*/
url: string;
private _resolve: () => void;
private _reject: (err: any) => void;
/**
* @param emitter The emitter to send updates about test progress to.
* @param def A BrowserDef describing and defining the browser to be run.
* Includes both metadata and a method for connecting/launching the
* browser.
* @param options WCT options.
* @param url The url of the generated index.html file that the browser should
* point at.
* @param waitFor Optional. If given, we won't try to start/connect to the
* browser until this promise resolves. Used for serializing access to
* Safari webdriver, which can only have one instance running at once.
*/
constructor(
emitter: NodeJS.EventEmitter, def: BrowserDef, options: Config,
url: string, waitFor?: Promise) {
this.emitter = emitter;
this.def = def;
this.options = options;
this.timeout = options.testTimeout;
this.emitter = emitter;
this.url = url;
this.stats = {status: 'initializing'};
this.donePromise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
waitFor = waitFor || Promise.resolve();
waitFor.then(() => {
this.browser = wd.remote(this.def.url);
// never retry selenium commands
this.browser.configureHttp({retries: -1});
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
if (!this.browser) {
return resolve();
}
this.donePromise.then(() => resolve(), () => resolve());
this.done('Interrupting');
});
});
this.browser.on('command', (method: any, context: any) => {
emitter.emit('log:debug', this.def, chalk.cyan(method), context);
});
this.browser.on('http', (method: any, path: any, data: any) => {
if (data) {
emitter.emit(
'log:debug', this.def, chalk.magenta(method), chalk.cyan(path),
data);
} else {
emitter.emit(
'log:debug', this.def, chalk.magenta(method), chalk.cyan(path));
}
});
this.browser.on('connection', (code: any, message: any, error: any) => {
emitter.emit(
'log:warn', this.def, 'Error code ' + code + ':', message, error);
});
this.emitter.emit('browser-init', this.def, this.stats);
// Make sure that we are passing a pristine capabilities object to
// webdriver. None of our screwy custom properties!
const webdriverCapabilities = _.clone(this.def);
delete webdriverCapabilities.id;
delete webdriverCapabilities.url;
delete webdriverCapabilities.sessionId;
// Reusing a session?
if (this.def.sessionId) {
this.browser.attach(this.def.sessionId, (error) => {
this._init(error, this.def.sessionId);
});
} else {
this.browser.init(webdriverCapabilities, this._init.bind(this));
}
});
}
_init(error: any, sessionId: string) {
if (!this.browser) {
return; // When interrupted.
}
if (error) {
// TODO(nevir): BEGIN TEMPORARY CHECK.
// https://github.com/Polymer/web-component-tester/issues/51
if (this.def.browserName === 'safari' && error.data) {
// debugger;
try {
const data = JSON.parse(error.data);
if (data.value && data.value.message &&
/Failed to connect to SafariDriver/i.test(data.value.message)) {
error = 'Until Selenium\'s SafariDriver supports ' +
'Safari 6.2+, 7.1+, & 8.0+, you must\n' +
'manually install it. Follow the steps at:\n' +
'https://github.com/SeleniumHQ/selenium/' +
'wiki/SafariDriver#getting-started';
}
} catch (error) {
// Show the original error.
}
}
// END TEMPORARY CHECK
this.done(error.data || error);
} else {
this.sessionId = sessionId;
this.startTest();
this.extendTimeout();
}
}
startTest() {
const paramDelim = (this.url.indexOf('?') === -1 ? '?' : '&');
const extra = `${paramDelim}cli_browser_id=${this.def.id}`;
this.browser.get(this.url + extra, (error) => {
if (error) {
this.done(error.data || error);
} else {
this.extendTimeout();
}
});
}
onEvent(event: string, data: any) {
this.extendTimeout();
if (event === 'browser-start') {
// Always assign, to handle re-runs (no browser-init).
this.stats = {
status: 'running',
passing: 0,
pending: 0,
failing: 0,
};
} else if (event === 'test-end') {
this.stats[data.state] = this.stats[data.state] + 1;
}
if (event === 'browser-end' || event === 'browser-fail') {
this.done(data);
} else {
this.emitter.emit(event, this.def, data, this.stats, this.browser);
}
}
done(error: any) {
// No quitting for you!
if (this.options.persistent) {
return;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
// Don't double-quit.
if (!this.browser) {
return;
}
const browser = this.browser;
this.browser = null;
this.stats.status = error ? 'error' : 'complete';
if (!error && this.stats.failing > 0) {
error = this.stats.failing + ' failed tests';
}
this.emitter.emit(
'browser-end', this.def, error, this.stats, this.sessionId, browser);
// Nothing to quit.
if (!this.sessionId) {
error ? this._reject(error) : this._resolve();
}
browser.quit((quitError) => {
if (quitError) {
this.emitter.emit(
'log:warn', this.def,
'Failed to quit:', quitError.data || quitError);
}
if (error) {
this._reject(error);
} else {
this._resolve();
}
});
}
extendTimeout() {
if (this.options.persistent) {
return;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
this.done('Timed out');
}, this.timeout);
}
quit() {
this.done('quit was called');
}
// HACK
static BrowserRunner = BrowserRunner;
}
module.exports = BrowserRunner;
================================================
FILE: runner/cli.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chalk from 'chalk';
import * as events from 'events';
import * as _ from 'lodash';
import {CliReporter} from './clireporter';
import * as config from './config';
import {Context} from './context';
import {Plugin} from './plugin';
import {test} from './test';
const PACKAGE_INFO = require('../package.json');
const noopNotifier = {
notify: () => {}
};
let updateNotifier = noopNotifier;
(function() {
try {
updateNotifier = require('update-notifier')({pkg: PACKAGE_INFO});
} catch (error) {
// S'ok if we don't have update-notifier. It's optional.
}
})();
export async function run(
_env: any, args: string[], output: NodeJS.WritableStream): Promise {
await wrapResult(output, _run(args, output));
}
async function _run(args: string[], output: NodeJS.WritableStream) {
// If the "--version" or "-V" flag is ever present, just print
// the current version. Useful for globally installed CLIs.
if (args.includes('--version') || args.includes('-V')) {
output.write(`${PACKAGE_INFO.version}\n`);
return Promise.resolve();
}
// Options parsing is a two phase affair. First, we need an initial set of
// configuration so that we know which plugins to load, etc:
let options = config.preparseArgs(args) as config.Config;
// Depends on values from the initial merge:
options = config.merge(options, {
output: output,
ttyOutput: !process.env.CI && output['isTTY'] && !options.simpleOutput,
});
const context = new Context(options);
if (options.skipUpdateCheck) {
updateNotifier = noopNotifier;
}
// `parseArgs` merges any new configuration into `context.options`.
await config.parseArgs(context, args);
await test(context);
}
// Note that we're cheating horribly here. Ideally all of this logic is within
// wct-sauce. The trouble is that we also want WCT's configuration lookup logic,
// and that's not (yet) cleanly exposed.
export async function runSauceTunnel(
_env: any, args: string[], output: NodeJS.WritableStream): Promise {
await wrapResult(output, _runSauceTunnel(args, output));
}
async function _runSauceTunnel(args: string[], output: NodeJS.WritableStream) {
const cmdOptions = config.preparseArgs(args) as config.Config;
const context = new Context(cmdOptions);
const diskOptions = context.options;
const baseOptions: config.Config =
(diskOptions.plugins && diskOptions.plugins['sauce']) ||
diskOptions.sauce || {};
const plugin = await Plugin.get('sauce');
const parser = require('nomnom');
parser.script('wct-st');
parser.options(_.omit(plugin.cliConfig, 'browsers', 'tunnelId'));
const options = _.merge(baseOptions, parser.parse(args));
const wctSauce = require('wct-sauce');
wctSauce.expandOptions(options);
const emitter = new events.EventEmitter();
new CliReporter(emitter, output, {});
const tunnelId = await new Promise((resolve, reject) => {
wctSauce.startTunnel(
options, emitter,
(error: any, tunnelId: string) =>
error ? reject(error) : resolve(tunnelId));
});
output.write('\n');
output.write(
'The tunnel will remain active while this process is running.\n');
output.write(
'To use this tunnel for other WCT runs, export the following:\n');
output.write('\n');
output.write(chalk.cyan('export SAUCE_TUNNEL_ID=' + tunnelId) + '\n');
}
async function wrapResult(
output: NodeJS.WritableStream, promise: Promise) {
let error: any;
try {
await promise;
} catch (e) {
error = e;
}
if (!process.env.CI) {
updateNotifier.notify();
}
if (error) {
output.write('\n');
output.write(chalk.red(error) + '\n');
output.write('\n');
throw error;
}
}
================================================
FILE: runner/clireporter.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chalk from 'chalk';
import * as cleankill from 'cleankill';
import * as events from 'events';
import * as _ from 'lodash';
import * as stacky from 'stacky';
import * as tty from 'tty';
import * as util from 'util';
import {BrowserDef, Stats} from './browserrunner';
import * as config from './config';
const STACKY_CONFIG = {
indent: ' ',
locationStrip: [
/^https?:\/\/[^\/]+/,
/\?[\d\.]+$/,
],
unimportantLocation: [
/^\/web-component-tester\//,
]
};
export type State = 'passing'|'pending'|'failing'|'unknown'|'error';
export type CompletedState = 'passing'|'failing'|'pending'|'unknown';
type Formatter = (value: string) => string;
const STATE_ICONS = {
passing: '✓',
pending: '✖',
failing: '✖',
unknown: '?',
};
const STATE_COLORS: {[state: string]: Formatter} = {
passing: chalk.green,
pending: chalk.yellow,
failing: chalk.red,
unknown: chalk.red,
error: chalk.red,
};
const SHORT = {
'internet explorer': 'IE',
};
const BROWSER_PAD = 24;
const STATUS_PAD = 38;
export interface TestEndData {
state: CompletedState;
/**
* The titles of the tests that ran.
*/
test: string[];
duration: number;
error: any;
}
export class CliReporter {
prettyBrowsers: {[id: number]: string} = {};
browserStats: {[id: number]: Stats} = {};
emitter: events.EventEmitter;
stream: NodeJS.WritableStream;
options: config.Config;
/**
* The number of lines written the last time writeLines was called.
*/
private linesWritten: number;
constructor(
emitter: events.EventEmitter, stream: NodeJS.WritableStream,
options: config.Config) {
this.emitter = emitter;
this.stream = stream;
this.options = options;
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
this.flush();
resolve();
});
});
emitter.on('log:error', this.log.bind(this, chalk.red));
if (!this.options.quiet) {
emitter.on('log:warn', this.log.bind(this, chalk.yellow));
emitter.on('log:info', this.log.bind(this));
if (this.options.verbose) {
emitter.on('log:debug', this.log.bind(this, chalk.dim));
}
}
emitter.on('browser-init', (browser: BrowserDef, stats: Stats) => {
this.browserStats[browser.id] = stats;
this.prettyBrowsers[browser.id] = this.prettyBrowser(browser);
this.updateStatus();
});
emitter.on(
'browser-start',
(browser: BrowserDef, data: {url: string}, stats: Stats) => {
this.browserStats[browser.id] = stats;
this.log(browser, 'Beginning tests via', chalk.magenta(data.url));
this.updateStatus();
});
emitter.on(
'test-end', (browser: BrowserDef, data: TestEndData, stats: Stats) => {
this.browserStats[browser.id] = stats;
if (data.state === 'failing') {
this.writeTestError(browser, data);
} else if (this.options.expanded || this.options.verbose) {
this.log(
browser, this.stateIcon(data.state), this.prettyTest(data));
}
this.updateStatus();
});
emitter.on(
'browser-end', (browser: BrowserDef, error: any, stats: Stats) => {
this.browserStats[browser.id] = stats;
if (error) {
this.log(chalk.red, browser, 'Tests failed:', error);
} else {
this.log(chalk.green, browser, 'Tests passed');
}
});
emitter.on('run-end', (error: any) => {
if (error) {
this.log(chalk.red, 'Test run ended in failure:', error);
} else {
this.log(chalk.green, 'Test run ended with great success');
}
if (!this.options.ttyOutput) {
this.updateStatus(true);
}
});
}
// Specialized Reporting
updateStatus(force?: boolean) {
if (!this.options.ttyOutput && !force) {
return;
}
// EXTREME TERMINOLOGY FAIL, but here's a glossary:
//
// stats: An object containing test stats (total, passing, failing, etc).
// state: The state that the run is in (running, etc).
// status: A string representation of above.
const statuses = Object.keys(this.browserStats).map((browserIdStr) => {
const browserId = parseInt(browserIdStr, 10);
const pretty = this.prettyBrowsers[browserId];
const stats = this.browserStats[browserId];
let status = '';
const counts = [stats.passing, stats.pending, stats.failing];
if (counts[0] > 0 || counts[1] > 0 || counts[2] > 0) {
if (counts[0] > 0) {
counts[0] = chalk.green(counts[0].toString());
}
if (counts[1] > 0) {
counts[1] = chalk.yellow(counts[1].toString());
}
if (counts[2] > 0) {
counts[2] = chalk.red(counts[2].toString());
}
status = counts.join('/');
}
if (stats.status === 'error') {
status = status + (status === '' ? '' : ' ') + chalk.red('error');
}
return padRight(pretty + ' (' + status + ')', STATUS_PAD);
});
this.writeWrapped(statuses, ' ');
}
writeTestError(browser: BrowserDef, data: TestEndData) {
this.log(browser, this.stateIcon(data.state), this.prettyTest(data));
const error = data.error || {};
this.write('\n');
let prettyMessage = error.message || error;
if (typeof prettyMessage !== 'string') {
prettyMessage = util.inspect(prettyMessage);
}
this.write(chalk.red(' ' + prettyMessage));
if (error.stack) {
try {
this.write(stacky.pretty(data.error.stack, STACKY_CONFIG));
} catch (err) {
// If we couldn't extract a stack (i.e. there was no stack), the message
// is enough.
}
}
this.write('\n');
}
// Object Formatting
stateIcon(state: State) {
const color = STATE_COLORS[state] || STATE_COLORS['unknown'];
return color(STATE_ICONS[state] || STATE_ICONS.unknown);
}
prettyTest(data: TestEndData) {
const color = STATE_COLORS[data.state] || STATE_COLORS['unknown'];
return color(data.test.join(' » ') || '');
}
prettyBrowser(browser: BrowserDef) {
const parts: string[] = [];
if (browser.platform && !browser.deviceName) {
parts.push(browser.platform);
}
const name = browser.deviceName || browser.browserName;
parts.push(SHORT[name] || name);
if (browser.version) {
parts.push(browser.version);
}
if (browser.variant) {
parts.push(`[${browser.variant}]`);
}
return chalk.blue(parts.join(' '));
}
// General Output Formatting
log(...values: any[]): void;
log() {
let values = Array.from(arguments);
let format: (line: string) => string;
if (_.isFunction(values[0])) {
format = values[0];
values = values.slice(1);
}
if (values[0] && values[0].browserName) {
values[0] = padRight(this.prettyBrowser(values[0]), BROWSER_PAD);
}
let line =
_.toArray(values)
.map((value) => _.isString(value) ? value : util.inspect(value))
.join(' ');
line = line.replace(/[\s\n\r]+$/, '');
if (format) {
line = format(line);
}
this.write(line);
}
writeWrapped(blocks: string[], separator: string) {
if (blocks.length === 0) {
return;
}
const lines = [''];
const width = (this.stream).columns || 0;
for (const block of blocks) {
const line = lines[lines.length - 1];
const combined = line + separator + block;
if (line === '') {
lines[lines.length - 1] = block;
} else if (chalk.stripColor(combined).length <= width) {
lines[lines.length - 1] = combined;
} else {
lines.push(block);
}
}
this.writeLines(['\n'].concat(lines));
if (this.options.ttyOutput) {
this.stream.write('\r');
this.stream.write('\u001b[' + (lines.length + 1) + 'A');
}
}
write(line: string) {
this.writeLines([line]);
this.updateStatus();
}
writeLines(lines: string[]) {
for (let line of lines) {
if (line[line.length - 1] !== '\n') {
line = line + '\n';
}
if (this.options.ttyOutput) {
line = '\u001b[J' + line;
}
this.stream.write(line);
}
this.linesWritten = lines.length;
}
flush() {
if (!this.options.ttyOutput) {
return;
}
// Add an extra line for padding.
for (let i = 0; i <= this.linesWritten; i++) {
this.stream.write('\n');
}
}
// HACK
static CliReporter = CliReporter;
}
// Yeah, yeah.
function padRight(str: string, length: number) {
let currLength = chalk.stripColor(str).length;
while (currLength < length) {
currLength = currLength + 1;
str = str + ' ';
}
return str;
}
module.exports = CliReporter;
================================================
FILE: runner/config.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as findup from 'findup-sync';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as nomnom from 'nomnom';
import * as path from 'path';
import * as resolve from 'resolve';
import {Capabilities} from 'wd';
import {BrowserDef} from './browserrunner';
import {Context} from './context';
import * as paths from './paths';
import {Plugin} from './plugin';
const HOME_DIR = path.resolve(
process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE);
const JSON_MATCHER = 'wct.conf.json';
const CONFIG_MATCHER = 'wct.conf.*';
export type Browser = string|{browserName: string, platform: string};
export interface Config {
suites?: string[];
output?: NodeJS.WritableStream;
ttyOutput?: boolean;
verbose?: boolean;
quiet?: boolean;
expanded?: boolean;
root?: string;
testTimeout?: number;
persistent?: boolean;
extraScripts?: string[];
wctPackageName?: string;
clientOptions?:
{root?: string; verbose?: boolean; environmentScripts?: string[]};
activeBrowsers?: BrowserDef[];
browserOptions?: {[name: string]: Capabilities};
plugins?: (string|boolean)[]|{[key: string]: ({disabled: boolean} | boolean)};
registerHooks?: (wct: Context) => void;
enforceJsonConf?: boolean;
webserver?: {
// The port that the main webserver should run on. A port will be
// determined at runtime if none is provided.
port: number;
// The hostname used when generating URLs for the webdriver client.
hostname: string;
_generatedIndexContent?: string;
_servers?: {variant: string, url: string}[];
};
npm?: boolean;
moduleResolution?: 'none'|'node';
packageName?: string;
skipPlugins?: string[];
sauce?: {};
remote?: {};
origSuites?: string[];
compile?: 'auto'|'always'|'never';
skipCleanup?: boolean;
simpleOutput?: boolean;
skipUpdateCheck?: boolean;
configFile?: string;
proxy?: {
// Top-level path that should be redirected to the proxy-target. E.g.
// `api/v1` when you want to redirect all requests of
// `https://localhost/api/v1/`.
path: string;
// Host URL to proxy to, for example `https://myredirect:8080/foo`.
target: string;
};
/** A deprecated option */
browsers?: Browser[]|Browser;
}
export interface NPMPackage {
/**
* Name of the node package. e.g. '@polymer/polymer'
*/
name: string;
/**
* JS entrypoints relative to packageName e.g. lodash/index.js would simply
* be ['index.js'] and myPackage/dist/addon.js and myPackage/lib/core.js would
* be ['dist/addon.js', 'lib/core.js']
*/
jsEntrypoint: string;
}
/**
* config helper: A basic function to synchronously read JSON,
* log any errors, and return null if no file or invalid JSON
* was found.
*/
function readJsonSync(filename: string, dir?: string): any|null {
const configPath = path.resolve(dir || '', filename);
let config: any;
try {
config = fs.readFileSync(configPath, 'utf-8');
} catch (e) {
return null;
}
try {
return JSON.parse(config);
} catch (e) {
console.error(`Could not parse ${configPath} as JSON`);
console.error(e);
}
return null;
}
/**
* Determines the package name by reading from the following sources:
*
* 1. `options.packageName`
* 2. bower.json or package.json, depending on options.npm
*/
export function getPackageName(options: Config): string|undefined {
if (options.packageName) {
return options.packageName;
}
const manifestName = (options.npm ? 'package.json' : 'bower.json');
const manifest = readJsonSync(manifestName, options.root);
if (manifest !== null) {
return manifest.name;
}
const basename = path.basename(options.root);
console.warn(
`no ${manifestName} found, defaulting to packageName=${basename}`);
return basename;
}
/**
* Truncates the path to the slash after the last occurrence of the given
* package name.
*
* @param directory Name of directory
* @param pathName Path to be truncated
*/
export function truncatePathToDir(directory: string, pathName: string): string|
null {
const delimitedDir = `/${directory}/`;
const lastDirOccurrence = pathName.lastIndexOf(delimitedDir);
if (lastDirOccurrence === -1) {
return null;
}
return pathName.substr(0, lastDirOccurrence + delimitedDir.length);
}
/**
* Resolves npm paths from current config root to ScriptNames.
*
* e.g. a/b.js is actually resolved in directory c's node modules, it would
* return c/node_modules/a/b.js. These dependencies must be direct dependencies
* to WCT.
*
* @param config Current config / options / scope
* @param npmPackages List of NPMScript objects to be resolved
* @param wctPackageName Name of wct package with browser.js (Defaults to
* wct-browser-legacy)
*/
export function resolveWctNpmEntrypointNames(
config: Config, npmPackages: NPMPackage[]): string[] {
// grab from CLI flag defaults to wct-browser-legacy
let wctPackageName = config.wctPackageName;
if (wctPackageName === undefined) {
wctPackageName = 'wct-browser-legacy';
}
let absoluteBrowserPath;
try {
absoluteBrowserPath = resolve.sync(wctPackageName, {basedir: config.root});
} catch {
throw new Error(
`${wctPackageName} not installed. Please change --wct-package-name` +
` flag or install the package.`);
}
// We want to find and inject dependencies WCT relies on not the local
// package or its dependencies' dependencies
const absoluteWCTRoot =
truncatePathToDir(wctPackageName, absoluteBrowserPath);
const resolvedEntrypoints: string[] = [];
for (const npmPackage of npmPackages) {
const absoluteNpmMainPath =
resolve.sync(npmPackage.name, {basedir: absoluteWCTRoot});
const absoluteBasePath =
truncatePathToDir(npmPackage.name, absoluteNpmMainPath);
// Find path relative to our testing element's node_modules
const nodeModulesDir = path.posix.join(config.root, 'node_modules');
const relativeBasePath =
path.posix.relative(nodeModulesDir, absoluteBasePath);
resolvedEntrypoints.push(
path.posix.join(relativeBasePath, npmPackage.jsEntrypoint));
}
return resolvedEntrypoints;
}
// The full set of options, as a reference.
export function defaults(): Config {
return {
// The test suites that should be run.
suites: ['test/'],
// Output stream to write log messages to.
output: process.stdout,
// Whether the output stream should be treated as a TTY (and be given more
// complex output formatting). Defaults to `output.isTTY`.
ttyOutput: undefined,
// Spew all sorts of debugging messages.
verbose: false,
// Silence output
quiet: false,
// Display test results in expanded form. Verbose implies expanded.
expanded: false,
// The on-disk path where tests & static files should be served from. Paths
// (such as `suites`) are evaluated relative to this.
//
// Defaults to the project directory.
root: undefined,
// Idle timeout for tests.
testTimeout: 90 * 1000,
// Whether the browser should be closed after the tests run.
persistent: false,
// Additional .js files to include in *generated* test indexes.
extraScripts: [],
// Configuration options passed to the browser client.
clientOptions: {
root: '/components/',
},
compile: 'auto',
// Webdriver capabilities objects for each browser that should be run.
//
// Capabilities can also contain a `url` value which is either a string URL
// for the webdriver endpoint, or {hostname:, port:, user:, pwd:}.
//
// Most of the time you will want to rely on the WCT browser plugins to fill
// this in for you (e.g. via `--local`, `--sauce`, etc).
activeBrowsers: [],
// Default capabilities to use when constructing webdriver connections (for
// each browser specified in `activeBrowsers`). A handy place to hang common
// configuration.
//
// Selenium: https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
// Sauce: https://docs.saucelabs.com/reference/test-configuration/
browserOptions: {},
// The plugins that should be loaded, and their configuration.
//
// When an array, the named plugins will be loaded with their default
// configuration. When an object, each key maps to a plugin, and values are
// configuration values to be merged.
//
// plugins: {
// local: {browsers: ['firefox', 'chrome']},
// }
//
plugins: ['local', 'sauce'],
// Callback that allows you to perform advanced configuration of the WCT
// runner.
//
// The hook is given the WCT context, and can generally be written like a
// plugin. For example, to serve custom content via the internal webserver:
//
// registerHooks: function(wct) {
// wct.hook('prepare:webserver', function(app) {
// app.use(...);
// return Promise.resolve();
// });
// }
//
registerHooks: function(_wct) {},
// Whether `wct.conf.*` is allowed, or only `wct.conf.json`.
//
// Handy for CI suites that want to be locked down.
enforceJsonConf: false,
// Configuration options for the webserver that serves up your test files
// and dependencies.
//
// Typically, you will not need to modify these values.
webserver: {
// The port that the webserver should run on. A port will be determined at
// runtime if none is provided.
port: undefined,
hostname: 'localhost',
},
// The name of the NPM package that is vending wct's browser.js
wctPackageName: 'wct-browser-legacy'
};
}
/**
* nomnom configuration for command line arguments.
*
* This might feel like duplication with `defaults()`, and out of place (why not
* in `cli.js`?). But, not every option matches a configurable value, and it is
* best to keep the configuration for these together to help keep them in sync.
*/
const ARG_CONFIG = {
persistent: {
help: 'Keep browsers active (refresh to rerun tests).',
abbr: 'p',
flag: true,
},
root: {
help: 'The root directory to serve tests from.',
transform: path.resolve,
},
plugins: {
help: 'Plugins that should be loaded.',
metavar: 'NAME',
full: 'plugin',
list: true,
},
skipPlugins: {
help: 'Configured plugins that should _not_ be loaded.',
metavar: 'NAME',
full: 'skip-plugin',
list: true,
},
expanded: {
help: 'Log a status line for each test run.',
flag: true,
},
verbose: {
help: 'Turn on debugging output.',
flag: true,
},
quiet: {
help: 'Silence output.',
flag: true,
},
simpleOutput: {
help: 'Avoid fancy terminal output.',
flag: true,
},
skipUpdateCheck: {
help: 'Don\'t check for updates.',
full: 'skip-update-check',
flag: true,
},
configFile: {
help: 'Config file that needs to be used by wct. ie: wct.config-sauce.js',
full: 'configFile',
},
npm: {
help: 'Use node_modules instead of bower_components for all browser ' +
'components and packages. Uses polyserve with `--npm` flag.',
flag: true,
},
moduleResolution: {
// kebab case to match the polyserve flag
full: 'module-resolution',
help: 'Algorithm to use for resolving module specifiers in import ' +
'and export statements when rewriting them to be web-compatible. ' +
'Valid values are "none" and "node". "none" disables module ' +
'specifier rewriting. "node" uses Node.js resolution to find modules.',
// type: 'string',
choices: ['none', 'node'],
},
version: {
help: 'Display the current version of web-component-tester. Ends ' +
'execution immediately (not useable with other options.)',
abbr: 'V',
flag: true,
},
'webserver.port': {
help: 'A port to use for the test webserver.',
full: 'webserver-port',
},
'webserver.hostname': {
full: 'webserver-hostname',
hidden: true,
},
// Managed by supports-color; let's not freak out if we see it.
color: {flag: true},
compile: {
help: 'Whether to compile ES2015 down to ES5. ' +
'Options: "always", "never", "auto". Auto means that we will ' +
'selectively compile based on the requesting user agent.'
},
wctPackageName: {
full: 'wct-package-name',
help: 'NPM package name that contains web-component-tester\'s browser.js.' +
' Defaults to wct-browser-legacy. This is only used in NPM projects.'
},
// Deprecated
browsers: {
abbr: 'b',
hidden: true,
list: true,
},
remote: {
abbr: 'r',
hidden: true,
flag: true,
},
};
// Values that should be extracted when pre-parsing args.
const PREPARSE_ARGS =
['plugins', 'skipPlugins', 'simpleOutput', 'skipUpdateCheck', 'configFile'];
export interface PreparsedArgs {
plugins?: string[];
skipPlugins?: string[];
simpleOutput?: boolean;
skipUpdateCheck?: boolean;
}
/**
* Discovers appropriate config files (global, and for the project), merging
* them, and returning them.
*
* @param {string} matcher
* @param {string} root
* @return {!Object} The merged configuration.
*/
export function fromDisk(matcher: string, root?: string): Config {
const globalFile = path.join(HOME_DIR, matcher);
const projectFile = findup(matcher, {nocase: true, cwd: root});
// Load a shared config from the user's home dir, if they have one, and then
// try the project-specific path (starting at the current working directory).
const paths = _.union([globalFile, projectFile]);
const configs = _.filter(paths, fs.existsSync).map(loadProjectFile);
const options: Config = merge.apply(null, configs);
if (!options.root && projectFile && projectFile !== globalFile) {
options.root = path.dirname(projectFile);
}
return options;
}
/**
* @param {string} file
* @return {Object?}
*/
function loadProjectFile(file: string) {
// If there are _multiple_ configs at this path, prefer `json`
if (path.extname(file) === '.js' && fs.existsSync(file + 'on')) {
file = file + 'on';
}
try {
if (path.extname(file) === '.json') {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
} else {
return require(file);
}
} catch (error) {
throw new Error(`Failed to load WCT config "${file}": ${error.message}`);
}
}
/**
* Runs a simplified options parse over the command line arguments, extracting
* any values that are necessary for a full parse.
*
* See const: PREPARSE_ARGS for the values that are extracted.
*
* @param {!Array} args
* @return {!Object}
*/
export function preparseArgs(args: string[]): PreparsedArgs {
// Don't let it short circuit on help.
args = _.difference(args, ['--help', '-h']);
const parser = nomnom();
parser.options(ARG_CONFIG);
parser.printer(function() {}); // No-op output & errors.
const options = parser.parse(args);
return _expandOptionPaths(_.pick(options, PREPARSE_ARGS));
}
/**
* Runs a complete options parse over the args, respecting plugin options.
*
* @param {!Context} context The context, containing plugin state and any base
* options to merge into.
* @param {!Array} args The args to parse.
*/
export async function parseArgs(
context: Context, args: string[]): Promise {
const parser = nomnom();
parser.script('wct');
parser.options(ARG_CONFIG);
const plugins = await context.plugins();
plugins.forEach(_configurePluginOptions.bind(null, parser));
const options = _expandOptionPaths(normalize(parser.parse(args)));
if (options._ && options._.length > 0) {
options.suites = options._;
}
context.options = merge(context.options, options);
}
function _configurePluginOptions(
parser: NomnomInternal.Parser, plugin: Plugin) {
/** HACK(rictic): this looks wrong, cliConfig shouldn't have a length. */
if (!plugin.cliConfig || (plugin.cliConfig).length === 0) {
return;
}
// Group options per plugin. It'd be nice to also have a header, but that ends
// up shifting all the options over.
parser.option('plugins.' + plugin.name + '.', {string: ' '});
_.each(plugin.cliConfig, function(config, key) {
// Make sure that we don't expose the name prefixes.
if (!config['full']) {
config['full'] = key;
}
parser.option(
'plugins.' + plugin.name + '.' + key,
config as NomnomInternal.Parser.Option);
});
}
function _expandOptionPaths(options: {[key: string]: any}): any {
const result = {};
_.each(options, function(value, key) {
let target = result;
const parts = key.split('.');
for (const part of parts.slice(0, -1)) {
target = target[part] = target[part] || {};
}
target[_.last(parts)] = value;
});
return result;
}
/**
* @param {!Object...} configs Configuration objects to merge.
* @return {!Object} The merged configuration, where configuration objects
* specified later in the arguments list are given precedence.
*/
export function merge(...configs: Config[]): Config;
export function merge(): Config {
let configs: Config[] = Array.prototype.slice.call(arguments);
const result = {};
configs = configs.map(normalize);
_.merge.apply(_, [result].concat(configs));
// false plugin configs are preserved.
configs.forEach(function(config) {
_.each(config.plugins, function(value, key) {
if (typeof value === 'boolean' && value === false) {
result.plugins[key] = false;
}
});
});
return result;
}
export function normalize(config: Config): Config {
if (_.isArray(config.plugins)) {
const pluginConfigs = <{[key: string]: {disabled: boolean}}>{};
for (let i = 0, name: string; name = config.plugins[i]; i++) {
// A named plugin is explicitly enabled (e.g. --plugin foo).
pluginConfigs[name] = {disabled: false};
}
config.plugins = pluginConfigs;
}
// Always wins.
if (config.skipPlugins) {
config.plugins = config.plugins || {};
for (let i = 0, name: string; name = config.skipPlugins[i]; i++) {
config.plugins[name] = false;
}
}
return config;
}
/**
* Expands values within the configuration based on the current environment.
*
* @param {!Context} context The context for the current run.
*/
export async function expand(context: Context): Promise {
const options = context.options;
let root = context.options.root || process.cwd();
context.options.root = root = path.resolve(root);
options.origSuites = _.clone(options.suites);
expandDeprecated(context);
options.suites = await paths.expand(root, options.suites);
}
/**
* Expands any options that have been deprecated, and warns about it.
*
* @param {!Context} context The context for the current run.
*/
function expandDeprecated(context: Context) {
const options = context.options;
// We collect configuration fragments to be merged into the options object.
const fragments = [];
let browsers: Browser[] = (
_.isArray(options.browsers) ? options.browsers : [options.browsers]);
browsers = _.compact(browsers);
if (browsers.length > 0) {
context.emit(
'log:warn',
'The --browsers flag/option is deprecated. Please use ' +
'--local and --sauce instead, or configure via plugins.' +
'[local|sauce].browsers.');
const fragment: {
plugins: {[name: string]: {browsers?: Browser[]}}
} = {plugins: {sauce: {}, local: {}}};
fragments.push(fragment);
for (const browser of browsers) {
const name = (browser).browserName || browser;
const plugin = (browser).platform || name.indexOf('/') !== -1 ?
'sauce' :
'local';
fragment.plugins[plugin].browsers =
fragment.plugins[plugin].browsers || [];
fragment.plugins[plugin].browsers.push(browser);
}
delete options.browsers;
}
if (options.sauce) {
context.emit(
'log:warn',
'The sauce configuration key is deprecated. Please use ' +
'plugins.sauce instead.');
fragments.push({
plugins: {sauce: options.sauce},
});
delete options.sauce;
}
if (options.remote) {
context.emit(
'log:warn',
'The --remote flag is deprecated. Please use ' +
'--sauce default instead.');
fragments.push({
plugins: {sauce: {browsers: ['default']}},
});
delete options.remote;
}
if (fragments.length > 0) {
// We are careful to modify context.options in place.
_.merge(context.options, merge.apply(null, fragments));
}
}
/**
* @param {!Object} options The configuration to validate.
*/
export async function validate(options: Config): Promise {
if (options['webRunner']) {
throw new Error(
'webRunner is no longer a supported configuration option. ' +
'Please list the files you wish to test as arguments, ' +
'or as `suites` in a configuration object.');
}
if (options['component']) {
throw new Error(
'component is no longer a supported configuration option. ' +
'Please list the files you wish to test as arguments, ' +
'or as `suites` in a configuration object.');
}
if (options.activeBrowsers.length === 0) {
throw new Error('No browsers configured to run');
}
if (options.suites.length === 0) {
const root = options.root || process.cwd();
const globs = options.origSuites.join(', ');
throw new Error(
'No test suites were found matching your configuration\n' +
'\n' +
' WCT searched for .js and .html files matching: ' + globs + '\n' +
'\n' +
' Relative paths were resolved against: ' + root);
}
}
================================================
FILE: runner/context.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as events from 'events';
import * as express from 'express';
import * as _ from 'lodash';
import {ExpressAppMapper, ServerOptions} from 'polyserve/lib/start_server';
import * as socketIO from 'socket.io';
import * as http from 'spdy';
import * as util from 'util';
import {BrowserRunner} from './browserrunner';
import * as config from './config';
import {Plugin} from './plugin';
const JSON_MATCHER = 'wct.conf.json';
const CONFIG_MATCHER = 'wct.conf.*';
export type Handler =
((...args: any[]) => Promise)|((done: (err?: any) => void) => void)|
((arg1: any, done: (err?: any) => void) => void)|
((arg1: any, arg2: any, done: (err?: any) => void) => void)|
((arg1: any, arg2: any, arg3: any, done: (err?: any) => void) => void);
/**
* Exposes the current state of a WCT run, and emits events/hooks for anyone
* downstream to listen to.
*
* TODO(rictic): break back-compat with plugins by moving hooks entirely away
* from callbacks to promises. Easiest way to do this would be to rename
* the hook-related methods on this object, so that downstream callers would
* break in obvious ways.
*
* @param {Object} options Any initially specified options.
*/
export class Context extends events.EventEmitter {
options: config.Config;
private _hookHandlers: {[key: string]: Handler[]} = {};
_socketIOServers: SocketIO.Server[];
_httpServers: http.Server[];
_testRunners: BrowserRunner[];
constructor(options?: config.Config) {
super();
options = options || {};
let matcher: string;
if (options.configFile) {
matcher = options.configFile;
} else if (options.enforceJsonConf) {
matcher = JSON_MATCHER;
} else {
matcher = CONFIG_MATCHER;
}
/**
* The configuration for the current WCT run.
*
* We guarantee that this object is never replaced (e.g. you are free to
* hold a reference to it, and make changes to it).
*/
this.options = config.merge(
config.defaults(), config.fromDisk(matcher, options.root), options);
}
// Hooks
//
// In addition to emitting events, a context also exposes "hooks" that
// interested parties can use to inject behavior.
/**
* Registers a handler for a particular hook. Hooks are typically configured
* to run _before_ a particular behavior.
*/
hook(name: string, handler: Handler) {
this._hookHandlers[name] = this._hookHandlers[name] || [];
this._hookHandlers[name].unshift(handler);
}
/**
* Registers a handler that will run after any handlers registered so far.
*
* @param {string} name
* @param {function(!Object, function(*))} handler
*/
hookLate(name: string, handler: Handler) {
this._hookHandlers[name] = this._hookHandlers[name] || [];
this._hookHandlers[name].push(handler);
}
/**
* Once all registered handlers have run for the hook, your callback will be
* triggered. If any of the handlers indicates an error state, any subsequent
* handlers will be canceled, and the error will be passed to the callback for
* the hook.
*
* Any additional arguments passed between `name` and `done` will be passed to
* hooks (before the callback).
*
* @param {string} name
* @param {function(*)} done
* @return {!Context}
*/
emitHook(
name: 'define:webserver', app: express.Express,
// The `mapper` param is a function the client of the hook uses to
// substitute a new app for the one given. This enables, for example,
// mounting the polyserve app on a custom app to handle requests or mount
// middleware that needs to sit in front of polyserve's own handlers.
mapper: (app: Express.Application) => void, options: ServerOptions,
done?: (err?: any) => void): Promise;
emitHook(
name: 'prepare:webserver', app: express.Express,
done?: (err?: any) => void): Promise;
emitHook(name: 'configure', done?: (err?: any) => void): Promise;
emitHook(name: 'prepare', done?: (err?: any) => void): Promise;
emitHook(name: 'cleanup', done?: (err?: any) => void): Promise;
emitHook(name: string, done?: (err?: any) => void): Promise;
emitHook(name: string, ...args: any[]): Promise;
async emitHook(name: string, ...args: any[]): Promise {
this.emit('log:debug', 'hook:', name);
const hooks = (this._hookHandlers[name] || []);
type BoundHook = (cb: (err: any) => void) => (void|Promise);
let boundHooks: BoundHook[];
let done: (err?: any) => void = (_err: any) => {};
let argsEnd = args.length - 1;
if (args[argsEnd] instanceof Function) {
done = args[argsEnd];
argsEnd = argsEnd--;
}
const hookArgs = args.slice(0, argsEnd + 1);
boundHooks =
hooks.map((hook) => hook.bind.apply(hook, [null].concat(hookArgs)));
if (!boundHooks) {
boundHooks = hooks;
}
// A hook may return a promise or it may call a callback. We want to
// treat hooks as though they always return promises, so this converts.
const hookToPromise = (hook: BoundHook) => {
return new Promise((resolve, reject) => {
const maybePromise = hook((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
if (maybePromise) {
maybePromise.then(resolve, reject);
}
});
};
// We execute the handlers _sequentially_. This may be slower, but it gives
// us a lighter cognitive load and more obvious logs.
try {
for (const hook of boundHooks) {
await hookToPromise(hook);
}
} catch (err) {
// TODO(rictic): stop silently swallowing the error here and just below.
// Looks like we'll need to track down some error being thrown from
// deep inside the express router.
try {
done(err);
} catch (_) {
}
throw err;
}
try {
done();
} catch (_) {
}
}
/**
* @param {function(*, Array)} done Asynchronously loads the plugins
* requested by `options.plugins`.
*/
async plugins(): Promise {
const plugins: Plugin[] = [];
for (const name of this.enabledPlugins()) {
plugins.push(await Plugin.get(name));
}
return plugins;
}
/**
* @return {!Array} The names of enabled plugins.
*/
enabledPlugins(): string[] {
// Plugins with falsy configuration or disabled: true are _not_ loaded.
const pairs = _.reject(
(_).pairs(this.options.plugins),
(p: [string, {disabled: boolean}]) => !p[1] || p[1].disabled);
return _.map(pairs, (p) => p[0]);
}
/**
* @param {string} name
* @return {!Object}
*/
pluginOptions(name: string) {
return this.options.plugins[Plugin.shortName(name)];
}
static Context = Context;
}
module.exports = Context;
================================================
FILE: runner/gulp.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chalk from 'chalk';
import {Gulp} from 'gulp';
import {Config} from './config';
import {test} from './test';
export function init(gulp: Gulp, dependencies?: string[]): void {
if (!dependencies) {
dependencies = [];
}
// TODO(nevir): Migrate fully to wct:local/etc.
gulp.task('test', ['wct:local']);
gulp.task('test:local', ['wct:local']);
gulp.task('test:remote', ['wct:sauce']);
gulp.task('wct', ['wct:local']);
gulp.task('wct:local', dependencies, () => {
return test({plugins: {local: {}, sauce: false}}).catch(cleanError);
});
gulp.task('wct:sauce', dependencies, () => {
return test({plugins: {local: false, sauce: {}}}).catch(cleanError);
});
}
// Utility
function cleanError(error: any) {
// Pretty error for gulp.
error = new Error(chalk.red(error.message || error));
error.showStack = false;
throw error;
}
================================================
FILE: runner/httpbin.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
'use strict';
import * as bodyParser from 'body-parser';
import * as cleankill from 'cleankill';
import * as express from 'express';
import * as http from 'http';
import * as multer from 'multer';
import * as serverDestroy from 'server-destroy';
import {findPort} from './port-scanner';
import {Router} from 'express';
export {Router} from 'express';
export const httpbin: Router = express.Router();
function capWords(s: string) {
return s.split('-')
.map((word) => word[0].toUpperCase() + word.slice(1))
.join('-');
}
function formatRequest(req: express.Request) {
const headers = {};
for (const key in req.headers) {
headers[capWords(key)] = req.headers[key];
}
const formatted = {
headers: headers,
url: req.originalUrl,
data: req.body,
files: (req).files,
form: {},
json: {},
};
const contentType =
(headers['Content-Type'] || '').toLowerCase().split(';')[0];
const field = {
'application/json': 'json',
'application/x-www-form-urlencoded': 'form',
'multipart/form-data': 'form'
}[contentType];
if (field) {
formatted[field] = req.body;
}
return formatted;
}
httpbin.use(bodyParser.urlencoded({extended: false}));
httpbin.use(bodyParser.json());
const storage = multer.memoryStorage();
const upload = multer({storage: storage});
httpbin.use(upload.any());
httpbin.use(bodyParser.text());
httpbin.use(bodyParser.text({type: 'html'}));
httpbin.use(bodyParser.text({type: 'xml'}));
httpbin.get('/delay/:seconds', function(req, res) {
setTimeout(function() {
res.json(formatRequest(req));
}, (req.params.seconds || 0) * 1000);
});
httpbin.post('/post', function(req, res) {
res.json(formatRequest(req));
});
// Running this script directly with `node httpbin.js` will start up a server
// that just serves out /httpbin/...
// Useful for debugging only the httpbin functionality without the rest of
// wct.
async function main() {
const app = express();
const server = http.createServer(app) as serverDestroy.DestroyableServer;
app.use('/httpbin', httpbin);
const port = await findPort([7777, 7000, 8000, 8080, 8888]);
server.listen(port);
(server).port = port;
serverDestroy(server);
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
server.destroy();
server.on('close', resolve);
});
});
console.log('Server running at http://localhost:' + port + '/httpbin/');
}
if (require.main === module) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
================================================
FILE: runner/paths.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as fs from 'fs';
import * as glob from 'glob';
import * as _ from 'lodash';
import * as path from 'path';
import * as promisify from 'promisify-node';
/**
* Expands a series of path patterns (globs, files, directories) into a set of
* files that represent those patterns.
*
* @param baseDir The directory that patterns are relative to.
* @param patterns The patterns to expand.
* @returns The expanded paths.
*/
export async function expand(
baseDir: string, patterns: string[]): Promise {
return expandDirectories(baseDir, await unglob(baseDir, patterns));
}
/**
* Expands any glob expressions in `patterns`.
*/
async function unglob(baseDir: string, patterns: string[]): Promise {
const strs: string[][] = [];
const pGlob: any = promisify(glob);
for (const pattern of patterns) {
strs.push(await pGlob(String(pattern), {cwd: baseDir, root: baseDir}));
}
// for non-POSIX support, replacing path separators
return _.union(_.flatten(strs)).map((str) => str.replace(/\//g, path.sep));
}
/**
* Expands any directories in `patterns`, following logic similar to a web
* server.
*
* If a pattern resolves to a directory, that directory is expanded. If the
* directory contains an `index.html`, it is expanded to that. Otheriwse, the
* it expands into its children (recursively).
*/
async function expandDirectories(baseDir: string, paths: string[]) {
const listsOfPaths: string[][] = [];
for (const aPath of paths) {
listsOfPaths.push(await expandDirectory(baseDir, aPath));
}
const files = _.union(_.flatten(listsOfPaths));
return files.filter((file) => /\.(js|html)$/.test(file));
}
async function expandDirectory(
baseDir: string, aPath: string): Promise {
const stat = await promisify(fs.stat)(path.resolve(baseDir, aPath));
if (!stat.isDirectory()) {
return [aPath];
}
const files = await promisify(fs.readdir)(path.resolve(baseDir, aPath));
// We have an index; defer to that.
if (_.includes(files, 'index.html')) {
return [path.join(aPath, 'index.html')];
}
const children = await expandDirectories(path.join(baseDir, aPath), files);
return children.map((child) => path.join(aPath, child));
}
================================================
FILE: runner/plugin.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as _ from 'lodash';
import * as path from 'path';
import {Config} from './config';
import {Context} from './context';
// Plugin module names can be prefixed by the following:
const PREFIXES = [
'web-component-tester-',
'wct-',
];
export interface Metadata {}
/**
* A WCT plugin. This constructor is private. Plugins can be retrieved via
* `Plugin.get`.
*/
export class Plugin {
name: string;
cliConfig: Config;
packageName: string;
metadata: Metadata;
constructor(packageName: string, metadata: Metadata) {
this.packageName = packageName;
this.metadata = metadata;
this.name = Plugin.shortName(packageName);
this.cliConfig = this.metadata['cli-options'] || {};
}
/**
* @param {!Context} context The context that this plugin should be evaluated
* within.
*/
async execute(context: Context): Promise {
try {
const plugin = require(this.packageName);
plugin(context, context.pluginOptions(this.name), this);
} catch (error) {
throw `Failed to load plugin "${this.name}": ${error}`;
}
}
/**
* Retrieves a plugin by shorthand or module name (loading it as necessary).
*
* @param {string} name
*/
static async get(name: string): Promise {
const shortName = Plugin.shortName(name);
if (_loadedPlugins[shortName]) {
return _loadedPlugins[shortName];
}
const names = [shortName].concat(PREFIXES.map((p) => p + shortName));
const loaded = _.compact(names.map(_tryLoadPluginPackage));
if (loaded.length > 1) {
const prettyNames = loaded.map((p) => p.packageName).join(' ');
throw `Loaded conflicting WCT plugin packages: ${prettyNames}`;
}
if (loaded.length < 1) {
throw `Could not find WCT plugin named "${name}"`;
}
return loaded[0];
}
/**
* @param {string} name
* @return {string} The short form of `name`.
*/
static shortName(name: string) {
for (const prefix of PREFIXES) {
if (name.indexOf(prefix) === 0) {
return name.substr(prefix.length);
}
}
return name;
}
// HACK(rictic): Makes es6 style imports happy, so that we can do, e.g.
// import {Plugin} from './plugin';
static Plugin = Plugin;
}
// Plugin Loading
// We maintain an identity map of plugins, keyed by short name.
const _loadedPlugins: {[name: string]: Plugin} = {};
/**
* @param {string} packageName Attempts to load a package as a WCT plugin.
* @return {Plugin}
*/
function _tryLoadPluginPackage(packageName: string) {
let packageInfo: Object;
try {
packageInfo = require(path.join(packageName, 'package.json'));
} catch (error) {
if (error.code !== 'MODULE_NOT_FOUND') {
console.log(error);
}
return null;
}
// Plugins must have a (truthy) wct-plugin field.
if (!packageInfo['wct-plugin']) {
return null;
}
// Allow {"wct-plugin": true} as a shorthand.
const metadata =
_.isObject(packageInfo['wct-plugin']) ? packageInfo['wct-plugin'] : {};
return new Plugin(packageName, metadata);
}
module.exports = Plugin;
================================================
FILE: runner/port-scanner.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as net from 'net';
function checkPort(port: number): Promise {
return new Promise(function(resolve) {
const server = net.createServer();
let hasPort = false;
// if server is listening, we have the port!
server.on('listening', function(_err: any) {
hasPort = true;
server.close();
});
// callback on server close to free up the port before report it can be used
server.on('close', function(_err: any) {
resolve(hasPort);
});
// our port is busy, ignore it
server.on('error', function(_err: any) {
// docs say the server should close, this doesn't seem to be the case :(
server.close();
});
server.listen(port);
});
}
interface PromiseGetter {
(val: T): Promise;
}
async function detectSeries(
values: T[], promiseGetter: PromiseGetter): Promise {
for (const value of values) {
if (await promiseGetter(value)) {
return value;
}
}
throw new Error('Couldn\'t find a good value in detectSeries');
}
export async function findPort(ports: number[]): Promise {
try {
return await detectSeries(ports, checkPort);
} catch (error) {
throw new Error('no port found!');
}
}
================================================
FILE: runner/steps.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as http from 'http';
import * as _ from 'lodash';
import * as socketIO from 'socket.io';
import {BrowserRunner} from './browserrunner';
import * as config from './config';
import {Context} from './context';
import {Plugin} from './plugin';
import {webserver} from './webserver';
interface ClientMessage {
browserId: number;
event: string;
data: T;
}
// Steps (& Hooks)
export async function setupOverrides(context: Context): Promise {
if (context.options.registerHooks) {
context.options.registerHooks(context);
}
}
export async function loadPlugins(context: Context): Promise {
context.emit('log:debug', 'step: loadPlugins');
const plugins = await context.plugins();
// built in quasi-plugin.
webserver(context);
// Actual plugins.
await Promise.all(plugins.map((plugin) => plugin.execute(context)));
return plugins;
}
export async function configure(context: Context): Promise {
context.emit('log:debug', 'step: configure');
const options = context.options;
await config.expand(context);
// Note that we trigger the configure hook _after_ filling in the `options`
// object.
//
// If you want to modify options prior to this; do it during plugin init.
await context.emitHook('configure');
// Even if the options don't validate; useful debugging info.
const cleanOptions = _.omit(options, 'output');
context.emit('log:debug', 'configuration:', cleanOptions);
await config.validate(options);
}
/**
* The prepare step is where a lot of the runner's initialization occurs. This
* is also typically where a plugin will want to spin up any long-running
* process it requires.
*
* Note that some "plugins" are also built directly into WCT (webserver).
*/
export async function prepare(context: Context): Promise {
await context.emitHook('prepare');
}
export async function runTests(context: Context): Promise {
context.emit('log:debug', 'step: runTests');
const result = runBrowsers(context);
const runners = result.runners;
context._testRunners = runners;
context._socketIOServers = context._httpServers.map((httpServer) => {
const socketIOServer = socketIO(httpServer);
socketIOServer.on('connection', function(socket) {
context.emit('log:debug', 'Test client opened sideband socket');
socket.on('client-event', function(data: ClientMessage) {
const runner = runners[data.browserId];
if (!runner) {
throw new Error(
`Unable to find browser runner for ` +
`browser with id: ${data.browserId}`);
}
runner.onEvent(data.event, data.data);
});
});
return socketIOServer;
});
await result.completionPromise;
}
export function cancelTests(context: Context): void {
if (!context._testRunners) {
return;
}
context._testRunners.forEach(function(tr) {
tr.quit();
});
}
// Helpers
function runBrowsers(context: Context) {
const options = context.options;
const numActiveBrowsers = options.activeBrowsers.length;
if (numActiveBrowsers === 0) {
throw new Error('No browsers configured to run');
}
// TODO(nevir): validate browser definitions.
// Up the socket limit so that we can maintain more active requests.
// TODO(nevir): We should be queueing the browsers above some limit too.
http.globalAgent.maxSockets =
Math.max(http.globalAgent.maxSockets, numActiveBrowsers * 2);
context.emit('run-start', options);
const errors: any[] = [];
const promises: Promise[] = [];
const runners: BrowserRunner[] = [];
let id = 0;
for (const originalBrowserDef of options.activeBrowsers) {
let waitFor: undefined|Promise = undefined;
for (const server of options.webserver._servers) {
// Needed by both `BrowserRunner` and `CliReporter`.
const browserDef = _.clone(originalBrowserDef);
browserDef.id = id++;
browserDef.variant = server.variant;
_.defaultsDeep(browserDef, options.browserOptions);
const runner =
new BrowserRunner(context, browserDef, options, server.url, waitFor);
promises.push(runner.donePromise.then(
() => {
context.emit('log:debug', browserDef, 'BrowserRunner complete');
},
(error) => {
context.emit('log:debug', browserDef, 'BrowserRunner complete');
errors.push(error);
}));
runners.push(runner);
if (browserDef.browserName === 'safari') {
// Control to Safari must be serialized. We can't launch two instances
// simultaneously, because security lol.
// https://webkit.org/blog/6900/webdriver-support-in-safari-10/
waitFor = runner.donePromise.catch(() => {
// The next runner doesn't care about errors, just wants to know when
// it can start.
return undefined;
});
}
}
}
return {
runners,
completionPromise: (async function() {
await Promise.all(promises);
const error = errors.length > 0 ? _.union(errors).join(', ') : null;
context.emit('run-end', error);
// TODO(nevir): Better rationalize run-end and hook.
await context.emitHook('cleanup');
if (error) {
throw new Error(error);
}
}())
};
}
================================================
FILE: runner/test.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as cleankill from 'cleankill';
import {CliReporter} from './clireporter';
import {Config} from './config';
import {Context} from './context';
import * as steps from './steps';
/**
* Runs a suite of web component tests.
*
* The returned Context (a kind of EventEmitter) fires various events to allow
* you to track the progress of the tests:
*
* Lifecycle Events:
*
* `run-start`
* WCT is ready to begin spinning up browsers.
*
* `browser-init` {browser} {stats}
* WCT is ready to begin spinning up browsers.
*
* `browser-start` {browser} {metadata} {stats}
* The browser has begun running tests. May fire multiple times (i.e. when
* manually refreshing the tests).
*
* `sub-suite-start` {browser} {sharedState} {stats}
* A suite file has begun running.
*
* `test-start` {browser} {test} {stats}
* A test has begun.
*
* `test-end` {browser} {test} {stats}
* A test has ended.
*
* `sub-suite-end` {browser} {sharedState} {stats}
* A suite file has finished running all of its tests.
*
* `browser-end` {browser} {error} {stats}
* The browser has completed, and it shutting down.
*
* `run-end` {error}
* WCT has run all browsers, and is shutting down.
*
* Generic Events:
*
* * log:debug
* * log:info
* * log:warn
* * log:error
*
* @param {!Config|!Context} options The configuration or an already formed
* `Context` object.
*/
export async function test(options: Config|Context): Promise {
const context = (options instanceof Context) ? options : new Context(options);
// We assume that any options related to logging are passed in via the initial
// `options`.
if (context.options.output) {
new CliReporter(context, context.options.output, context.options);
}
try {
await steps.setupOverrides(context);
await steps.loadPlugins(context);
await steps.configure(context);
await steps.prepare(context);
await steps.runTests(context);
} finally {
if (!context.options.skipCleanup) {
await cleankill.close();
}
}
}
// HACK
test['test'] = test;
module.exports = test;
================================================
FILE: runner/webserver.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as bowerConfig from 'bower-config';
import * as cleankill from 'cleankill';
import * as express from 'express';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import {MainlineServer, PolyserveServer, RequestHandler, ServerOptions, startServers, VariantServer} from 'polyserve';
import * as semver from 'semver';
import * as send from 'send';
import * as serverDestroy from 'server-destroy';
import {getPackageName, NPMPackage, resolveWctNpmEntrypointNames} from './config';
import {Context} from './context';
// Template for generated indexes.
const INDEX_TEMPLATE = _.template(fs.readFileSync(
path.resolve(__dirname, '../data/index.html'), {encoding: 'utf-8'}));
const DEFAULT_HEADERS = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
};
// scripts to be injected into the running test
const ENVIRONMENT_SCRIPTS: NPMPackage[] = [
{name: 'stacky', jsEntrypoint: 'browser.js'},
{name: 'async', jsEntrypoint: 'lib/async.js'},
{name: 'lodash', jsEntrypoint: 'index.js'},
{name: 'mocha', jsEntrypoint: 'mocha.js'},
{name: 'chai', jsEntrypoint: 'chai.js'},
{name: '@polymer/sinonjs', jsEntrypoint: 'sinon.js'},
{name: 'sinon-chai', jsEntrypoint: 'lib/sinon-chai.js'},
{
name: 'accessibility-developer-tools',
jsEntrypoint: 'dist/js/axs_testing.js'
},
{name: '@polymer/test-fixture', jsEntrypoint: 'test-fixture.js'},
];
/**
* The webserver module is a quasi-plugin. This ensures that it is hooked in a
* sane way (for other plugins), and just follows the same flow.
*
* It provides a static HTTP server for serving the desired tests and WCT's
* `browser.js`/`environment.js`.
*/
export function webserver(wct: Context): void {
const options = wct.options;
wct.hook('configure', async function() {
// For now, you should treat all these options as an implementation detail
// of WCT. They may be opened up for public configuration, but we need to
// spend some time rationalizing interactions with external webservers.
options.webserver = _.merge(options.webserver, {});
if (options.verbose) {
options.clientOptions.verbose = true;
}
// Hacky workaround for Firefox + Windows issue where FF screws up pathing.
// Bug: https://github.com/Polymer/web-component-tester/issues/194
options.suites = options.suites.map((cv) => cv.replace(/\\/g, '/'));
// The generated index needs the correct "browser.js" script. When using
// npm, the wct-browser-legacy package may be used, so we test for that
// package and will use its "browser.js" if present.
let browserScript = 'web-component-tester/browser.js';
if (options.npm) {
try {
const wctBrowserLegacyPath =
path.join(options.root, 'node_modules', 'wct-browser-legacy');
const version =
require(path.join(wctBrowserLegacyPath, 'package.json')).version;
if (version) {
browserScript = 'wct-browser-legacy/browser.js';
}
} catch (e) {
// Safely ignore.
}
const packageName = getPackageName(options);
const isPackageScoped = packageName && packageName[0] === '@';
// concat options.clientOptions.environmentScripts with resolved
// ENVIRONMENT_SCRIPTS
options.clientOptions = options.clientOptions || {};
options.clientOptions.environmentScripts =
options.clientOptions.environmentScripts || [];
options.clientOptions.environmentScripts =
options.clientOptions.environmentScripts.concat(
resolveWctNpmEntrypointNames(options, ENVIRONMENT_SCRIPTS));
if (isPackageScoped) {
browserScript = `../${browserScript}`;
}
}
const a11ySuiteScript = 'web-component-tester/data/a11ySuite.js';
options.webserver._generatedIndexContent = INDEX_TEMPLATE(
Object.assign({browserScript, a11ySuiteScript}, options));
});
wct.hook('prepare', async function() {
const wsOptions = options.webserver;
const additionalRoutes = new Map();
const packageName = getPackageName(options);
let componentDir;
// Check for client-side compatibility.
// Non-npm case.
if (!options.npm) {
componentDir = bowerConfig.read(options.root).directory;
const pathToLocalWct =
path.join(options.root, componentDir, 'web-component-tester');
let version: string|undefined = undefined;
const mdFilenames = ['package.json', 'bower.json', '.bower.json'];
for (const mdFilename of mdFilenames) {
const pathToMetadata = path.join(pathToLocalWct, mdFilename);
try {
if (!version) {
version = require(pathToMetadata).version;
}
} catch (e) {
// Handled below, where we check if we found a version.
}
}
if (!version) {
throw new Error(`
The web-component-tester Bower package is not installed as a dependency of this project (${
packageName}).
Please run this command to install:
bower install --save-dev web-component-tester
Web Component Tester >=6.0 requires that support files needed in the browser are installed as part of the project's dependencies or dev-dependencies. This is to give projects greater control over the versions that are served, while also making Web Component Tester's behavior easier to understand.
Expected to find a ${mdFilenames.join(' or ')} at: ${pathToLocalWct}/
`);
}
const allowedRange =
require(path.join(
__dirname, '..', 'package.json'))['--private-wct--']
['client-side-version-range'] as
string;
if (!semver.satisfies(version, allowedRange)) {
throw new Error(`
The web-component-tester Bower package installed is incompatible with the
wct node package you're using.
The test runner expects a version that satisfies ${allowedRange} but the
bower package you have installed is ${version}.
`);
}
let hasWarnedBrowserJs = false;
additionalRoutes.set('/browser.js', function(request, response) {
if (!hasWarnedBrowserJs) {
console.warn(`
WARNING:
Loading WCT's browser.js from /browser.js is deprecated.
Instead load it from ../web-component-tester/browser.js
(or with the absolute url /components/web-component-tester/browser.js)
`);
hasWarnedBrowserJs = true;
}
const browserJsPath = path.join(pathToLocalWct, 'browser.js');
send(request, browserJsPath).pipe(response);
});
}
const pathToGeneratedIndex =
`/components/${packageName}/generated-index.html`;
additionalRoutes.set(pathToGeneratedIndex, (_request, response) => {
response.set(DEFAULT_HEADERS);
response.send(options.webserver._generatedIndexContent);
});
const appMapper = async (app: express.Express, options: ServerOptions) => {
// Using the define:webserver hook to provide a mapper function that
// allows user to substitute their own app for the generated polyserve
// app.
await wct.emitHook(
'define:webserver', app, (substitution: express.Express) => {
app = substitution;
}, options);
return app;
};
// Serve up project & dependencies via polyserve
const polyserveResult = await startServers(
{
root: options.root,
componentDir,
compile: options.compile,
hostname: options.webserver.hostname,
headers: DEFAULT_HEADERS,
packageName,
additionalRoutes,
npm: !!options.npm,
moduleResolution: options.moduleResolution,
proxy: options.proxy,
},
appMapper);
let servers: Array;
const onDestroyHandlers: Array<() => Promise> = [];
const registerServerTeardown = (serverInfo: PolyserveServer) => {
const destroyableServer = serverInfo.server as any;
serverDestroy(destroyableServer);
onDestroyHandlers.push(() => {
destroyableServer.destroy();
return new Promise(
(resolve) => serverInfo.server.on('close', () => resolve()));
});
};
if (polyserveResult.kind === 'mainline') {
servers = [polyserveResult];
registerServerTeardown(polyserveResult);
wsOptions.port = polyserveResult.server.address().port;
} else if (polyserveResult.kind === 'MultipleServers') {
servers = [polyserveResult.mainline];
servers = servers.concat(polyserveResult.variants);
wsOptions.port = polyserveResult.mainline.server.address().port;
for (const server of polyserveResult.servers) {
registerServerTeardown(server);
}
} else {
const never: never = polyserveResult;
throw new Error(
`Internal error: Got unknown response from polyserve.startServers:` +
`${never}`);
}
wct._httpServers = servers.map((s) => s.server);
// At this point, we allow other plugins to hook and configure the
// webservers as they please.
for (const server of servers) {
await wct.emitHook('prepare:webserver', server.app);
}
options.webserver._servers = servers.map((s) => {
const port = s.server.address().port;
const hostname = s.options.hostname;
const url = `http://${hostname}:${port}${pathToGeneratedIndex}`;
return {url, variant: s.kind === 'mainline' ? '' : s.variantName};
});
// TODO(rictic): re-enable this stuff. need to either move this code into
// polyserve or let the polyserve API expose this stuff.
// app.use('/httpbin', httpbin.httpbin);
// app.get('/favicon.ico', function(request, response) {
// response.end();
// });
// app.use(function(request, response, next) {
// wct.emit('log:warn', '404', chalk.magenta(request.method),
// request.url);
// next();
// });
async function interruptHandler() {
// close the socket IO server directly if it is spun up
for (const io of (wct._socketIOServers || [])) {
// we will close the underlying server ourselves
(io).httpServer = null;
io.close();
}
await Promise.all(onDestroyHandlers.map((f) => f()));
}
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
interruptHandler().then(() => resolve(), resolve);
});
});
});
}
function exists(path: string): boolean {
try {
fs.statSync(path);
return true;
} catch (_err) {
return false;
}
}
// HACK(rictic): remove this ES6-compat hack and export webserver itself
webserver['webserver'] = webserver;
module.exports = webserver;
================================================
FILE: runner.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
module.exports = {
cli: require('./runner/cli'),
config: require('./runner/config'),
gulp: require('./runner/gulp'),
steps: require('./runner/steps'),
test: require('./runner/test'),
};
================================================
FILE: tasks/test.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
var chalk = require('chalk');
var test = require('../runner/test');
module.exports = function(grunt) {
grunt.registerMultiTask('wct-test', 'Runs tests via web-component-tester', function() {
var done = this.async();
test(this.options()).then(() => done(), (error) => {
console.log(chalk.red(error));
// Grunt only errors on `false` and instances of `Error`.
done(new Error(error));
});
});
};
================================================
FILE: test/fixtures/cli/conf/branch/leaf/thing.js
================================================
================================================
FILE: test/fixtures/cli/conf/json/wct.conf.js
================================================
var path = require('path');
module.exports = {
root: path.resolve(__dirname, '..'),
plugins: {
sauce: {
username: 'jsconf',
},
},
};
================================================
FILE: test/fixtures/cli/conf/json/wct.conf.json
================================================
{
"root": "..",
"plugins": {
"sauce": {
"username": "jsonconf"
}
}
}
================================================
FILE: test/fixtures/cli/conf/rooted/wct.conf.js
================================================
var path = require('path');
module.exports = {
root: path.resolve(__dirname, '../../..'),
suites: ['cli/conf/test'],
};
================================================
FILE: test/fixtures/cli/conf/test/foo.js
================================================
================================================
FILE: test/fixtures/cli/conf/wct.conf.js
================================================
module.exports = {
plugins: {
sauce: {
username: 'abc123',
},
},
};
================================================
FILE: test/fixtures/cli/standard/test/a.html
================================================
================================================
FILE: test/fixtures/cli/standard/test/b.js
================================================
================================================
FILE: test/fixtures/cli/standard/x-foo.html
================================================
================================================
FILE: test/fixtures/early-failure/bower_components/web-component-tester/package.json
================================================
{
"version": "0.0.1"
}
================================================
FILE: test/fixtures/early-failure/test/index.html
================================================
================================================
FILE: test/fixtures/fake-packages/duplicated-dep/package.json
================================================
{
"name": "duplicated-dep",
"version": "1.0.0"
}
================================================
FILE: test/fixtures/fake-packages/singleton-dep/package.json
================================================
{
"name": "singleton-dep",
"version": "1.0.0"
}
================================================
FILE: test/fixtures/integration/compilation/golden.json
================================================
{
"passing": 1,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"ES6 works": {
"state": "passing"
}
}
}
}
================================================
FILE: test/fixtures/integration/compilation/test/index.html
================================================
================================================
FILE: test/fixtures/integration/components_dir/bower_components/foo-element/foo-element.js
================================================
window.fooElementLoaded = 'yes';
================================================
FILE: test/fixtures/integration/components_dir/golden.json
================================================
{
"passing": 1,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"inline passing test": {"state": "passing"}
}
}
}
================================================
FILE: test/fixtures/integration/components_dir/test/index.html
================================================
================================================
FILE: test/fixtures/integration/components_dir/wct.conf.json
================================================
{
"plugins": {
"local": {
"browsers": [
"chrome"
],
"skipSeleniumInstall": true
}
}
}
================================================
FILE: test/fixtures/integration/custom-components_dir/.bowerrc
================================================
{
"directory": "my_components/"
}
================================================
FILE: test/fixtures/integration/custom-components_dir/golden.json
================================================
{
"passing": 1,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"inline passing test": {"state": "passing"}
}
}
}
================================================
FILE: test/fixtures/integration/custom-components_dir/my_components/bar-element/bar-element.js
================================================
window.barElementLoaded = 'yes';
================================================
FILE: test/fixtures/integration/custom-components_dir/test/index.html
================================================
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/.bowerrc
================================================
{
"directory": "my_components"
}
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/golden.json
================================================
{
"variants": {
"": {
"passing": 1,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {"state": "passing"}
}
}
},
"foo": {
"passing": 0,
"pending": 0,
"failing": 1,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {"state": "failing"}
}
},
"errors": {
"test/": {
"only works with mainline components": [
"expected 'foo' to equal 'mainline'",
"at index\\.html:11"
]
}
}
},
"bar": {
"passing": 0,
"pending": 0,
"failing": 1,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {"state": "failing"}
}
},
"errors": {
"test/": {
"only works with mainline components": [
"expected 'bar' to equal 'mainline'",
"at index\\.html:11"
]
}
}
}
}
}
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/my_components/package/index.js
================================================
window.nameOfThing = 'mainline';
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/my_components-bar/package/index.js
================================================
window.nameOfThing = 'bar';
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/my_components-foo/package/index.js
================================================
window.nameOfThing = 'foo';
================================================
FILE: test/fixtures/integration/custom-multiple-component_dirs/test/index.html
================================================
================================================
FILE: test/fixtures/integration/define-webserver-hook/golden.json
================================================
{
"passing": 2,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/tests.html": {
"suite": {
"nested test": {
"state": "passing"
}
},
"test": {
"state": "passing"
}
}
}
}
================================================
FILE: test/fixtures/integration/define-webserver-hook/test/index.html
================================================
================================================
FILE: test/fixtures/integration/define-webserver-hook/test/tests.html
================================================
================================================
FILE: test/fixtures/integration/failing/golden.json
================================================
{
"passing": 3,
"pending": 0,
"failing": 3,
"status": "complete",
"tests": {
"test/": {
"failing test": {
"state": "failing"
},
"inline failing test": {
"state": "failing"
},
"inline passing test": {
"state": "passing"
},
"passing test": {
"state": "passing"
}
},
"test/tests.html": {
"failing test": {
"state": "failing"
},
"passing test": {
"state": "passing"
}
}
},
"errors": {
"test/": {
"inline failing test": [
"expected false to be true",
"at index\\.html:(12|15)(:|$)"
],
"failing test": [
"expected false to be true",
"at tests\\.js:3(:|$)"
]
},
"test/tests.html": {
"failing test": [
"expected false to be true",
"at tests\\.html:(10|13)(:|$)"
]
}
}
}
================================================
FILE: test/fixtures/integration/failing/test/index.html
================================================
================================================
FILE: test/fixtures/integration/failing/test/tests.html
================================================
================================================
FILE: test/fixtures/integration/failing/test/tests.js
================================================
test('passing test', function() {});
test('failing test', function() {
assert.isTrue(false);
});
================================================
FILE: test/fixtures/integration/missing/golden.json
================================================
{
"passing": 0,
"pending": 0,
"failing": 0,
"status": "error"
}
================================================
FILE: test/fixtures/integration/missing/test/missing.html
================================================
================================================
FILE: test/fixtures/integration/mixed-suites/golden.json
================================================
{
"passing": 10,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"inline suite": {
"inline nested test": {
"state": "passing"
}
},
"inline test": {
"state": "passing"
},
"suite 1": {
"nested test 1": {
"state": "passing"
}
},
"test 1": {
"state": "passing"
},
"suite 2": {
"nested test 2": {
"state": "passing"
}
},
"test 2": {
"state": "passing"
}
},
"test/one.html": {
"suite 1": {
"nested test 1": {
"state": "passing"
}
},
"test 1": {
"state": "passing"
}
},
"test/two.html": {
"suite 2": {
"nested test 2": {
"state": "passing"
}
},
"test 2": {
"state": "passing"
}
}
}
}
================================================
FILE: test/fixtures/integration/mixed-suites/test/index.html
================================================
================================================
FILE: test/fixtures/integration/mixed-suites/test/one.html
================================================
================================================
FILE: test/fixtures/integration/mixed-suites/test/one.js
================================================
suite('suite 1', function() {
test('nested test 1', function() {});
});
test('test 1', function() {});
================================================
FILE: test/fixtures/integration/mixed-suites/test/two.html
================================================
================================================
FILE: test/fixtures/integration/mixed-suites/test/two.js
================================================
suite('suite 2', function() {
test('nested test 2', function() {});
});
test('test 2', function() {});
================================================
FILE: test/fixtures/integration/multiple-component_dirs/bower_components/package/index.js
================================================
window.nameOfThing = 'mainline';
================================================
FILE: test/fixtures/integration/multiple-component_dirs/bower_components-bar/package/index.js
================================================
window.nameOfThing = 'bar';
================================================
FILE: test/fixtures/integration/multiple-component_dirs/bower_components-foo/package/index.js
================================================
window.nameOfThing = 'foo';
================================================
FILE: test/fixtures/integration/multiple-component_dirs/golden.json
================================================
{
"variants": {
"": {
"passing": 1,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {
"state": "passing"
}
}
}
},
"foo": {
"passing": 0,
"pending": 0,
"failing": 1,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {
"state": "failing"
}
}
},
"errors": {
"test/": {
"only works with mainline components": [
"expected 'foo' to equal 'mainline'",
"at index\\.html:(10|13)"
]
}
}
},
"bar": {
"passing": 0,
"pending": 0,
"failing": 1,
"status": "complete",
"tests": {
"test/": {
"only works with mainline components": {
"state": "failing"
}
}
},
"errors": {
"test/": {
"only works with mainline components": [
"expected 'bar' to equal 'mainline'",
"at index\\.html:(10|13)"
]
}
}
}
}
}
================================================
FILE: test/fixtures/integration/multiple-component_dirs/test/index.html
================================================
================================================
FILE: test/fixtures/integration/multiple-replace/dom-if-element.html
================================================
================================================
FILE: test/fixtures/integration/multiple-replace/dom-repeat-fixture.html
================================================
================================================
FILE: test/fixtures/integration/multiple-replace/test/index.html
================================================
================================================
FILE: test/fixtures/integration/multiple-replace/test/tests.html
================================================
================================================
FILE: test/fixtures/integration/nested/golden.json
================================================
{
"passing": 4,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/": {"js test": {"state": "passing"}},
"test/one/tests.html": {"test": {"state": "passing"}},
"test/two/": {"inline test": {"state": "passing"}},
"test/leaf.html": {"test": {"state": "passing"}}
}
}
================================================
FILE: test/fixtures/integration/nested/test/index.html
================================================
================================================
FILE: test/fixtures/integration/nested/test/leaf.html
================================================
================================================
FILE: test/fixtures/integration/nested/test/leaf.js
================================================
test('js test', function() {});
================================================
FILE: test/fixtures/integration/nested/test/one/index.html
================================================
================================================
FILE: test/fixtures/integration/nested/test/one/tests.html
================================================
================================================
FILE: test/fixtures/integration/nested/test/two/index.html
================================================
================================================
FILE: test/fixtures/integration/no-tests/golden.json
================================================
{
"passing": 0,
"pending": 0,
"failing": 0,
"status": "complete"
}
================================================
FILE: test/fixtures/integration/no-tests/test/index.html
================================================
================================================
FILE: test/fixtures/integration/query-string/golden.json
================================================
{
"passing": 3,
"pending": 0,
"failing": 0,
"status": "complete",
"tests": {
"test/tests.html": {
"preserves query strings": {
"state": "passing"
}
},
"test/": {
"preserves query strings (?fizz=buzz&foo=bar)": {
"state": "passing"
},
"preserves query strings (?fizz=buzz)": {
"state": "passing"
}
}
}
}
================================================
FILE: test/fixtures/integration/query-string/test/index.html
================================================
================================================
FILE: test/fixtures/integration/query-string/test/tests.html
================================================
================================================
FILE: test/fixtures/integration/query-string/test/tests.js
================================================
// TODO(usergenic): Figure out a reasonable solution to get URL() and
// document.currentScript to work in IE11 and then put these tests back
// in commission.
//
// See https://github.com/PolymerElements/iron-location/blob/3ef6d758514d7cb80a3297f8ef5208774d486e88/iron-location.html#L65
// as a possible solution.
/*
var url = new URL(document.currentScript.src);
test('preserves query strings (' + url.search + ')', function () {
expect(url.search).to.match(/\?fizz=buzz/);
});
*/
test('preserves query strings (?fizz=buzz&foo=bar)', function () { });
test('preserves query strings (?fizz=buzz)', function () { });
================================================
FILE: test/fixtures/paths/bar/a.js
================================================
================================================
FILE: test/fixtures/paths/bar/index.html
================================================
================================================
FILE: test/fixtures/paths/bar/index.js
================================================
================================================
FILE: test/fixtures/paths/baz/a/fizz.html
================================================
================================================
FILE: test/fixtures/paths/baz/a.html
================================================
================================================
FILE: test/fixtures/paths/baz/b/deep/index.html
================================================
================================================
FILE: test/fixtures/paths/baz/b/deep/stuff.html
================================================
================================================
FILE: test/fixtures/paths/baz/b/deep/stuff.js
================================================
================================================
FILE: test/fixtures/paths/baz/b/index.html
================================================
================================================
FILE: test/fixtures/paths/baz/b/one.js
================================================
================================================
FILE: test/fixtures/paths/baz/b.js
================================================
================================================
FILE: test/fixtures/paths/foo/one.js
================================================
================================================
FILE: test/fixtures/paths/foo/three.css
================================================
================================================
FILE: test/fixtures/paths/foo/two.html
================================================
================================================
FILE: test/fixtures/paths/foo.html
================================================
================================================
FILE: test/fixtures/paths/foo.js
================================================
================================================
FILE: test/integration/browser.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {expect} from 'chai';
import * as express from 'express';
import * as fs from 'fs';
import * as lodash from 'lodash';
import * as path from 'path';
import {ExpressAppMapper, ServerOptions} from 'polyserve/lib/start_server';
import {BrowserDef, Stats} from '../../runner/browserrunner';
import {CompletedState, TestEndData} from '../../runner/clireporter';
import * as config from '../../runner/config';
import {Context} from '../../runner/context';
import {test} from '../../runner/test';
import {makeProperTestDir} from './setup_test_dir';
function parseList(stringList?: string): string[] {
return (stringList || '')
.split(',')
.map((item) => item.trim())
.filter((item) => !!item);
}
function loadOptionsFile(dir: string): config.Config {
const filename = path.join(dir, 'wct.conf.json');
try {
const jsonOptions = fs.readFileSync(filename, 'utf-8').toString();
const parsedOptions = JSON.parse(jsonOptions);
if (parsedOptions !== null && typeof parsedOptions === 'object') {
return parsedOptions;
} else {
return {};
}
} catch (e) {
return {};
}
}
const testLocalBrowsers = !process.env.SKIP_LOCAL_BROWSERS;
const testLocalBrowsersList = parseList(process.env.TEST_LOCAL_BROWSERS);
const testRemoteBrowsers = !process.env.SKIP_REMOTE_BROWSERS &&
process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY;
const testRemoteBrowsersList = parseList(process.env.TEST_REMOTE_BROWSERS);
if (testRemoteBrowsersList.length === 0) {
testRemoteBrowsersList.push('default');
}
interface TestErrorExpectation {
[fileName: string]: {
// The test name mapped to the error
[testName: string]: [string, string];
};
}
type Golden = VariantsGolden|VariantResultGolden;
function isVariantsGolden(golden: Golden): golden is VariantsGolden {
return !!golden['variants'];
}
interface VariantsGolden {
variants: {[variantName: string]: VariantResultGolden};
}
interface VariantResultGolden {
passing: number;
pending: number;
failing: number;
status: string;
tests: TestNode;
errors: TestErrorExpectation;
}
type TestNode = {
state?: CompletedState; [subTestName: string]: TestNode | CompletedState;
};
class TestResults {
variants: {[variantName: string]: VariantResults} = {};
runError: any = null;
testRunnerError: any = null;
getVariantResults(variantName: string): VariantResults {
this.variants[variantName] =
this.variants[variantName] || new VariantResults();
return this.variants[variantName];
}
}
class VariantResults {
tests: TestNode = {};
testErrors: TestNode = {};
stats: {[browserName: string]: Stats} = {};
errors: {[browserName: string]: any} = {};
}
// Tests
/** Describes all suites, mixed into the environments being run. */
function runsAllIntegrationSuites(options: config.Config = {}) {
const integrationDirnames =
fs.readdirSync(integrationDir).filter((fn) => fn !== 'temp');
// Overwrite integrationDirnames to run tests in isolation while developing:
// integrationDirnames = ['components_dir'];
// TODO(#421): `missing` correctly fails, but currently it times out which
// takes ~2 minutes.
const suitesToSkip = new Set(['missing']);
for (const fn of integrationDirnames) {
runIntegrationSuiteForDir(fn, options, suitesToSkip.has(fn));
}
}
function runIntegrationSuiteForDir(
dirname: string, options: config.Config, skip: boolean) {
runsIntegrationSuite(dirname, options, skip, function(testResults) {
const golden: Golden = JSON.parse(fs.readFileSync(
path.join(integrationDir, dirname, 'golden.json'), 'utf-8'));
let variantsGolden: VariantsGolden;
if (isVariantsGolden(golden)) {
variantsGolden = golden;
} else {
variantsGolden = {variants: {'': golden}};
}
it('ran the correct variants', function() {
expect(Object.keys(testResults.variants).sort())
.to.deep.equal(Object.keys(variantsGolden.variants).sort());
});
for (const variantName in variantsGolden.variants) {
const run = () => assertVariantResultsConformToGolden(
variantsGolden.variants[variantName],
testResults.getVariantResults(variantName));
if (variantName !== '') {
describe(`the variant with bower_components-${variantName}`, run);
} else {
run();
}
}
});
}
const integrationDir = path.resolve(__dirname, '../fixtures/integration');
/**
* Creates a mocha context that runs an integration suite (once), and hangs onto
* the output for tests.
*/
function runsIntegrationSuite(
dirName: string, options: config.Config, skip: boolean,
contextFunction: (context: TestResults) => void) {
const suiteName = `integration fixture dir '${dirName}'`;
let describer: (suiteName: string, spec: () => void) => void = describe;
if (skip) {
describer = describe.skip;
}
describer(suiteName, function() {
const log: string[] = [];
const testResults = new TestResults();
before(async function() {
const suiteRoot = await makeProperTestDir(dirName);
const suiteOptions = loadOptionsFile(
path.join('test', 'fixtures', 'integration', dirName));
// Filter the list of browsers within the suite's options by the global
// overrides if they are present.
if (suiteOptions.plugins !== undefined) {
if (testLocalBrowsersList.length > 0 &&
!testLocalBrowsersList.includes('default') &&
suiteOptions.plugins.local !== undefined &&
suiteOptions.plugins.local.browsers !== undefined) {
suiteOptions.plugins.local.browsers =
suiteOptions.plugins.local.browsers.filter(
(b: string) => testLocalBrowsersList.includes(b));
}
if (testRemoteBrowsersList.length > 0 &&
suiteOptions.plugins.sauce !== undefined &&
suiteOptions.plugins.sauce.browsers !== undefined) {
suiteOptions.plugins.sauce.browsers =
suiteOptions.plugins.sauce.browsers.filter(
(b: string) => testRemoteBrowsersList.includes(b));
}
}
const allOptions: config.Config = Object.assign(
{
output: {write: log.push.bind(log)},
ttyOutput: false,
root: suiteRoot,
browserOptions: {
name: 'web-component-tester',
tags: ['org:Polymer', 'repo:web-component-tester'],
},
},
options, suiteOptions);
const context = new Context(allOptions);
const addEventHandler = (name: string, handler: Function) => {
context.on(name, function() {
try {
handler.apply(null, arguments);
} catch (error) {
console.error(`Error inside ${name} handler in integration tests:`);
console.error(error.stack);
}
});
};
addEventHandler(
'test-end',
(browserDef: BrowserDef, data: TestEndData, stats: Stats) => {
const variantResults =
testResults.getVariantResults(browserDef.variant || '');
const browserName = getBrowserName(browserDef);
variantResults.stats[browserName] = stats;
let testNode = (
variantResults.tests[browserName] =
variantResults.tests[browserName] || {});
let errorNode = variantResults.testErrors[browserName] =
variantResults.testErrors[browserName] || {};
for (let i = 0; i < data.test.length; i++) {
const name = data.test[i];
testNode = (testNode[name] = testNode[name] || {});
if (i < data.test.length - 1) {
errorNode = errorNode[name] = errorNode[name] || {};
} else if (data.error) {
errorNode[name] = data.error;
}
}
testNode.state = data.state;
});
addEventHandler(
'browser-end', (browserDef: BrowserDef, error: any, stats: Stats) => {
const variantResults =
testResults.getVariantResults(browserDef.variant || '');
const browserName = getBrowserName(browserDef);
variantResults.stats[browserName] = stats;
variantResults.errors[browserName] = error || null;
});
addEventHandler('run-end', (error: any) => {
testResults.runError = error;
});
// Don't fail the integration suite on test errors.
try {
await test(context);
} catch (error) {
testResults.testRunnerError = error.message;
}
});
afterEach(function() {
if (this.currentTest.state === 'failed') {
process.stderr.write(
`\n Output of wct for integration suite named \`${dirName}\`` +
`\n` +
` ======================================================\n\n`);
for (const line of log.join('').split('\n')) {
process.stderr.write(` ${line}\n`);
}
process.stderr.write(
`\n ======================================================\n\n`);
}
});
contextFunction(testResults);
});
}
if (testLocalBrowsers || testRemoteBrowsers) {
describe('Browser Tests', function() {
const pluginConfig = {};
if (testLocalBrowsers) {
pluginConfig.local = {
browsers: testLocalBrowsersList,
skipSeleniumInstall: true,
};
}
if (testRemoteBrowsers) {
pluginConfig.sauce = {
browsers: testRemoteBrowsersList,
};
}
runsAllIntegrationSuites({
plugins: pluginConfig,
});
});
}
/** Assert that all browsers passed. */
function assertPassed(context: TestResults) {
if (context.runError) {
console.error(
context.runError.stack || context.runError.message || context.runError);
}
if (context.testRunnerError) {
console.error(
context.testRunnerError.stack || context.testRunnerError.message ||
context.testRunnerError);
}
expect(context.runError).to.not.be.ok;
expect(context.testRunnerError).to.not.be.ok;
// expect(context.errors).to.deep.equal(repeatBrowsers(context, null));
}
function assertFailed(context: VariantResults, expectedError: string) {
// expect(context.runError).to.eq(expectedError);
// expect(context.testRunnerError).to.be.eq(expectedError);
expect(context.errors).to.deep.equal(repeatBrowsers(context, expectedError));
}
/** Asserts that all browsers match the given stats. */
function assertStats(
context: VariantResults, passing: number, pending: number, failing: number,
status: 'complete') {
const expected: Stats = {passing, pending, failing, status};
expect(context.stats).to.deep.equal(repeatBrowsers(context, expected));
}
/** Asserts that all browsers match the given test layout. */
function assertTests(context: VariantResults, expected: TestNode) {
expect(context.tests).to.deep.equal(repeatBrowsers(context, expected));
}
/** Asserts that all browsers emitted the given errors. */
function assertTestErrors(
context: VariantResults, expected: TestErrorExpectation) {
lodash.each(context.testErrors, function(actual: any, browser) {
expect(Object.keys(expected))
.to.have.members(
Object.keys(actual),
'Test file mismatch for ' + browser +
`: expected ${JSON.stringify(Object.keys(expected))} - got ${
JSON.stringify(Object.keys(actual))}`);
lodash.each(actual, function(errors: any, file: any) {
const expectedErrors = expected[file];
// Currently very dumb for simplicity: We don't support suites.
expect(Object.keys(expectedErrors))
.to.have.members(
Object.keys(errors),
`Test failure mismatch for ${file} on ${browser}`);
lodash.each(errors, function(error: Error, test: string) {
const locationInfo = `for ${file} - "${test}" on ${browser}`;
const expectedError = expectedErrors[test];
const stackLines = error.stack.split('\n');
expect(error.message)
.to.eq(expectedError[0], `Error message mismatch ${locationInfo}`);
// Chai fails to emit stacks for Firefox.
// https://github.com/chaijs/chai/issues/100
if (browser.match(/firefox|internet explorer 11/)) {
return;
}
const expectedErrorText = expectedError[0];
const stackTraceMatcher = expectedError[1];
expect(stackLines[0]).to.eq(expectedErrorText);
expect(stackLines[stackLines.length - 1])
.to.match(
new RegExp(stackTraceMatcher), `error.stack="${error.stack}"`);
});
});
});
}
function assertVariantResultsConformToGolden(
golden: VariantResultGolden, variantResults: VariantResults) {
// const variantResults = testResults.getVariantResults('');
it('records the correct result stats', function() {
try {
assertStats(
variantResults, golden.passing, golden.pending, golden.failing,
golden.status);
} catch (_) {
// mocha reports twice the failures because reasons
// https://github.com/mochajs/mocha/issues/2083
assertStats(
variantResults, golden.passing, golden.pending, golden.failing * 2,
golden.status);
}
});
if (golden.passing + golden.pending + golden.failing === 0 && !golden.tests) {
return;
}
it('runs the correct tests', function() {
assertTests(variantResults, golden.tests);
});
if (golden.errors || golden.failing > 0) {
it('emits well formed errors', function() {
assertTestErrors(variantResults, golden.errors);
});
}
// it('passed the test', function() {
// assertPassed(testResults);
// });
}
function getBrowserName(browser: BrowserDef) {
const parts: string[] = [];
if (browser.platform && !browser.deviceName) {
parts.push(browser.platform);
}
parts.push(browser.deviceName || browser.browserName);
if (browser.version) {
parts.push(browser.version);
}
if (browser.variant) {
parts.push(`[${browser.variant}]`);
}
return parts.join(' ');
}
function repeatBrowsers(
context: VariantResults, data: T): {[browserId: string]: T} {
expect(Object.keys(context.stats).length)
.to.be.greaterThan(0, 'No browsers were run. Bad environment?');
return lodash.mapValues(context.stats, () => data);
}
describe('define:webserver hook', () => {
it('supports substituting given app', async function() {
const suiteRoot = await makeProperTestDir('define-webserver-hook');
const log: string[] = [];
const requestedUrls: string[] = [];
const options: config.Config = {
output: {write: log.push.bind(log)},
ttyOutput: false,
root: suiteRoot,
browserOptions: {
name: 'web-component-tester',
tags: ['org:Polymer', 'repo:web-component-tester'],
},
plugins: {
local: {
browsers: testLocalBrowsersList,
skipSeleniumInstall: true,
}
},
};
const context = new Context(options);
context.hook(
'define:webserver',
(app: express.Application, assign: (sub: express.Express) => void,
_options: ServerOptions, done: (err?: any) => void) => {
const newApp = express();
newApp.get('*', (request, _response, next) => {
requestedUrls.push(request.url);
next();
});
newApp.use(app);
assign(newApp);
done();
});
await test(context);
// Our middleware records all the requested urls into this requestedUrls
// array, so we can test that the middleware works by inspecting it for
// expected tests.html file which should be loaded by index.html
expect(requestedUrls)
.to.include('/components/define-webserver-hook/test/tests.html');
return true;
});
});
describe('early failures', () => {
it(`wct doesn't start testing if it's not bower installed locally`,
async function() {
const log: string[] = [];
const options: config.Config = {
output: {write: log.push.bind(log)},
ttyOutput: false,
root: path.join(
__dirname, '..', 'fixtures', 'integration', 'components_dir'),
browserOptions: {
name: 'web-component-tester',
tags: ['org:Polymer', 'repo:web-component-tester'],
},
plugins: {
local: {browsers: testLocalBrowsersList, skipSeleniumInstall: true},
},
};
const context = new Context(options);
try {
await test(context);
throw new Error('Expected test() to fail!');
} catch (e) {
expect(e.message).to.match(
/The web-component-tester Bower package is not installed as a dependency of this project/);
}
});
it('fails if the client side library is out of allowed version range',
async function() {
const log: string[] = [];
const options: config.Config = {
output: {write: log.push.bind(log)},
ttyOutput: false,
root: path.join(__dirname, '..', 'fixtures', 'early-failure'),
browserOptions: {
name: 'web-component-tester',
tags: ['org:Polymer', 'repo:web-component-tester'],
},
plugins: {
local: {browsers: testLocalBrowsersList, skipSeleniumInstall: true},
},
};
const context = new Context(options);
try {
await test(context);
throw new Error('Expected test() to fail!');
} catch (e) {
expect(e.message).to.match(
/The web-component-tester Bower package installed is incompatible with the\n\s*wct node package you're using/);
}
});
});
================================================
FILE: test/integration/setup_test_dir.ts
================================================
import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as bowerConfig from 'bower-config';
const baseDir = path.join(__dirname, '..', 'fixtures', 'integration');
/**
* Sets up the given integration fixture with proper bower components.
*
* For wct to work it needs to be installed in the bower_components directory
* (or, with variants, in each variant directory). So this copies the given
* integration test fixture, then sets up symlinks from
* bower_components/web-component-tester/browser.js to the browser.js of this
* repo. It also makes symlinks for each of wct's bower dependencies into the
* integration tests' bower_components dir.
*
* @param dirname The basename of an integration fixture directory.
* @return A fully resolved path to a copy of the fixture directory with
* a proper bower_components directory.
*/
export async function makeProperTestDir(dirname: string) {
const startingDir = path.join(baseDir, dirname);
const tempDir = path.join(baseDir, 'temp');
if (await exists(tempDir)) {
await new Promise((resolve, reject) => {
rimraf(tempDir, (err) => err ? reject(err) : resolve());
});
}
fs.mkdirSync(tempDir);
// copy dir
const pathToTestDir = await copyDir(startingDir, tempDir);
const bowerDir = bowerConfig.read(pathToTestDir).directory;
fs.mkdirSync(path.join(pathToTestDir, 'node_modules'));
fs.mkdirSync(
path.join(pathToTestDir, 'node_modules', 'web-component-tester'));
// set up symlinks into component dirs for browser.js, data/, and wct's
// dependencies (like mocha, sinon, etc)
const componentsDirs = new Set([bowerDir]);
for (const baseFile of fs.readdirSync(startingDir)) {
if (new RegExp(`${bowerDir}(-|$)`).test(baseFile)) {
componentsDirs.add(baseFile);
}
}
for (const baseComponentsDir of componentsDirs) {
const componentsDir = path.join(pathToTestDir, baseComponentsDir);
if (!await exists(componentsDir)) {
fs.mkdirSync(componentsDir);
}
// all of wct's bower deps should be present in the project under tests'
// components dir
const bowerDeps =
fs.readdirSync(path.join(__dirname, '../../bower_components'));
for (const baseFile of bowerDeps) {
fs.symlinkSync(
path.join('../../../../../../bower_components', baseFile),
path.join(componentsDir, baseFile));
}
// Also set up a web-component-tester dir with symlinks into our own
// client-side files.
const wctDir = path.join(componentsDir, 'web-component-tester');
fs.mkdirSync(wctDir);
fs.symlinkSync(
'../../../../../../../browser.js', path.join(wctDir, 'browser.js'),
'file');
fs.symlinkSync(
'../../../../../../../package.json', path.join(wctDir, 'package.json'),
'file');
fs.symlinkSync(
'../../../../../../../data', path.join(wctDir, 'data'), 'dir');
}
return pathToTestDir;
}
async function copyDir(from: string, to: string) {
const newDir = path.join(to, path.basename(from));
fs.mkdirSync(newDir);
for (const baseFile of fs.readdirSync(from)) {
const file = path.join(from, baseFile);
if (fs.statSync(file).isDirectory()) {
await copyDir(file, newDir);
} else {
const newFile = path.join(newDir, baseFile);
fs.writeFileSync(newFile, fs.readFileSync(file));
}
}
return newDir;
}
async function exists(fn: string) {
return new Promise((resolve) => fs.stat(fn, (err) => resolve(!err)));
}
================================================
FILE: test/unit/cli.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chai from 'chai';
import * as _ from 'lodash';
import * as path from 'path';
import * as sinon from 'sinon';
import * as cli from '../../runner/cli';
import * as context from '../../runner/context';
import * as steps from '../../runner/steps';
const expect = chai.expect;
const wctLocalBrowsers = require('wct-local/lib/browsers');
const FIXTURES = path.resolve(__dirname, '../fixtures/cli');
const LOCAL_BROWSERS = {
aurora: {browserName: 'aurora', version: '1'},
canary: {browserName: 'canary', version: '2'},
chrome: {browserName: 'chrome', version: '3'},
firefox: {browserName: 'firefox', version: '4'},
};
describe('cli', () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(steps, 'prepare').callsFake(async(): Promise => undefined);
sandbox.stub(steps, 'runTests').callsFake(async(): Promise => undefined);
sandbox.stub(wctLocalBrowsers, 'detect')
.callsFake(
async () => _.omit(LOCAL_BROWSERS, 'aurora'));
sandbox.stub(wctLocalBrowsers, 'supported').callsFake(() => _.keys(LOCAL_BROWSERS));
});
afterEach(() => {
sandbox.restore();
});
describe('.run', () => {
const expectRun = async (args: string[], logInput?: string[]) => {
const log = logInput || [];
const stream = {write: log.push.bind(log)};
try {
await cli.run({}, args, stream);
} catch (error) {
log.forEach((line) => process.stderr.write(line));
throw error;
}
const call = <{args: [context.Context]}>steps.runTests['getCall'](0);
return call.args[0];
};
it('honors --version flags', async () => {
const version: String = require('../../package.json').version;
let output: String;
await cli.run({}, ['--version'], {write: (o: String) => output = o});
expect(output).to.eq(`${version}\n`);
await cli.run({}, ['-V'], {write: (o: String) => output = o});
expect(output).to.eq(`${version}\n`);
});
it('expands test/ by default, ' +
'and serves from /components/',
async () => {
process.chdir(path.join(FIXTURES, 'standard'));
const options = (await expectRun([])).options;
expect(options.suites).to.have.members([
'test/a.html',
'test/b.js',
]);
expect(options.root).to.equal(path.join(FIXTURES, 'standard'));
});
it('honors globs', async () => {
process.chdir(path.join(FIXTURES, 'standard'));
const options = (await expectRun(['**/*.html'])).options;
expect(options.suites).to.have.members([
'test/a.html',
'x-foo.html',
]);
});
it('honors expanded files', async () => {
process.chdir(path.join(FIXTURES, 'standard'));
const options = (await expectRun(['test/b.js', 'x-foo.html'])).options;
expect(options.suites).to.have.members([
'test/b.js',
'x-foo.html',
]);
});
it('honors --root with no specified suites', async () => {
process.chdir(__dirname);
const root = path.join(FIXTURES, 'standard');
const options = (await expectRun(['--root', root])).options;
expect(options.suites).to.have.members([
'test/a.html',
'test/b.js',
]);
expect(options.root).to.equal(root);
});
it('honors --root with specified suites', async () => {
process.chdir(__dirname);
const root = path.join(FIXTURES, 'standard');
const options = (await expectRun(['--root', root, '**/*.html'])).options;
expect(options.suites).to.have.members([
'test/a.html',
'x-foo.html',
]);
expect(options.root).to.equal(root);
});
it('throws an error if no suites could be found', async () => {
try {
await cli.run({}, ['404'], {write: () => {}});
} catch (error) {
expect(error).to.match(/no.*suites.*found/i);
return;
}
throw new Error('cli.run should have failed');
});
it('loads the local and sauce plugins by default', async () => {
process.chdir(path.join(FIXTURES, 'standard'));
const context = await expectRun([]);
expect(context.enabledPlugins()).to.have.members(['local', 'sauce']);
});
it('allows plugins to be diabled via --skip-plugin', async () => {
process.chdir(path.join(FIXTURES, 'standard'));
const context = await expectRun(['--skip-plugin', 'sauce']);
expect(context.enabledPlugins()).to.have.members(['local']);
});
// TODO(nevir): Remove after deprecation period.
it('throws an error when --webRunner is set', () => {
return cli.run({}, ['--webRunner', 'foo'], {write: () => {}})
.then(
() => {
throw new Error('cli.run should have failed');
},
(error) => {
expect(error.message).to.include('webRunner');
expect(error.message).to.include('suites');
});
});
describe('with wct.conf.js', () => {
const ROOT = path.join(FIXTURES, 'conf');
it('serves from /components/', async () => {
process.chdir(ROOT);
const options = (await expectRun([])).options;
expect(options.suites).to.have.members([
'test/foo.js',
]);
expect(options.root).to.equal(ROOT);
});
it('walks the ancestry', async () => {
process.chdir(path.join(ROOT, 'branch/leaf'));
const options = (await expectRun([])).options;
expect(options.suites).to.have.members([
'test/foo.js',
]);
expect(options.root).to.equal(ROOT);
});
it('honors specified values', async () => {
process.chdir(ROOT);
const options = (await expectRun([])).options;
expect(options.plugins['sauce'].username).to.eq('abc123');
});
it('honors root', async () => {
process.chdir(path.join(ROOT, 'rooted'));
const options = (await expectRun([])).options;
expect(options.suites).to.have.members([
'cli/conf/test/foo.js',
]);
expect(options.root).to.equal(path.dirname(FIXTURES));
});
});
describe('deprecated flags', () => {
beforeEach(() => {
process.chdir(path.join(FIXTURES, 'standard'));
});
describe('--browsers', () => {
it('warns when used', async () => {
const log: string[] = [];
await expectRun(['--browsers', 'firefox'], log);
const hasWarning =
_.some(log, (l) => /--browsers.*deprecated/i.test(l));
expect(hasWarning)
.to.eq(true, 'Expected a warning that --browsers is deprecated');
});
// Semi-integration test.
// This also checks that wct-local (mostly) works.
it('supports local browsers', async () => {
const args = ['--browsers', 'firefox', '-b', 'chrome'];
const options = (await expectRun(args)).options;
const names =
options.activeBrowsers.map((browser) => browser.browserName);
expect(names).to.have.members(['firefox', 'chrome']);
});
// Semi-integration test.
// This also checks that wct-sauce (mostly) works.
it('supports sauce browsers', async () => {
const args = ['--browsers', 'linux/firefox', '-b', 'linux/chrome'];
const options = (await expectRun(args)).options;
const names =
options.activeBrowsers.map((browser) => browser.browserName);
expect(names).to.have.members(['firefox', 'chrome']);
});
});
describe('--remote', () => {
it('warns when used', async () => {
const log: string[] = [];
await expectRun(['--remote'], log);
const hasWarning = _.some(log, (l) => /--remote.*--sauce/.test(l));
expect(hasWarning)
.to.eq(true, 'Expected a warning that --remote is deprecated');
});
// Semi-integration test.
// This also checks that wct-sauce (mostly) works.
it('sets up default sauce browsers', async () => {
const options = (await expectRun(['--remote'])).options;
const platforms =
options.activeBrowsers.map((browser) => browser.platform);
expect(_.compact(platforms).length).to.be.gt(0);
});
});
});
});
});
================================================
FILE: test/unit/config.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chai from 'chai';
import * as path from 'path';
import * as config from '../../runner/config';
import {Context} from '../../runner/context';
const expect = chai.expect;
describe('config', function() {
describe('.merge', function() {
it('avoids modifying the input', function() {
const one = {foo: 1};
const two = {foo: 2};
const merged = config.merge(one, two);
expect(one.foo).to.eq(1);
expect(two.foo).to.eq(2);
expect(merged.foo).to.eq(2);
expect(merged).to.not.equal(two);
});
it('honors false as an explicit blacklisting', function() {
const merged = config.merge(
{plugins: {foo: {}}}, {plugins: {foo: false}},
{plugins: {foo: {}, bar: {}}});
expect(merged).to.deep.equal({plugins: {foo: false, bar: {}}});
});
});
describe('.expand', function() {
describe('deprecated options', function() {
it('expands local string browsers', function() {
const context = new Context({browsers: ['chrome']});
return config.expand(context).then(() => {
expect(context.options.plugins['local'].browsers).to.have.members([
'chrome'
]);
});
});
it('expands sauce string browsers', function() {
const context = new Context({browsers: ['linux/firefox']});
return config.expand(context).then(() => {
expect(context.options.plugins['sauce'].browsers).to.have.members([
'linux/firefox'
]);
});
});
it('expands local object browsers', function() {
const context =
new Context({browsers: [{browserName: 'firefox'}]});
return config.expand(context).then(() => {
expect(context.options.plugins['local'].browsers)
.to.deep['have']
.members([{browserName: 'firefox'}]);
});
});
it('expands sauce object browsers', function() {
const context = new Context(
{browsers: [{browserName: 'safari', platform: 'OS X'}]});
return config.expand(context).then(() => {
expect(context.options.plugins['sauce'].browsers)
.to.deep['have']
.members([{browserName: 'safari', platform: 'OS X'}]);
});
});
});
});
describe('npm pathing', function() {
describe('Resolves simple names to paths', function() {
const localPackagePath =
path.join(__dirname, '../fixtures/fake-packages/singleton-dep');
const options = {root: localPackagePath};
const npmPackages: config.NPMPackage[] = [
{name: 'dependency', jsEntrypoint: 'index.js'},
{name: 'dependency', jsEntrypoint: 'arbitraryJsFile.js'}
];
const resolvedEntrypoints =
config.resolveWctNpmEntrypointNames(options, npmPackages);
expect(resolvedEntrypoints[0]).to.equal('dependency/index.js');
expect(resolvedEntrypoints[1]).to.equal('dependency/arbitraryJsFile.js');
});
it('Resolves duplicated names to paths', function() {
const localPackagePath =
path.join(__dirname, '../fixtures/fake-packages/duplicated-dep');
const options = {root: localPackagePath};
const npmPackages: config.NPMPackage[] = [
{name: 'dependency', jsEntrypoint: 'index.js'},
{name: 'dependency', jsEntrypoint: 'arbitraryJsFile.js'}
];
const resolvedEntrypoints =
config.resolveWctNpmEntrypointNames(options, npmPackages);
expect(resolvedEntrypoints[0])
.to.equal('wct-browser-legacy/node_modules/dependency/index.js');
expect(resolvedEntrypoints[1])
.to.equal(
'wct-browser-legacy/node_modules/dependency/arbitraryJsFile.js');
});
});
});
================================================
FILE: test/unit/context.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chai from 'chai';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import {Context} from '../../runner/context';
import {Plugin} from '../../runner/plugin';
const expect = chai.expect;
chai.use(sinonChai);
describe('Context', () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
afterEach(() => {
sandbox.restore();
});
describe('.plugins', () => {
it('excludes plugins with a falsy config', async () => {
const context = new Context({plugins: {local: false, sauce: {}}});
const stub = sandbox.stub(Plugin, 'get').callsFake((name: string) => {
return Promise.resolve(name);
});
const plugins = await context.plugins();
expect(stub).to.have.been.calledOnce;
expect(stub).to.have.been.calledWith('sauce');
expect(plugins).to.have.members(['sauce']);
});
it('excludes plugins disabled: true', async () => {
const context =
new Context({plugins: {local: {}, sauce: {disabled: true}}});
const stub = sandbox.stub(Plugin, 'get').callsFake((name: string) => {
return Promise.resolve(name);
});
const plugins = await context.plugins();
expect(stub).to.have.been.calledOnce;
expect(stub).to.have.been.calledWith('local');
expect(plugins).to.have.members(['local']);
});
describe('hook handlers with non-callback second argument', async () => {
it('are passed the "done" callback function instead of the argument passed to emitHook',
async () => {
const context = new Context();
context.hook('foo', function(arg1: any, done: () => void) {
expect(arg1).to.eq('hookArg');
done();
});
await context.emitHook('foo', 'hookArg');
});
});
describe('hook handlers written to call callbacks', () => {
it('passes additional arguments through', async () => {
const context = new Context();
context.hook(
'foo',
(arg1: string, arg2: number, hookDone: (err?: any) => void) => {
expect(arg1).to.eq('one');
expect(arg2).to.eq(2);
hookDone();
});
// Tests the promise form of emitHook.
await context.emitHook('foo', 'one', 2);
// Tests the callback form of emitHook.
const error = await new Promise((resolve) => {
context.emitHook('foo', 'one', 2, resolve);
});
expect(error).to.not.be.ok;
});
it('halts on error', async () => {
const context = new Context();
context.hook('bar', function(hookDone: (err?: any) => void) {
hookDone('nope');
});
// Tests the promise form of emitHook.
try {
await context.emitHook('bar');
throw new Error('emitHook should have thrown');
} catch (error) {
expect(error).to.eq('nope');
}
// Tests the callback form of emitHook.
const error = await new Promise((resolve) => {
context.emitHook('bar', resolve);
});
expect(error).to.eq('nope');
});
});
describe('hooks handlers written to return promises', () => {
it('passes additional arguments through', async () => {
const context = new Context();
context.hook('foo', async function(arg1: any, arg2: any) {
expect(arg1).to.eq('one');
expect(arg2).to.eq(2);
});
await context.emitHook('foo', 'one', 2);
const error = await new Promise((resolve) => {
context.emitHook('foo', 'one', 2, resolve);
});
expect(error).to.not.be.ok;
});
it('halts on error', async () => {
const context = new Context();
context.hook('bar', async () => {
throw 'nope';
});
// Tests the promise form of emitHook.
try {
await context.emitHook('bar');
throw new Error('emitHook should have thrown');
} catch (error) {
expect(error).to.eq('nope');
}
// Tests the callback form of emitHook.
const error = await new Promise((resolve) => {
context.emitHook('bar', resolve);
});
expect(error).to.eq('nope');
});
});
});
});
================================================
FILE: test/unit/grunt.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chai from 'chai';
import * as grunt from 'grunt';
import * as _ from 'lodash';
import * as path from 'path';
import * as sinon from 'sinon';
import {Context} from '../../runner/context';
import * as steps from '../../runner/steps';
const wctLocalBrowsers = require('wct-local/lib/browsers');
const expect = chai.expect;
chai.use(require('sinon-chai'));
const LOCAL_BROWSERS = {
aurora: {browserName: 'aurora', version: '1'},
canary: {browserName: 'canary', version: '2'},
chrome: {browserName: 'chrome', version: '3'},
firefox: {browserName: 'firefox', version: '4'},
};
describe('grunt', function() {
// Sinon doesn't stub process.env very well.
let origEnv: any, origArgv: any;
beforeEach(function() {
origEnv = _.clone(process.env);
origArgv = process.argv;
});
afterEach(function() {
_.assign(process.env, origEnv);
_.difference(_.keys(process.env), _.keys(origEnv)).forEach(function(key) {
delete process.env[key];
});
process.argv = origArgv;
});
before(function() {
grunt.initConfig({
'wct-test': {
'passthrough': {
options: {foo: 1, bar: 'asdf'},
},
'override': {
options: {sauce: {username: '--real-sauce--'}},
},
},
});
grunt.loadTasks(path.resolve(__dirname, '../../tasks'));
});
async function runTask(task: string) {
await new Promise((resolve, reject) => {
grunt.task['options']({error: reject, done: resolve});
grunt.task.run('wct-test:' + task)['start']();
});
// We shouldn't error before hitting it.
expect(steps.runTests).to.have.been.calledOnce;
return <{args: [Context]}>steps.runTests['getCall'](0);
}
describe('wct-test', function() {
let sandbox: sinon.SinonSandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.stub(steps, 'prepare')
.callsFake(
async(_context: Context): Promise => undefined);
sandbox.stub(wctLocalBrowsers, 'detect').callsFake(async () => LOCAL_BROWSERS);
sandbox.stub(wctLocalBrowsers, 'supported').callsFake(() => _.keys(LOCAL_BROWSERS));
process.chdir(path.resolve(__dirname, '../fixtures/cli/standard'));
});
afterEach(function() {
sandbox.restore();
});
describe('with a passing suite', function() {
beforeEach(function() {
sandbox.stub(steps, 'runTests').callsFake(async(): Promise => undefined);
});
it('passes configuration through', async () => {
const call = await runTask('passthrough');
expect(call.args[0].options).to.include({foo: 1, bar: 'asdf'});
});
});
describe('with a failing suite', function() {
beforeEach(function() {
sandbox.stub(steps, 'runTests').callsFake(async () => {
throw 'failures';
});
});
it('passes errors out', async () => {
try {
await runTask('passthrough');
} catch (error) {
return; // All's well!
}
throw new Error('Expected runTask to fail!');
});
});
});
});
================================================
FILE: test/unit/gulp.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chai from 'chai';
import * as gulp from 'gulp';
import * as path from 'path';
import * as sinon from 'sinon';
import {Config} from '../../runner/config';
import {Context} from '../../runner/context';
import * as wctGulp from '../../runner/gulp';
import {Plugin} from '../../runner/plugin';
import * as steps from '../../runner/steps';
const expect = chai.expect;
chai.use(require('sinon-chai'));
const FIXTURES = path.resolve(__dirname, '../fixtures/cli');
describe('gulp', function() {
let pluginsCalled: string[];
let sandbox: sinon.SinonSandbox;
let orch: gulp.Gulp;
let options: Config;
beforeEach(function() {
orch = new gulp['Gulp']();
wctGulp.init(orch);
sandbox = sinon.sandbox.create();
sandbox.stub(
steps, 'prepare').callsFake(async(_context: Context): Promise => undefined);
sandbox.stub(steps, 'runTests').callsFake(async (context: Context) => {
options = context.options;
});
pluginsCalled = [];
sandbox.stub(Plugin.prototype, 'execute').callsFake(async function(context: Context) {
pluginsCalled.push(this.name);
context.options.activeBrowsers.push(
{browserName: 'fake for ' + this.name});
});
});
afterEach(function() {
sandbox.restore();
});
async function runGulpTask(name: string) {
await new Promise((resolve, reject) => {
orch.start(name, (error) => error ? reject(error) : resolve());
});
}
it('honors wcf.conf.js', async () => {
process.chdir(path.join(FIXTURES, 'conf'));
await runGulpTask('wct:sauce');
expect(options.plugins['sauce'].username).to.eq('abc123');
});
it('prefers wcf.conf.json', async () => {
process.chdir(path.join(FIXTURES, 'conf', 'json'));
await runGulpTask('wct:sauce');
expect(options.plugins['sauce'].username).to.eq('jsonconf');
});
describe('wct:local', function() {
it('kicks off local tests', async () => {
await runGulpTask('wct:local');
expect(steps.runTests).to.have.been.calledOnce;
expect(pluginsCalled).to.have.members(['local']);
});
});
describe('wct:sauce', function() {
it('kicks off sauce tests', async () => {
await runGulpTask('wct:sauce');
expect(steps.runTests).to.have.been.calledOnce;
expect(pluginsCalled).to.have.members(['sauce']);
});
});
});
================================================
FILE: test/unit/paths.ts
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {expect} from 'chai';
import * as path from 'path';
import * as paths from '../../runner/paths';
describe('paths', function() {
describe('.expand', function() {
const baseDir = path.resolve(__dirname, '../fixtures/paths');
async function expectExpands(patterns: string[], expected: string[]) {
const actual = await paths.expand(baseDir, patterns);
// for non-POSIX support
expected = expected.map((str) => str.replace(/\//g, path.sep));
expect(actual).to.have.members(expected);
}
it('is ok with an empty list', async () => {
await expectExpands([], []);
});
it('ignores explicit files that are missing', async () => {
await expectExpands(['404.js'], []);
await expectExpands(['404.js', 'foo.html'], ['foo.html']);
});
it('does not expand explicit files', async () => {
await expectExpands(['foo.js'], ['foo.js']);
await expectExpands(['foo.html'], ['foo.html']);
await expectExpands(['foo.js', 'foo.html'], ['foo.js', 'foo.html']);
});
it('expands directories into their files', async () => {
await expectExpands(['foo'], ['foo/one.js', 'foo/two.html']);
await expectExpands(['foo/'], ['foo/one.js', 'foo/two.html']);
});
it('expands directories into index.html when present', async () => {
await expectExpands(['bar'], ['bar/index.html']);
await expectExpands(['bar/'], ['bar/index.html']);
});
it('expands directories recursively, honoring all rules', async () => {
await expectExpands(['baz'], [
'baz/a/fizz.html',
'baz/b/index.html',
'baz/a.html',
'baz/b.js',
]);
});
it('accepts globs for explicit file matches', async () => {
await expectExpands(['baz/*.js'], ['baz/b.js']);
await expectExpands(['baz/*.html'], ['baz/a.html']);
await expectExpands(['baz/**/*.js'], [
'baz/b/deep/stuff.js',
'baz/b/one.js',
'baz/b.js',
]);
await expectExpands(['baz/**/*.html'], [
'baz/a/fizz.html',
'baz/b/deep/index.html',
'baz/b/deep/stuff.html',
'baz/b/index.html',
'baz/a.html',
]);
});
it('accepts globs for directories, honoring directory behavior',
async () => {
await expectExpands(['*'], [
'bar/index.html',
'baz/a/fizz.html',
'baz/b/index.html',
'baz/a.html',
'baz/b.js',
'foo/one.js',
'foo/two.html',
'foo.html',
'foo.js',
]);
await expectExpands(['baz/*'], [
'baz/a/fizz.html',
'baz/b/index.html',
'baz/a.html',
'baz/b.js',
]);
});
it('deduplicates', async () => {
await expectExpands(['bar/a.js', 'bar/*.js', 'bar', 'bar/*.html'], [
'bar/a.js',
'bar/index.html',
'bar/index.js',
]);
});
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"isolatedModules": false,
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noImplicitThis": false,
"strictNullChecks": false,
"removeComments": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": true,
"lib": ["es2017"],
"declaration": true,
"sourceMap": true,
"pretty": true
},
"exclude": [
"node_modules"
],
"include": [
"runner/*.ts",
"test/**/*.ts",
"custom_typings/*.d.ts",
"node_modules/@types/mocha/index.d.ts"
]
}
================================================
FILE: tslint.json
================================================
{
"rules": {
"arrow-parens": true,
"class-name": true,
"indent": [
true,
"spaces"
],
"prefer-const": true,
"no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-var-keyword": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [
true,
"always"
],
"trailing-comma": [
true,
"multiline"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}
================================================
FILE: wct-browser-legacy/a11ySuite.js
================================================
import * as polymerDom from '../@polymer/polymer/lib/legacy/polymer.dom.js';
const Polymer = { dom: polymerDom };
export {a11ySuiteExport as a11ySuite};
// wct-browser-legacy/a11ySuite.js is a generated file. Source is in web-component-tester/data/a11ySuite.js
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
var a11ySuiteExport;
(function(Mocha, chai, axs) {
Object.keys(Mocha.interfaces).forEach(function(iface) {
var orig = Mocha.interfaces[iface];
Mocha.interfaces[iface] = function(suite) {
orig.apply(this, arguments);
var Suite = Mocha.Suite;
var Test = Mocha.Test;
suite.on('pre-require', function(context, file, mocha) {
/**
* Runs the Chrome Accessibility Developer Tools Audit against a test-fixture
*
* @param {String} fixtureId ID of the fixture element in the document to use
* @param {Array?} ignoredRules Array of rules to ignore for this suite
* @param {Function?} beforeEach Function to be called before each test to ensure proper setup
*/
a11ySuiteExport = context.a11ySuite = function(fixtureId, ignoredRules, beforeEach) {
// capture a reference to the fixture element early
var fixtureElement = document.getElementById(fixtureId);
if (!fixtureElement) {
return;
}
// build an audit config to disable certain ignorable tests
var axsConfig = new axs.AuditConfiguration();
axsConfig.scope = document.body;
axsConfig.showUnsupportedRulesWarning = false;
axsConfig.auditRulesToIgnore = ignoredRules;
// build mocha suite
var a11ySuite = Suite.create(suite, 'A11y Audit - Fixture: ' + fixtureId);
// override the `eachTest` function to hackily create the tests
//
// eachTest is called right before test runs to calculate the total
// number of tests
a11ySuite.eachTest = function() {
// instantiate fixture
fixtureElement.create();
// Make sure lazy-loaded dom is ready (eg )
Polymer.dom.flush();
// If we have a beforeEach function, call it
if (beforeEach) {
beforeEach();
}
// run audit
var auditResults = axs.Audit.run(axsConfig);
// create tests for audit results
auditResults.forEach(function(result, index) {
// only show applicable tests
if (result.result !== 'NA') {
var title = result.rule.heading;
// fail test if audit result is FAIL
var error = result.result === 'FAIL' ? axs.Audit.accessibilityErrorMessage(result) : null;
var test = new Test(title, function() {
if (error) {
throw new Error(error);
}
});
test.file = file;
a11ySuite.addTest(test);
}
});
// teardown fixture
fixtureElement.restore();
suite.eachTest.apply(a11ySuite, arguments);
this.eachTest = suite.eachTest;
};
return a11ySuite;
};
});
};
});
chai.use(function(chai, util) {
var Assertion = chai.Assertion;
// assert
chai.assert.a11yLabel = function(node, exp, msg){
new Assertion(node).to.have.a11yLabel(exp, msg);
};
// expect / should
Assertion.addMethod('a11yLabel', function(str, msg) {
if (msg) {
util.flag(this, 'message', msg);
}
var node = this._obj;
// obj must be a Node
new Assertion(node).to.be.instanceOf(Node);
// vind the text alternative with the help of accessibility dev tools
var textAlternative = axs.properties.findTextAlternatives(node, {});
this.assert(
textAlternative === str,
'expected #{this} to have text alternative #{exp} but got #{act}',
'expected #{this} to not have text alternative #{act}',
str,
textAlternative,
true
);
});
});
})(window.Mocha, window.chai, window.axs);
================================================
FILE: wct-browser-legacy/browser.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* THIS FILE IS AUTOMATICALLY GENERATED!
* To make changes to browser.js, please edit the source files in the repo's `browser/` directory!
*/
(function () {
'use strict';
window.__wctUseNpm = true;
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
// Make sure that we use native timers, in case they're being stubbed out.
var nativeSetInterval = window.setInterval;
var nativeSetTimeout = window.setTimeout;
var nativeRequestAnimationFrame = window.requestAnimationFrame;
/**
* Runs `stepFn`, catching any error and passing it to `callback` (Node-style).
* Otherwise, calls `callback` with no arguments on success.
*
* @param {function()} callback
* @param {function()} stepFn
*/
function safeStep(callback, stepFn) {
var err;
try {
stepFn();
}
catch (error) {
err = error;
}
callback(err);
}
/**
* Runs your test at declaration time (before Mocha has begun tests). Handy for
* when you need to test document initialization.
*
* Be aware that any errors thrown asynchronously cannot be tied to your test.
* You may want to catch them and pass them to the done event, instead. See
* `safeStep`.
*
* @param {string} name The name of the test.
* @param {function(?function())} testFn The test function. If an argument is
* accepted, the test will be treated as async, just like Mocha tests.
*/
function testImmediate(name, testFn) {
if (testFn.length > 0) {
return testImmediateAsync(name, testFn);
}
var err;
try {
testFn();
}
catch (error) {
err = error;
}
test(name, function (done) {
done(err);
});
}
/**
* An async-only variant of `testImmediate`.
*
* @param {string} name
* @param {function(?function())} testFn
*/
function testImmediateAsync(name, testFn) {
var testComplete = false;
var err;
test(name, function (done) {
var intervalId = nativeSetInterval(function () {
if (!testComplete)
return;
clearInterval(intervalId);
done(err);
}, 10);
});
try {
testFn(function (error) {
if (error)
err = error;
testComplete = true;
});
}
catch (error) {
err = error;
testComplete = true;
}
}
/**
* Triggers a flush of any pending events, observations, etc and calls you back
* after they have been processed.
*
* @param {function()} callback
*/
function flush(callback) {
// Ideally, this function would be a call to Polymer.dom.flush, but that
// doesn't support a callback yet
// (https://github.com/Polymer/polymer-dev/issues/851),
// ...and there's cross-browser flakiness to deal with.
// Make sure that we're invoking the callback with no arguments so that the
// caller can pass Mocha callbacks, etc.
var done = function done() {
callback();
};
// Because endOfMicrotask is flaky for IE, we perform microtask checkpoints
// ourselves (https://github.com/Polymer/polymer-dev/issues/114):
var isIE = navigator.appName === 'Microsoft Internet Explorer';
if (isIE && window.Platform && window.Platform.performMicrotaskCheckpoint) {
var reallyDone_1 = done;
done = function doneIE() {
Platform.performMicrotaskCheckpoint();
nativeSetTimeout(reallyDone_1, 0);
};
}
// Everyone else gets a regular flush.
var scope;
if (window.Polymer && window.Polymer.dom && window.Polymer.dom.flush) {
scope = window.Polymer.dom;
}
else if (window.Polymer && window.Polymer.flush) {
scope = window.Polymer;
}
else if (window.WebComponents && window.WebComponents.flush) {
scope = window.WebComponents;
}
if (scope) {
scope.flush();
}
// Ensure that we are creating a new _task_ to allow all active microtasks to
// finish (the code you're testing may be using endOfMicrotask, too).
nativeSetTimeout(done, 0);
}
/**
* Advances a single animation frame.
*
* Calls `flush`, `requestAnimationFrame`, `flush`, and `callback` sequentially
* @param {function()} callback
*/
function animationFrameFlush(callback) {
flush(function () {
nativeRequestAnimationFrame(function () {
flush(callback);
});
});
}
/**
* DEPRECATED: Use `flush`.
* @param {function} callback
*/
function asyncPlatformFlush(callback) {
console.warn('asyncPlatformFlush is deprecated in favor of the more terse flush()');
return window.flush(callback);
}
/**
*
*/
function waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime) {
timeoutTime = timeoutTime || Date.now() + (timeout || 1000);
intervalOrMutationEl = intervalOrMutationEl || 32;
try {
fn();
}
catch (e) {
if (Date.now() > timeoutTime) {
throw e;
}
else {
if (typeof intervalOrMutationEl !== 'number') {
intervalOrMutationEl.onMutation(intervalOrMutationEl, function () {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
});
}
else {
nativeSetTimeout(function () {
waitFor(fn, next, intervalOrMutationEl, timeout, timeoutTime);
}, intervalOrMutationEl);
}
return;
}
}
next();
}
window.safeStep = safeStep;
window.testImmediate = testImmediate;
window.testImmediateAsync = testImmediateAsync;
window.flush = flush;
window.animationFrameFlush = animationFrameFlush;
window.asyncPlatformFlush = asyncPlatformFlush;
window.waitFor = waitFor;
/**
* The global configuration state for WCT's browser client.
*/
var _config = {
environmentScripts: !!window.__wctUseNpm ?
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/index.js',
'mocha/mocha.js', 'chai/chai.js', '@polymer/sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js',
'@polymer/test-fixture/test-fixture.js'
] :
[
'stacky/browser.js', 'async/lib/async.js', 'lodash/lodash.js',
'mocha/mocha.js', 'chai/chai.js', 'sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js'
],
environmentImports: !!window.__wctUseNpm ? [] :
['test-fixture/test-fixture.html'],
root: null,
waitForFrameworks: true,
waitFor: null,
numConcurrentSuites: 1,
trackConsoleError: true,
mochaOptions: { timeout: 10 * 1000 },
verbose: false,
};
/**
* Merges initial `options` into WCT's global configuration.
*
* @param {Object} options The options to merge. See `browser/config.js` for a
* reference.
*/
function setup(options) {
var childRunner = ChildRunner.current();
if (childRunner) {
_deepMerge(_config, childRunner.parentScope.WCT._config);
// But do not force the mocha UI
delete _config.mochaOptions.ui;
}
if (options && typeof options === 'object') {
_deepMerge(_config, options);
}
if (!_config.root) {
// Sibling dependencies.
var root = scriptPrefix('browser.js');
_config.root = basePath(root.substr(0, root.length - 1));
if (!_config.root) {
throw new Error('Unable to detect root URL for WCT sources. Please set WCT.root before including browser.js');
}
}
}
/**
* Retrieves a configuration value.
*/
function get(key) {
return _config[key];
}
// Internal
function _deepMerge(target, source) {
Object.keys(source).forEach(function (key) {
if (target[key] !== null && typeof target[key] === 'object' &&
!Array.isArray(target[key])) {
_deepMerge(target[key], source[key]);
}
else {
target[key] = source[key];
}
});
}
/**
* @param {function()} callback A function to call when the active web component
* frameworks have loaded.
*/
function whenFrameworksReady(callback) {
debug('whenFrameworksReady');
var done = function () {
debug('whenFrameworksReady done');
callback();
};
// If webcomponents script is in the document, wait for WebComponentsReady.
if (window.WebComponents && !window.WebComponents.ready) {
debug('WebComponentsReady?');
window.addEventListener('WebComponentsReady', function wcReady() {
window.removeEventListener('WebComponentsReady', wcReady);
debug('WebComponentsReady');
done();
});
}
else {
done();
}
}
/**
* @return {string} ' tests' or ' test'.
*/
function pluralizedStat(count, kind) {
if (count === 1) {
return count + ' ' + kind + ' test';
}
else {
return count + ' ' + kind + ' tests';
}
}
/**
* @param {string} path The URI of the script to load.
* @param {function} done
*/
function loadScript(path, done) {
var script = document.createElement('script');
script.src = path;
if (done) {
script.onload = done.bind(null, null);
script.onerror = done.bind(null, 'Failed to load script ' + script.src);
}
document.head.appendChild(script);
}
/**
* @param {string} path The URI of the stylesheet to load.
* @param {function} done
*/
function loadStyle(path, done) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = path;
if (done) {
link.onload = done.bind(null, null);
link.onerror = done.bind(null, 'Failed to load stylesheet ' + link.href);
}
document.head.appendChild(link);
}
/**
* @param {...*} var_args Logs values to the console when the `debug`
* configuration option is true.
*/
function debug() {
var var_args = [];
for (var _i = 0; _i < arguments.length; _i++) {
var_args[_i] = arguments[_i];
}
if (!get('verbose')) {
return;
}
var args = [window.location.pathname].concat(var_args);
(console.debug || console.log).apply(console, args);
}
// URL Processing
/**
* @param {string} url
* @return {{base: string, params: string}}
*/
function parseUrl(url) {
var parts = url.match(/^(.*?)(?:\?(.*))?$/);
return {
base: parts[1],
params: getParams(parts[2] || ''),
};
}
/**
* Expands a URL that may or may not be relative to `base`.
*
* @param {string} url
* @param {string} base
* @return {string}
*/
function expandUrl(url, base) {
if (!base)
return url;
if (url.match(/^(\/|https?:\/\/)/))
return url;
if (base.substr(base.length - 1) !== '/') {
base = base + '/';
}
return base + url;
}
/**
* @param {string=} opt_query A query string to parse.
* @return {!Object>} All params on the URL's query.
*/
function getParams(query) {
query = typeof query === 'string' ? query : window.location.search;
if (query.substring(0, 1) === '?') {
query = query.substring(1);
}
// python's SimpleHTTPServer tacks a `/` on the end of query strings :(
if (query.slice(-1) === '/') {
query = query.substring(0, query.length - 1);
}
if (query === '')
return {};
var result = {};
query.split('&').forEach(function (part) {
var pair = part.split('=');
if (pair.length !== 2) {
console.warn('Invalid URL query part:', part);
return;
}
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
});
return result;
}
/**
* Merges params from `source` into `target` (mutating `target`).
*
* @param {!Object>} target
* @param {!Object>} source
*/
function mergeParams(target, source) {
Object.keys(source).forEach(function (key) {
if (!(key in target)) {
target[key] = [];
}
target[key] = target[key].concat(source[key]);
});
}
/**
* @param {string} param The param to return a value for.
* @return {?string} The first value for `param`, if found.
*/
function getParam(param) {
var params = getParams();
return params[param] ? params[param][0] : null;
}
/**
* @param {!Object>} params
* @return {string} `params` encoded as a URI query.
*/
function paramsToQuery(params) {
var pairs = [];
Object.keys(params).forEach(function (key) {
params[key].forEach(function (value) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
});
});
return (pairs.length > 0) ? ('?' + pairs.join('&')) : '';
}
function getPathName(location) {
return typeof location === 'string' ? location : location.pathname;
}
function basePath(location) {
return getPathName(location).match(/^.*\//)[0];
}
function relativeLocation(location, basePath) {
var path = getPathName(location);
if (path.indexOf(basePath) === 0) {
path = path.substring(basePath.length);
}
return path;
}
function cleanLocation(location) {
var path = getPathName(location);
if (path.slice(-11) === '/index.html') {
path = path.slice(0, path.length - 10);
}
return path;
}
function parallel(runners, maybeLimit, done) {
var limit;
if (typeof maybeLimit !== 'number') {
done = maybeLimit;
limit = 0;
}
else {
limit = maybeLimit;
}
if (!runners.length) {
return done();
}
var called = false;
var total = runners.length;
var numActive = 0;
var numDone = 0;
function runnerDone(error) {
if (called) {
return;
}
numDone = numDone + 1;
numActive = numActive - 1;
if (error || numDone >= total) {
called = true;
done(error);
}
else {
runOne();
}
}
function runOne() {
if (limit && numActive >= limit) {
return;
}
if (!runners.length) {
return;
}
numActive = numActive + 1;
runners.shift()(runnerDone);
}
runners.forEach(runOne);
}
/**
* Finds the directory that a loaded script is hosted on.
*
* @param {string} filename
* @return {string?}
*/
function scriptPrefix(filename) {
var scripts = document.querySelectorAll('script[src*="' + filename + '"]');
if (scripts.length !== 1) {
return null;
}
var script = scripts[0].src;
return script.substring(0, script.indexOf(filename));
}
var util = Object.freeze({
whenFrameworksReady: whenFrameworksReady,
pluralizedStat: pluralizedStat,
loadScript: loadScript,
loadStyle: loadStyle,
debug: debug,
parseUrl: parseUrl,
expandUrl: expandUrl,
getParams: getParams,
mergeParams: mergeParams,
getParam: getParam,
paramsToQuery: paramsToQuery,
basePath: basePath,
relativeLocation: relativeLocation,
cleanLocation: cleanLocation,
parallel: parallel,
scriptPrefix: scriptPrefix
});
/**
* A Mocha suite (or suites) run within a child iframe, but reported as if they
* are part of the current context.
*/
var ChildRunner = /** @class */ (function () {
function ChildRunner(url, parentScope) {
var urlBits = parseUrl(url);
mergeParams(urlBits.params, getParams(parentScope.location.search));
delete urlBits.params.cli_browser_id;
this.url = urlBits.base + paramsToQuery(urlBits.params);
this.parentScope = parentScope;
this.state = 'initializing';
}
/**
* @return {ChildRunner} The `ChildRunner` that was registered for this
* window.
*/
ChildRunner.current = function () {
return ChildRunner.get(window);
};
/**
* @param {!Window} target A window to find the ChildRunner of.
* @param {boolean} traversal Whether this is a traversal from a child window.
* @return {ChildRunner} The `ChildRunner` that was registered for `target`.
*/
ChildRunner.get = function (target, traversal) {
var childRunner = ChildRunner._byUrl[target.location.href];
if (childRunner) {
return childRunner;
}
if (window.parent === window) { // Top window.
if (traversal) {
console.warn('Subsuite loaded but was never registered. This most likely is due to wonky history behavior. Reloading...');
window.location.reload();
}
return null;
}
// Otherwise, traverse.
return window.parent.WCT._ChildRunner.get(target, true);
};
/**
* Loads and runs the subsuite.
*
* @param {function} done Node-style callback.
*/
ChildRunner.prototype.run = function (done) {
debug('ChildRunner#run', this.url);
this.state = 'loading';
this.onRunComplete = done;
this.iframe = document.createElement('iframe');
this.iframe.src = this.url;
this.iframe.classList.add('subsuite');
var container = document.getElementById('subsuites');
if (!container) {
container = document.createElement('div');
container.id = 'subsuites';
document.body.appendChild(container);
}
container.appendChild(this.iframe);
// let the iframe expand the URL for us.
this.url = this.iframe.src;
ChildRunner._byUrl[this.url] = this;
this.timeoutId = setTimeout(this.loaded.bind(this, new Error('Timed out loading ' + this.url)), ChildRunner.loadTimeout);
this.iframe.addEventListener('error', this.loaded.bind(this, new Error('Failed to load document ' + this.url)));
this.iframe.contentWindow.addEventListener('DOMContentLoaded', this.loaded.bind(this, null));
};
/**
* Called when the sub suite's iframe has loaded (or errored during load).
*
* @param {*} error The error that occured, if any.
*/
ChildRunner.prototype.loaded = function (error) {
debug('ChildRunner#loaded', this.url, error);
if (this.iframe.contentWindow == null && error) {
this.signalRunComplete(error);
this.done();
return;
}
// Not all targets have WCT loaded (compatiblity mode)
if (this.iframe.contentWindow.WCT) {
this.share = this.iframe.contentWindow.WCT.share;
}
if (error) {
this.signalRunComplete(error);
this.done();
}
};
/**
* Called in mocha/run.js when all dependencies have loaded, and the child is
* ready to start running tests
*
* @param {*} error The error that occured, if any.
*/
ChildRunner.prototype.ready = function (error) {
debug('ChildRunner#ready', this.url, error);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (error) {
this.signalRunComplete(error);
this.done();
}
};
/**
* Called when the sub suite's tests are complete, so that it can clean up.
*/
ChildRunner.prototype.done = function () {
debug('ChildRunner#done', this.url, arguments);
// make sure to clear that timeout
this.ready();
this.signalRunComplete();
if (!this.iframe)
return;
// Be safe and avoid potential browser crashes when logic attempts to
// interact with the removed iframe.
setTimeout(function () {
this.iframe.parentNode.removeChild(this.iframe);
this.iframe = null;
this.share = null;
}.bind(this), 1);
};
ChildRunner.prototype.signalRunComplete = function (error) {
if (!this.onRunComplete)
return;
this.state = 'complete';
this.onRunComplete(error);
this.onRunComplete = null;
};
// ChildRunners get a pretty generous load timeout by default.
ChildRunner.loadTimeout = 60000;
// We can't maintain properties on iframe elements in Firefox/Safari/???, so
// we track childRunners by URL.
ChildRunner._byUrl = {};
return ChildRunner;
}());
var SOCKETIO_ENDPOINT = window.location.protocol + '//' + window.location.host;
var SOCKETIO_LIBRARY = SOCKETIO_ENDPOINT + '/socket.io/socket.io.js';
/**
* A socket for communication between the CLI and browser runners.
*
* @param {string} browserId An ID generated by the CLI runner.
* @param {!io.Socket} socket The socket.io `Socket` to communicate over.
*/
var CLISocket = /** @class */ (function () {
function CLISocket(browserId, socket) {
this.browserId = browserId;
this.socket = socket;
}
/**
* @param {!Mocha.Runner} runner The Mocha `Runner` to observe, reporting
* interesting events back to the CLI runner.
*/
CLISocket.prototype.observe = function (runner) {
var _this = this;
this.emitEvent('browser-start', {
url: window.location.toString(),
});
// We only emit a subset of events that we care about, and follow a more
// general event format that is hopefully applicable to test runners beyond
// mocha.
//
// For all possible mocha events, see:
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36
runner.on('test', function (test) {
_this.emitEvent('test-start', { test: getTitles(test) });
});
runner.on('test end', function (test) {
_this.emitEvent('test-end', {
state: getState(test),
test: getTitles(test),
duration: test.duration,
error: test.err,
});
});
runner.on('fail', function (test, err) {
// fail the test run if we catch errors outside of a test function
if (test.type !== 'test') {
_this.emitEvent('browser-fail', 'Error thrown outside of test function: ' + err.stack);
}
});
runner.on('childRunner start', function (childRunner) {
_this.emitEvent('sub-suite-start', childRunner.share);
});
runner.on('childRunner end', function (childRunner) {
_this.emitEvent('sub-suite-end', childRunner.share);
});
runner.on('end', function () {
_this.emitEvent('browser-end');
});
};
/**
* @param {string} event The name of the event to fire.
* @param {*} data Additional data to pass with the event.
*/
CLISocket.prototype.emitEvent = function (event, data) {
this.socket.emit('client-event', {
browserId: this.browserId,
event: event,
data: data,
});
};
/**
* Builds a `CLISocket` if we are within a CLI-run environment; short-circuits
* otherwise.
*
* @param {function(*, CLISocket)} done Node-style callback.
*/
CLISocket.init = function (done) {
var browserId = getParam('cli_browser_id');
if (!browserId)
return done();
// Only fire up the socket for root runners.
if (ChildRunner.current())
return done();
loadScript(SOCKETIO_LIBRARY, function (error) {
if (error)
return done(error);
var socket = io(SOCKETIO_ENDPOINT);
socket.on('error', function (error) {
socket.off();
done(error);
});
socket.on('connect', function () {
socket.off();
done(null, new CLISocket(browserId, socket));
});
});
};
return CLISocket;
}());
// Misc Utility
/**
* @param {!Mocha.Runnable} runnable The test or suite to extract titles from.
* @return {!Array.} The titles of the runnable and its parents.
*/
function getTitles(runnable) {
var titles = [];
while (runnable && !runnable.root && runnable.title) {
titles.unshift(runnable.title);
runnable = runnable.parent;
}
return titles;
}
/**
* @param {!Mocha.Runnable} runnable
* @return {string}
*/
function getState(runnable) {
if (runnable.state === 'passed') {
return 'passing';
}
else if (runnable.state === 'failed') {
return 'failing';
}
else if (runnable.pending) {
return 'pending';
}
else {
return 'unknown';
}
}
// We capture console events when running tests; so make sure we have a
// reference to the original one.
var console$1 = window.console;
var FONT = ';font: normal 13px "Roboto", "Helvetica Neue", "Helvetica", sans-serif;';
var STYLES = {
plain: FONT,
suite: 'color: #5c6bc0' + FONT,
test: FONT,
passing: 'color: #259b24' + FONT,
pending: 'color: #e65100' + FONT,
failing: 'color: #c41411' + FONT,
stack: 'color: #c41411',
results: FONT + 'font-size: 16px',
};
// I don't think we can feature detect this one...
var userAgent = navigator.userAgent.toLowerCase();
var CAN_STYLE_LOG = userAgent.match('firefox') || userAgent.match('webkit');
var CAN_STYLE_GROUP = userAgent.match('webkit');
// Track the indent for faked `console.group`
var logIndent = '';
function log(text, style) {
text = text.split('\n')
.map(function (l) {
return logIndent + l;
})
.join('\n');
if (CAN_STYLE_LOG) {
console$1.log('%c' + text, STYLES[style] || STYLES.plain);
}
else {
console$1.log(text);
}
}
function logGroup(text, style) {
if (CAN_STYLE_GROUP) {
console$1.group('%c' + text, STYLES[style] || STYLES.plain);
}
else if (console$1.group) {
console$1.group(text);
}
else {
logIndent = logIndent + ' ';
log(text, style);
}
}
function logGroupEnd() {
if (console$1.groupEnd) {
console$1.groupEnd();
}
else {
logIndent = logIndent.substr(0, logIndent.length - 2);
}
}
function logException(error) {
log(error.stack || error.message || (error + ''), 'stack');
}
/**
* A Mocha reporter that logs results out to the web `console`.
*/
var Console = /** @class */ (function () {
/**
* @param runner The runner that is being reported on.
*/
function Console(runner) {
Mocha.reporters.Base.call(this, runner);
runner.on('suite', function (suite) {
if (suite.root) {
return;
}
logGroup(suite.title, 'suite');
}.bind(this));
runner.on('suite end', function (suite) {
if (suite.root) {
return;
}
logGroupEnd();
}.bind(this));
runner.on('test', function (test) {
logGroup(test.title, 'test');
}.bind(this));
runner.on('pending', function (test) {
logGroup(test.title, 'pending');
}.bind(this));
runner.on('fail', function (_test, error) {
logException(error);
}.bind(this));
runner.on('test end', function (_test) {
logGroupEnd();
}.bind(this));
runner.on('end', this.logSummary.bind(this));
}
/** Prints out a final summary of test results. */
Console.prototype.logSummary = function () {
logGroup('Test Results', 'results');
if (this.stats.failures > 0) {
log(pluralizedStat(this.stats.failures, 'failing'), 'failing');
}
if (this.stats.pending > 0) {
log(pluralizedStat(this.stats.pending, 'pending'), 'pending');
}
log(pluralizedStat(this.stats.passes, 'passing'));
if (!this.stats.failures) {
log('test suite passed', 'passing');
}
log('Evaluated ' + this.stats.tests + ' tests in ' +
this.stats.duration + 'ms.');
logGroupEnd();
};
return Console;
}());
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* WCT-specific behavior on top of Mocha's default HTML reporter.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
function HTML(runner) {
var output = document.createElement('div');
output.id = 'mocha';
document.body.appendChild(output);
runner.on('suite', function (_test) {
this.total = runner.total;
}.bind(this));
Mocha.reporters.HTML.call(this, runner);
}
// Woo! What a hack. This just saves us from adding a bunch of complexity around
// style loading.
var style = document.createElement('style');
style.textContent = "\n html, body {\n position: relative;\n height: 100%;\n width: 100%;\n min-width: 900px;\n }\n #mocha, #subsuites {\n height: 100%;\n position: absolute;\n top: 0;\n }\n #mocha {\n box-sizing: border-box;\n margin: 0 !important;\n padding: 60px 20px;\n right: 0;\n left: 500px;\n }\n #subsuites {\n -ms-flex-direction: column;\n -webkit-flex-direction: column;\n display: -ms-flexbox;\n display: -webkit-flex;\n display: flex;\n flex-direction: column;\n left: 0;\n width: 500px;\n }\n #subsuites .subsuite {\n border: 0;\n width: 100%;\n height: 100%;\n }\n #mocha .test.pass .duration {\n color: #555 !important;\n }\n";
document.head.appendChild(style);
var STACKY_CONFIG = {
indent: ' ',
locationStrip: [
/^https?:\/\/[^\/]+/,
/\?.*$/,
],
filter: function (line) {
return !!line.location.match(/\/web-component-tester\/[^\/]+(\?.*)?$/);
},
};
// https://github.com/visionmedia/mocha/blob/master/lib/runner.js#L36-46
var MOCHA_EVENTS = [
'start', 'end', 'suite', 'suite end', 'test', 'test end', 'hook', 'hook end',
'pass', 'fail', 'pending', 'childRunner end'
];
// Until a suite has loaded, we assume this many tests in it.
var ESTIMATED_TESTS_PER_SUITE = 3;
/**
* A Mocha-like reporter that combines the output of multiple Mocha suites.
*/
var MultiReporter = /** @class */ (function () {
/**
* @param numSuites The number of suites that will be run, in order to
* estimate the total number of tests that will be performed.
* @param reporters The set of reporters that
* should receive the unified event stream.
* @param parent The parent reporter, if present.
*/
function MultiReporter(numSuites, reporters, parent) {
var _this = this;
this.reporters = reporters.map(function (reporter) {
return new reporter(_this);
});
this.parent = parent;
this.basePath = parent && parent.basePath || basePath(window.location);
this.total = numSuites * ESTIMATED_TESTS_PER_SUITE;
// Mocha reporters assume a stream of events, so we have to be careful to
// only report on one runner at a time...
this.currentRunner = null;
// ...while we buffer events for any other active runners.
this.pendingEvents = [];
this.emit('start');
}
/**
* @param location The location this reporter represents.
* @return A reporter-like "class" for each child suite
* that should be passed to `mocha.run`.
*/
MultiReporter.prototype.childReporter = function (location) {
var name = this.suiteTitle(location);
// The reporter is used as a constructor, so we can't depend on `this` being
// properly bound.
var self = this;
return _a = /** @class */ (function () {
function ChildReporter(runner) {
runner.name = window.name;
self.bindChildRunner(runner);
}
return ChildReporter;
}()),
_a.title = window.name,
_a;
var _a;
};
/** Must be called once all runners have finished. */
MultiReporter.prototype.done = function () {
this.complete = true;
this.flushPendingEvents();
this.emit('end');
};
/**
* Emit a top level test that is not part of any suite managed by this
* reporter.
*
* Helpful for reporting on global errors, loading issues, etc.
*
* @param title The title of the test.
* @param error An error associated with this test. If falsy, test is
* considered to be passing.
* @param suiteTitle Title for the suite that's wrapping the test.
* @param estimated If this test was included in the original
* estimate of `numSuites`.
*/
MultiReporter.prototype.emitOutOfBandTest = function (title, error, suiteTitle, estimated) {
debug('MultiReporter#emitOutOfBandTest(', arguments, ')');
var root = new Mocha.Suite(suiteTitle || '');
var test = new Mocha.Test(title, function () { });
test.parent = root;
test.state = error ? 'failed' : 'passed';
test.err = error;
if (!estimated) {
this.total = this.total + ESTIMATED_TESTS_PER_SUITE;
}
var runner = { total: 1 };
this.proxyEvent('start', runner);
this.proxyEvent('suite', runner, root);
this.proxyEvent('test', runner, test);
if (error) {
this.proxyEvent('fail', runner, test, error);
}
else {
this.proxyEvent('pass', runner, test);
}
this.proxyEvent('test end', runner, test);
this.proxyEvent('suite end', runner, root);
this.proxyEvent('end', runner);
};
/**
* @param {!Location|string} location
* @return {string}
*/
MultiReporter.prototype.suiteTitle = function (location) {
var path = relativeLocation(location, this.basePath);
path = cleanLocation(path);
return path;
};
// Internal Interface
/** @param {!Mocha.runners.Base} runner The runner to listen to events for. */
MultiReporter.prototype.bindChildRunner = function (runner) {
var _this = this;
MOCHA_EVENTS.forEach(function (eventName) {
runner.on(eventName, _this.proxyEvent.bind(_this, eventName, runner));
});
};
/**
* Evaluates an event fired by `runner`, proxying it forward or buffering it.
*
* @param {string} eventName
* @param {!Mocha.runners.Base} runner The runner that emitted this event.
* @param {...*} var_args Any additional data passed as part of the event.
*/
MultiReporter.prototype.proxyEvent = function (eventName, runner) {
var _args = [];
for (var _i = 2; _i < arguments.length; _i++) {
_args[_i - 2] = arguments[_i];
}
var extraArgs = Array.prototype.slice.call(arguments, 2);
if (this.complete) {
console.warn('out of order Mocha event for ' + runner.name + ':', eventName, extraArgs);
return;
}
if (this.currentRunner && runner !== this.currentRunner) {
this.pendingEvents.push(Array.prototype.slice.call(arguments));
return;
}
debug('MultiReporter#proxyEvent(', arguments, ')');
// This appears to be a Mocha bug: Tests failed by passing an error to their
// done function don't set `err` properly.
//
// TODO(nevir): Track down.
if (eventName === 'fail' && !extraArgs[0].err) {
extraArgs[0].err = extraArgs[1];
}
if (eventName === 'start') {
this.onRunnerStart(runner);
}
else if (eventName === 'end') {
this.onRunnerEnd(runner);
}
else {
this.cleanEvent(eventName, runner, extraArgs);
this.emit.apply(this, [eventName].concat(extraArgs));
}
};
/**
* Cleans or modifies an event if needed.
*
* @param eventName
* @param runner The runner that emitted this event.
* @param extraArgs
*/
MultiReporter.prototype.cleanEvent = function (eventName, _runner, extraArgs) {
// Suite hierarchy
if (extraArgs[0]) {
extraArgs[0] = this.showRootSuite(extraArgs[0]);
}
// Normalize errors
if (eventName === 'fail') {
extraArgs[1] = Stacky.normalize(extraArgs[1], STACKY_CONFIG);
}
if (extraArgs[0] && extraArgs[0].err) {
extraArgs[0].err = Stacky.normalize(extraArgs[0].err, STACKY_CONFIG);
}
};
/**
* We like to show the root suite's title, which requires a little bit of
* trickery in the suite hierarchy.
*
* @param {!Mocha.Runnable} node
*/
MultiReporter.prototype.showRootSuite = function (node) {
var leaf = node = Object.create(node);
while (node && node.parent) {
var wrappedParent = Object.create(node.parent);
node.parent = wrappedParent;
node = wrappedParent;
}
node.root = false;
return leaf;
};
/** @param {!Mocha.runners.Base} runner */
MultiReporter.prototype.onRunnerStart = function (runner) {
debug('MultiReporter#onRunnerStart:', runner.name);
this.total = this.total - ESTIMATED_TESTS_PER_SUITE + runner.total;
this.currentRunner = runner;
};
/** @param {!Mocha.runners.Base} runner */
MultiReporter.prototype.onRunnerEnd = function (runner) {
debug('MultiReporter#onRunnerEnd:', runner.name);
this.currentRunner = null;
this.flushPendingEvents();
};
/**
* Flushes any buffered events and runs them through `proxyEvent`. This will
* loop until all buffered runners are complete, or we have run out of
* buffered events.
*/
MultiReporter.prototype.flushPendingEvents = function () {
var _this = this;
var events = this.pendingEvents;
this.pendingEvents = [];
events.forEach(function (eventArgs) {
_this.proxyEvent.apply(_this, eventArgs);
});
};
return MultiReporter;
}());
var ARC_OFFSET = 0; // start at the right.
var ARC_WIDTH = 6;
/**
* A Mocha reporter that updates the document's title and favicon with
* at-a-glance stats.
*
* @param {!Mocha.Runner} runner The runner that is being reported on.
*/
var Title = /** @class */ (function () {
function Title(runner) {
Mocha.reporters.Base.call(this, runner);
runner.on('test end', this.report.bind(this));
}
/** Reports current stats via the page title and favicon. */
Title.prototype.report = function () {
this.updateTitle();
this.updateFavicon();
};
/** Updates the document title with a summary of current stats. */
Title.prototype.updateTitle = function () {
if (this.stats.failures > 0) {
document.title = pluralizedStat(this.stats.failures, 'failing');
}
else {
document.title = pluralizedStat(this.stats.passes, 'passing');
}
};
/** Updates the document's favicon w/ a summary of current stats. */
Title.prototype.updateFavicon = function () {
var canvas = document.createElement('canvas');
canvas.height = canvas.width = 32;
var context = canvas.getContext('2d');
var passing = this.stats.passes;
var pending = this.stats.pending;
var failing = this.stats.failures;
var total = Math.max(this.runner.total, passing + pending + failing);
drawFaviconArc(context, total, 0, passing, '#0e9c57');
drawFaviconArc(context, total, passing, pending, '#f3b300');
drawFaviconArc(context, total, pending + passing, failing, '#ff5621');
this.setFavicon(canvas.toDataURL());
};
/** Sets the current favicon by URL. */
Title.prototype.setFavicon = function (url) {
var current = document.head.querySelector('link[rel="icon"]');
if (current) {
document.head.removeChild(current);
}
var link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = url;
link.setAttribute('sizes', '32x32');
document.head.appendChild(link);
};
return Title;
}());
/**
* Draws an arc for the favicon status, relative to the total number of tests.
*/
function drawFaviconArc(context, total, start, length, color) {
var arcStart = ARC_OFFSET + Math.PI * 2 * (start / total);
var arcEnd = ARC_OFFSET + Math.PI * 2 * ((start + length) / total);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = ARC_WIDTH;
context.arc(16, 16, 16 - ARC_WIDTH / 2, arcStart, arcEnd);
context.stroke();
}
var htmlSuites$1 = [];
var jsSuites$1 = [];
// We process grep ourselves to avoid loading suites that will be filtered.
var GREP = getParam('grep');
// work around mocha bug (https://github.com/mochajs/mocha/issues/2070)
if (GREP) {
GREP = GREP.replace(/\\\./g, '.');
}
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param files The files to load.
*/
function loadSuites(files) {
files.forEach(function (file) {
if (/\.js(\?.*)?$/.test(file)) {
jsSuites$1.push(file);
}
else if (/\.html(\?.*)?$/.test(file)) {
htmlSuites$1.push(file);
}
else {
throw new Error('Unknown resource type: ' + file);
}
});
}
/**
* @return The child suites that should be loaded, ignoring
* those that would not match `GREP`.
*/
function activeChildSuites() {
var subsuites = htmlSuites$1;
if (GREP) {
var cleanSubsuites = [];
for (var i = 0, subsuite = void 0; subsuite = subsuites[i]; i++) {
if (GREP.indexOf(cleanLocation(subsuite)) !== -1) {
cleanSubsuites.push(subsuite);
}
}
subsuites = cleanSubsuites;
}
return subsuites;
}
/**
* Loads all `.js` sources requested by the current suite.
*/
function loadJsSuites(_reporter, done) {
debug('loadJsSuites', jsSuites$1);
var loaders = jsSuites$1.map(function (file) {
// We only support `.js` dependencies for now.
return loadScript.bind(util, file);
});
parallel(loaders, done);
}
function runSuites(reporter, childSuites, done) {
debug('runSuites');
var suiteRunners = [
// Run the local tests (if any) first, not stopping on error;
_runMocha.bind(null, reporter),
];
// As well as any sub suites. Again, don't stop on error.
childSuites.forEach(function (file) {
suiteRunners.push(function (next) {
var childRunner = new ChildRunner(file, window);
reporter.emit('childRunner start', childRunner);
childRunner.run(function (error) {
reporter.emit('childRunner end', childRunner);
if (error)
reporter.emitOutOfBandTest(file, error);
next();
});
});
});
parallel(suiteRunners, get('numConcurrentSuites'), function (error) {
reporter.done();
done(error);
});
}
/**
* Kicks off a mocha run, waiting for frameworks to load if necessary.
*
* @param {!MultiReporter} reporter Where to send Mocha's events.
* @param {function} done A callback fired, _no error is passed_.
*/
function _runMocha(reporter, done, waited) {
if (get('waitForFrameworks') && !waited) {
var waitFor = (get('waitFor') || whenFrameworksReady).bind(window);
waitFor(_runMocha.bind(null, reporter, done, true));
return;
}
debug('_runMocha');
var mocha = window.mocha;
var Mocha = window.Mocha;
mocha.reporter(reporter.childReporter(window.location));
mocha.suite.title = reporter.suiteTitle(window.location);
mocha.grep(GREP);
// We can't use `mocha.run` because it bashes over grep, invert, and friends.
// See https://github.com/visionmedia/mocha/blob/master/support/tail.js#L137
var runner = Mocha.prototype.run.call(mocha, function (_error) {
if (document.getElementById('mocha')) {
Mocha.utils.highlightTags('code');
}
done(); // We ignore the Mocha failure count.
});
// Mocha's default `onerror` handling strips the stack (to support really old
// browsers). We upgrade this to get better stacks for async errors.
//
// TODO(nevir): Can we expand support to other browsers?
if (navigator.userAgent.match(/chrome/i)) {
window.onerror = null;
window.addEventListener('error', function (event) {
if (!event.error)
return;
if (event.error.ignore)
return;
runner.uncaught(event.error);
});
}
}
/**
* @param {CLISocket} socket The CLI socket, if present.
* @param {MultiReporter} parent The parent reporter, if present.
* @return {!Array. 0 || jsSuites$1.length > 0) {
reporters.push(HTML);
}
return reporters;
}
/**
* Yeah, hideous, but this allows us to be loaded before Mocha, which is handy.
*/
function injectMocha(Mocha) {
_injectPrototype(Console, Mocha.reporters.Base.prototype);
_injectPrototype(HTML, Mocha.reporters.HTML.prototype);
// Mocha doesn't expose its `EventEmitter` shim directly, so:
_injectPrototype(MultiReporter, Object.getPrototypeOf(Mocha.Runner.prototype));
}
function _injectPrototype(klass, prototype) {
var newPrototype = Object.create(prototype);
// Only support
Object.keys(klass.prototype).forEach(function (key) {
newPrototype[key] = klass.prototype[key];
});
klass.prototype = newPrototype;
}
/**
* Loads all environment scripts ...synchronously ...after us.
*/
function loadSync() {
debug('Loading environment scripts:');
var a11ySuiteScriptPath = 'web-component-tester/data/a11ySuite.js';
var scripts = get('environmentScripts');
var a11ySuiteWillBeLoaded = window.__generatedByWct || scripts.indexOf(a11ySuiteScriptPath) > -1;
// We can't inject a11ySuite when running the npm version because it is a
// module-based script that needs `'); // jshint ignore:line
});
debug('Environment scripts loaded');
var imports = get('environmentImports');
imports.forEach(function (path) {
var url = expandUrl(path, get('root'));
debug('Loading environment import:', url);
// Synchronous load.
document.write(''); // jshint ignore:line
});
debug('Environment imports loaded');
}
/**
* We have some hard dependencies on things that should be loaded via
* `environmentScripts`, so we assert that they're present here; and do any
* post-facto setup.
*/
function ensureDependenciesPresent() {
_ensureMocha();
_checkChai();
}
function _ensureMocha() {
var Mocha = window.Mocha;
if (!Mocha) {
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/browser.js');
}
injectMocha(Mocha);
// Magic loading of mocha's stylesheet
var mochaPrefix = scriptPrefix('mocha.js');
// only load mocha stylesheet for the test runner output
// Not the end of the world, if it doesn't load.
if (mochaPrefix && window.top === window.self) {
loadStyle(mochaPrefix + 'mocha.css');
}
}
function _checkChai() {
if (!window.chai) {
debug('Chai not present; not registering shorthands');
return;
}
window.assert = window.chai.assert;
window.expect = window.chai.expect;
}
// We may encounter errors during initialization (for example, syntax errors in
// a test file). Hang onto those (and more) until we are ready to report them.
var globalErrors = [];
/**
* Hook the environment to pick up on global errors.
*/
function listenForErrors() {
window.addEventListener('error', function (event) {
globalErrors.push(event.error);
});
// Also, we treat `console.error` as a test failure. Unless you prefer not.
var origConsole = console;
var origError = console.error;
console.error = function wctShimmedError() {
origError.apply(origConsole, arguments);
if (get('trackConsoleError')) {
throw 'console.error: ' + Array.prototype.join.call(arguments, ' ');
}
};
}
var interfaceExtensions = [];
/**
* Registers an extension that extends the global `Mocha` implementation
* with new helper methods. These helper methods will be added to the `window`
* when tests run for both BDD and TDD interfaces.
*/
function extendInterfaces(helperName, helperFactory) {
interfaceExtensions.push(function () {
var Mocha = window.Mocha;
// For all Mocha interfaces (probably just TDD and BDD):
Object.keys(Mocha.interfaces)
.forEach(function (interfaceName) {
// This is the original callback that defines the interface (TDD or
// BDD):
var originalInterface = Mocha.interfaces[interfaceName];
// This is the name of the "teardown" or "afterEach" property for the
// current interface:
var teardownProperty = interfaceName === 'tdd' ? 'teardown' : 'afterEach';
// The original callback is monkey patched with a new one that appends
// to the global context however we want it to:
Mocha.interfaces[interfaceName] = function (suite) {
// Call back to the original callback so that we get the base
// interface:
originalInterface.apply(this, arguments);
// Register a listener so that we can further extend the base
// interface:
suite.on('pre-require', function (context, _file, _mocha) {
// Capture a bound reference to the teardown function as a
// convenience:
var teardown = context[teardownProperty].bind(context);
// Add our new helper to the testing context. The helper is
// generated by a factory method that receives the context,
// the teardown function and the interface name and returns
// the new method to be added to that context:
context[helperName] =
helperFactory(context, teardown, interfaceName);
});
};
});
});
}
/**
* Applies any registered interface extensions. The extensions will be applied
* as many times as this function is called, so don't call it more than once.
*/
function applyExtensions() {
interfaceExtensions.forEach(function (applyExtension) {
applyExtension();
});
}
extendInterfaces('fixture', function (context, teardown) {
// Return context.fixture if it is already a thing, for backwards
// compatibility with `test-fixture-mocha.js`:
return context.fixture || function fixture(fixtureId, model) {
// Automatically register a teardown callback that will restore the
// test-fixture:
teardown(function () {
document.getElementById(fixtureId).restore();
});
// Find the test-fixture with the provided ID and create it, returning
// the results:
return document.getElementById(fixtureId).create(model);
};
});
/**
* stub
*
* The stub addon allows the tester to partially replace the implementation of
* an element with some custom implementation. Usage example:
*
* beforeEach(function() {
* stub('x-foo', {
* attached: function() {
* // Custom implementation of the `attached` method of element `x-foo`..
* },
* otherMethod: function() {
* // More custom implementation..
* },
* getterSetterProperty: {
* get: function() {
* // Custom getter implementation..
* },
* set: function() {
* // Custom setter implementation..
* }
* },
* // etc..
* });
* });
*/
extendInterfaces('stub', function (_context, teardown) {
return function stub(tagName, implementation) {
// Find the prototype of the element being stubbed:
var proto = document.createElement(tagName).constructor.prototype;
// For all keys in the implementation to stub with..
var stubs = Object.keys(implementation).map(function (key) {
// Stub the method on the element prototype with Sinon:
return sinon.stub(proto, key, implementation[key]);
});
// After all tests..
teardown(function () {
stubs.forEach(function (stub) {
stub.restore();
});
});
};
});
// replacement map stores what should be
var replacements = {};
var replaceTeardownAttached = false;
/**
* replace
*
* The replace addon allows the tester to replace all usages of one element with
* another element within all Polymer elements created within the time span of
* the test. Usage example:
*
* beforeEach(function() {
* replace('x-foo').with('x-fake-foo');
* });
*
* All annotations and attributes will be set on the placement element the way
* they were set for the original element.
*/
extendInterfaces('replace', function (_context, teardown) {
return function replace(oldTagName) {
return {
with: function (tagName) {
// Standardizes our replacements map
oldTagName = oldTagName.toLowerCase();
tagName = tagName.toLowerCase();
replacements[oldTagName] = tagName;
// If the function is already a stub, restore it to original
if (document.importNode.isSinonProxy) {
return;
}
if (!window.Polymer.Element) {
window.Polymer.Element = function () { };
window.Polymer.Element.prototype._stampTemplate = function () { };
}
// Keep a reference to the original `document.importNode`
// implementation for later:
var originalImportNode = document.importNode;
// Use Sinon to stub `document.ImportNode`:
sinon.stub(document, 'importNode', function (origContent, deep) {
var templateClone = document.createElement('template');
var content = templateClone.content;
var inertDoc = content.ownerDocument;
// imports node from inertDoc which holds inert nodes.
templateClone.content.appendChild(inertDoc.importNode(origContent, true));
// optional arguments are not optional on IE.
var nodeIterator = document.createNodeIterator(content, NodeFilter.SHOW_ELEMENT, null, true);
var node;
// Traverses the tree. A recently-replaced node will be put next,
// so if a node is replaced, it will be checked if it needs to be
// replaced again.
while (node = nodeIterator.nextNode()) {
var currentTagName = node.tagName.toLowerCase();
if (replacements.hasOwnProperty(currentTagName)) {
currentTagName = replacements[currentTagName];
// find the final tag name.
while (replacements[currentTagName]) {
currentTagName = replacements[currentTagName];
}
// Create a replacement:
var replacement = document.createElement(currentTagName);
// For all attributes in the original node..
for (var index = 0; index < node.attributes.length; ++index) {
// Set that attribute on the replacement:
replacement.setAttribute(node.attributes[index].name, node.attributes[index].value);
}
// Replace the original node with the replacement node:
node.parentNode.replaceChild(replacement, node);
}
}
return originalImportNode.call(this, content, deep);
});
if (!replaceTeardownAttached) {
// After each test...
teardown(function () {
replaceTeardownAttached = true;
// Restore the stubbed version of `document.importNode`:
var documentImportNode = document.importNode;
if (documentImportNode.isSinonProxy) {
documentImportNode.restore();
}
// Empty the replacement map
replacements = {};
});
}
}
};
};
});
// Mocha global helpers, broken out by testing method.
//
// Keys are the method for a particular interface; values are their analog in
// the opposite interface.
var MOCHA_EXPORTS = {
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/tdd.js
tdd: {
'setup': '"before"',
'teardown': '"after"',
'suiteSetup': '"beforeEach"',
'suiteTeardown': '"afterEach"',
'suite': '"describe" or "context"',
'test': '"it" or "specify"',
},
// https://github.com/visionmedia/mocha/blob/master/lib/interfaces/bdd.js
bdd: {
'before': '"setup"',
'after': '"teardown"',
'beforeEach': '"suiteSetup"',
'afterEach': '"suiteTeardown"',
'describe': '"suite"',
'context': '"suite"',
'xdescribe': '"suite.skip"',
'xcontext': '"suite.skip"',
'it': '"test"',
'xit': '"test.skip"',
'specify': '"test"',
'xspecify': '"test.skip"',
},
};
/**
* Exposes all Mocha methods up front, configuring and running mocha
* automatically when you call them.
*
* The assumption is that it is a one-off (sub-)suite of tests being run.
*/
function stubInterfaces() {
var keys = Object.keys(MOCHA_EXPORTS);
keys.forEach(function (ui) {
Object.keys(MOCHA_EXPORTS[ui]).forEach(function (key) {
window[key] = function wrappedMochaFunction() {
_setupMocha(ui, key, MOCHA_EXPORTS[ui][key]);
if (!window[key] || window[key] === wrappedMochaFunction) {
throw new Error('Expected mocha.setup to define ' + key);
}
window[key].apply(window, arguments);
};
});
});
}
// Whether we've called `mocha.setup`
var _mochaIsSetup = false;
/**
* @param {string} ui Sets up mocha to run `ui`-style tests.
* @param {string} key The method called that triggered this.
* @param {string} alternate The matching method in the opposite interface.
*/
function _setupMocha(ui, key, alternate) {
var mochaOptions = get('mochaOptions');
if (mochaOptions.ui && mochaOptions.ui !== ui) {
var message = 'Mixing ' + mochaOptions.ui + ' and ' + ui +
' Mocha styles is not supported. ' +
'You called "' + key + '". Did you mean ' + alternate + '?';
throw new Error(message);
}
if (_mochaIsSetup) {
return;
}
applyExtensions();
mochaOptions.ui = ui;
mocha.setup(mochaOptions); // Note that the reporter is configured in run.js.
}
// You can configure WCT before it has loaded by assigning your custom
// configuration to the global `WCT`.
setup(window.WCT);
// Maybe some day we'll expose WCT as a module to whatever module registry you
// are using (aka the UMD approach), or as an es6 module.
var WCT = window.WCT = {
// A generic place to hang data about the current suite. This object is
// reported
// back via the `sub-suite-start` and `sub-suite-end` events.
share: {},
// Until then, we get to rely on it to expose parent runners to their
// children.
_ChildRunner: ChildRunner,
_reporter: undefined,
_config: _config,
// Public API
/**
* Loads suites of tests, supporting both `.js` and `.html` files.
*
* @param {!Array.} files The files to load.
*/
loadSuites: loadSuites,
};
// Load Process
listenForErrors();
stubInterfaces();
loadSync();
// Give any scripts on the page a chance to declare tests and muck with things.
document.addEventListener('DOMContentLoaded', function () {
debug('DOMContentLoaded');
ensureDependenciesPresent();
// We need the socket built prior to building its reporter.
CLISocket.init(function (error, socket) {
if (error)
throw error;
// Are we a child of another run?
var current = ChildRunner.current();
var parent = current && current.parentScope.WCT._reporter;
debug('parentReporter:', parent);
var childSuites = activeChildSuites();
var reportersToUse = determineReporters(socket, parent);
// +1 for any local tests.
var reporter = new MultiReporter(childSuites.length + 1, reportersToUse, parent);
WCT._reporter = reporter; // For environment/compatibility.js
// We need the reporter so that we can report errors during load.
loadJsSuites(reporter, function (error) {
// Let our parent know that we're about to start the tests.
if (current)
current.ready(error);
if (error)
throw error;
// Emit any errors we've encountered up til now
globalErrors.forEach(function onError(error) {
reporter.emitOutOfBandTest('Test Suite Initialization', error);
});
runSuites(reporter, childSuites, function (error) {
// Make sure to let our parent know that we're done.
if (current)
current.done();
if (error)
throw error;
});
});
});
});
}());
//# sourceMappingURL=browser.js.map
================================================
FILE: wct-browser-legacy/package.json
================================================
{
"name": "wct-browser-legacy",
"version": "0.0.1-pre.12",
"description": "Client-side dependencies for web-component-tester tests installed via npm.",
"main": "browser.js",
"license": "http://polymer.github.io/LICENSE.txt",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/polymer/web-component-tester.git"
},
"keywords": [
"browser",
"gulp",
"polymer",
"test",
"testing",
"web",
"web component"
],
"bugs": {
"url": "https://github.com/polymer/web-component-tester/issues"
},
"homepage": "https://github.com/polymer/web-component-tester#readme",
"dependencies": {
"@polymer/polymer": "^3.0.0-pre.1",
"@polymer/sinonjs": "^1.14.1",
"@polymer/test-fixture": "^3.0.0-pre.1",
"@webcomponents/webcomponentsjs": "^1.0.7",
"accessibility-developer-tools": "^2.12.0",
"async": "^1.5.2",
"chai": "^3.5.0",
"lodash": "^3.10.1",
"mocha": "^3.4.2",
"sinon": "^1.17.1",
"sinon-chai": "^2.10.0",
"stacky": "^1.3.1"
}
}