web-component-tester
Version:
web-component-tester makes testing your web components a breeze!
532 lines (531 loc) • 19.7 kB
JavaScript
;
/**
* @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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const findup = require("findup-sync");
const fs = require("fs");
const _ = require("lodash");
const nomnom = require("nomnom");
const path = require("path");
const resolve = require("resolve");
const paths = require("./paths");
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.*';
/**
* 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, dir) {
const configPath = path.resolve(dir || '', filename);
let config;
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
*/
function getPackageName(options) {
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;
}
exports.getPackageName = getPackageName;
/**
* Return the root package directory of the given NPM package.
*/
function resolvePackageDir(packageName, opts) {
// package.json files are always in the root directory of a package, so we can
// resolve that and then drop the filename.
return path.dirname(resolve.sync(path.join(packageName, 'package.json'), opts));
}
// The full set of options, as a reference.
function defaults() {
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 will be
// determined automatically if no name is specified.
wctPackageName: undefined,
moduleResolution: 'node'
};
}
exports.defaults = defaults;
/**
* 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.',
full: 'simple-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: 'config-file',
},
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 ' +
'code. By default, WCT will detect the installed package, but ' +
'this flag allows explicitly naming the package. ' +
'This is only to be used with --npm option.'
},
// 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'];
/**
* 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.
*/
function fromDisk(matcher, root) {
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((f) => loadProjectFile(f));
const options = merge.apply(null, configs);
if (!options.root && projectFile && projectFile !== globalFile) {
options.root = path.dirname(projectFile);
}
return options;
}
exports.fromDisk = fromDisk;
/**
* @param {string} file
* @return {Object?}
*/
function loadProjectFile(file) {
// 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<string>} args
* @return {!Object}
*/
function preparseArgs(args) {
// 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));
}
exports.preparseArgs = preparseArgs;
/**
* 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<string>} args The args to parse.
*/
function parseArgs(context, args) {
return __awaiter(this, void 0, void 0, function* () {
const parser = nomnom();
parser.script('wct');
parser.options(ARG_CONFIG);
const plugins = yield 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);
});
}
exports.parseArgs = parseArgs;
function _configurePluginOptions(parser, 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);
});
}
function _expandOptionPaths(options) {
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;
}
function merge() {
let configs = Array.prototype.slice.call(arguments);
const result = {};
configs = configs.map(normalize);
_.merge.apply(_, [result, ...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;
}
exports.merge = merge;
function normalize(config) {
if (_.isArray(config.plugins)) {
const pluginConfigs = {};
for (let i = 0, name; 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; name = config.skipPlugins[i]; i++) {
config.plugins[name] = false;
}
}
return config;
}
exports.normalize = normalize;
/**
* Expands values within the configuration based on the current environment.
*
* @param {!Context} context The context for the current run.
*/
function expand(context) {
return __awaiter(this, void 0, void 0, function* () {
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 = yield paths.expand(root, options.suites);
});
}
exports.expand = expand;
/**
* Expands any options that have been deprecated, and warns about it.
*
* @param {!Context} context The context for the current run.
*/
function expandDeprecated(context) {
const options = context.options;
// We collect configuration fragments to be merged into the options object.
const fragments = [];
let browsers = (_.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: { 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.
*/
function validate(options) {
return __awaiter(this, void 0, void 0, function* () {
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);
}
});
}
exports.validate = validate;