testem
Version:
Test'em 'scripts! Javascript Unit testing made easy.
537 lines (468 loc) • 15.4 kB
JavaScript
/*
config.js
=========
This object returns all config info for the app. It handles reading the `testem.yml`
or `testem.json` config file.
*/
'use strict';
const os = require('os');
const fs = require('fs');
const yaml = require('js-yaml');
const log = require('npmlog');
const path = require('path');
const glob = require('glob');
const url = require('url');
const querystring = require('querystring');
const Bluebird = require('bluebird');
const browser_launcher = require('./browser_launcher');
const LauncherFactory = require('./launcher-factory');
const Chars = require('./utils/chars');
const pad = require('./utils/strutils').pad;
const isa = require('./utils/isa');
const fileExists = require('./utils/fileutils').fileExists;
const _ = require('lodash');
const knownBrowsers = require('./utils/known-browsers');
const globAsync = Bluebird.promisify(glob);
class Config {
constructor(appMode, progOptions, config) {
this.appMode = appMode;
this.progOptions = progOptions || {};
this.defaultOptions = {};
this.fileOptions = {};
this.config = config || {};
this.getters = {
test_page: 'getTestPage'
};
if (appMode === 'dev') {
this.progOptions.reporter = 'dev';
this.progOptions.parallel = -1;
}
if (this.progOptions.debug === true) {
this.progOptions.debug = 'testem.log';
}
if (appMode === 'ci') {
this.progOptions.disable_watching = true;
this.progOptions.single_run = true;
}
}
setDefaultOptions(defaultOptions) {
this.defaultOptions = defaultOptions;
}
read(callback) {
let configFile = this.progOptions.file;
if (configFile) {
this.readConfigFile(configFile, callback);
} else {
log.info('Seeking for config file...');
// Try all testem.json, testem.yml and testem.js
// testem.json gets precedence
let files = ['testem.json', '.testem.json', '.testem.yml', 'testem.yml', 'testem.js', '.testem.js', 'testem.cjs', '.testem.cjs'];
return Bluebird.filter(files.map(this.resolveConfigPath.bind(this)), fileExists).then(matched => {
let configFile = matched[0];
if (matched.length > 1) {
let baseNames = matched.map(fileName => path.basename(fileName));
console.warn('Found ' + matched.length + ' config files (' + baseNames + '), using ' + baseNames[0]);
}
if (configFile) {
this.readConfigFile(configFile, callback);
} else {
if (callback) {
callback.call(this);
}
}
});
}
}
resolvePath(filepath) {
if (filepath[0] === '/') {
return filepath;
}
return path.resolve(this.cwd(), filepath);
}
client() {
return {
decycle_depth: this.get('client_decycle_depth')
};
}
resolveConfigPath(filepath) {
if (this.progOptions.config_dir) {
return path.resolve(this.progOptions.config_dir, filepath);
} else if (this.defaultOptions && this.defaultOptions.config_dir) {
return path.resolve(this.defaultOptions.config_dir, filepath);
} else {
return this.resolvePath(filepath);
}
}
reverseResolvePath(filepath) {
return path.relative(this.cwd(), filepath);
}
cwd() {
return this.get('cwd') || process.cwd();
}
readConfigFile(configFile, callback) {
if (!configFile) { // allow empty configFile for programmatic setups
if (callback) {
callback.call(this);
}
} else if (configFile.match(/\.c?js$/)) {
this.readJS(configFile, callback);
} else if (configFile.match(/\.json$/)) {
this.readJSON(configFile, callback);
} else if (configFile.match(/\.yml$/)) {
this.readYAML(configFile, callback);
} else {
log.error('Unrecognized config file format for ' + configFile);
if (callback) {
callback.call(this);
}
}
}
readJS(configFile, callback) {
let exportedConfig = require(this.resolveConfigPath(configFile));
if (typeof exportedConfig === 'function') {
Promise.resolve(exportedConfig()).then(obj => {
this.fileOptions = obj;
if (callback) {
callback.call(this);
}
});
} else {
this.fileOptions = exportedConfig;
if (callback) {
callback.call(this);
}
}
}
readYAML(configFile, callback) {
fs.readFile(configFile, (err, data) => {
if (!err) {
let cfg = yaml.load(String(data));
this.fileOptions = cfg;
}
if (callback) {
callback.call(this);
}
});
}
readJSON(configFile, callback) {
fs.readFile(configFile, (err, data) => {
if (!err) {
let cfg = JSON.parse(data.toString());
this.fileOptions = cfg;
this.progOptions.file = configFile;
}
if (callback) {
callback.call(this);
}
});
}
mergeUrlAndQueryParams(urlString, queryParamsObj) {
if (!queryParamsObj) {
return urlString;
}
if (typeof queryParamsObj === 'string') {
if (queryParamsObj[0] === '?') {
queryParamsObj = queryParamsObj.substr(1);
}
queryParamsObj = querystring.parse(queryParamsObj);
}
let urlObj = url.parse(urlString);
let outputQueryParams = querystring.parse(urlObj.query) || {};
Object.keys(queryParamsObj).forEach(param => {
outputQueryParams[param] = queryParamsObj[param];
});
urlObj.query = outputQueryParams;
urlObj.search = querystring.stringify(outputQueryParams)
.replace(/=&/g, '&')
.replace(/=$/, '');
urlObj.path = urlObj.pathname + urlObj.search;
return url.format(urlObj);
}
getTestPage() {
let testPage = this.getConfigProperty('test_page');
let queryParams = this.getConfigProperty('query_params');
if (!Array.isArray(testPage)) {
testPage = [testPage];
}
return testPage.map(page => this.mergeUrlAndQueryParams(page, queryParams));
}
getConfigProperty(key) {
if (this.config && key in this.config) {
return this.config[key];
}
if (key in this.progOptions && typeof this.progOptions[key] !== 'undefined') {
return this.progOptions[key];
}
if (key in this.fileOptions && typeof this.fileOptions[key] !== 'undefined') {
return this.fileOptions[key];
}
if (this.defaultOptions && key in this.defaultOptions && typeof this.defaultOptions[key] !== 'undefined') {
return this.defaultOptions[key];
}
if (key in this.defaults) {
let defaultVal = this.defaults[key];
if (typeof defaultVal === 'function') {
return defaultVal.call(this);
} else {
return defaultVal;
}
}
}
get(key) {
let getterKey = this.getters[key];
let getter = getterKey && this[getterKey];
if (getter) {
return getter.call(this, key);
} else {
return this.getConfigProperty(key);
}
}
set(key, value) {
if (!this.config) {
this.config = {};
}
this.config[key] = value;
}
isCwdMode() {
return !this.get('src_files') && !this.get('test_page');
}
getAvailableLaunchers(cb) {
let browsers = knownBrowsers(process.platform, this);
browser_launcher.getAvailableBrowsers(this, browsers, (err, availableBrowsers) => {
if (err) {
return cb(err);
}
let availableLaunchers = {};
availableBrowsers.forEach(browser => {
let newLauncher = new LauncherFactory(browser.name, browser, this);
availableLaunchers[browser.name.toLowerCase()] = newLauncher;
});
// add custom launchers
let customLaunchers = this.get('launchers');
if (customLaunchers) {
for (let name in customLaunchers) {
let newLauncher = new LauncherFactory(name, customLaunchers[name], this);
availableLaunchers[name.toLowerCase()] = newLauncher;
}
}
cb(null, availableLaunchers);
});
}
getLaunchers(cb) {
this.getAvailableLaunchers((err, availableLaunchers) => {
if (err) {
return cb(err);
}
this.getWantedLaunchers(availableLaunchers, cb);
});
}
getWantedLauncherNames(available) {
let launchers = this.get('launch');
if (launchers) {
launchers = launchers.toLowerCase().split(',');
} else if (this.appMode === 'dev') {
launchers = this.get('launch_in_dev') || [];
} else {
launchers = this.get('launch_in_ci') || Object.keys(available);
}
let skip = this.get('skip');
if (skip) {
skip = skip.toLowerCase().split(',');
launchers = launchers.filter(name => skip.indexOf(name) === -1);
}
return launchers;
}
getWantedLaunchers(available, cb) {
let launchers = [];
let wanted = this.getWantedLauncherNames(available);
let err = null;
wanted.forEach(name => {
let launcher = available[name.toLowerCase()];
if (!launcher) {
if (this.appMode === 'dev' || this.get('ignore_missing_launchers')) {
log.warn('Launcher "' + name + '" is not recognized.');
} else {
err = new Error(
'Launcher ' +
name +
' not found. Not installed? Available: ' +
Object.keys(available).map(elm => `"${elm}"`).join(', ') +
'.'
);
}
} else {
launchers.push(launcher);
}
});
cb(err, launchers);
}
printLauncherInfo() {
this.getAvailableLaunchers((err, launchers) => {
let launch_in_dev = (this.get('launch_in_dev') || [])
.map(s => s.toLowerCase());
let launch_in_ci = this.get('launch_in_ci');
if (launch_in_ci) {
launch_in_ci = launch_in_ci.map(s => s.toLowerCase());
}
launchers = Object.keys(launchers).map(k => launchers[k]);
const launcherColumnWidth = launchers.reduce((acc, current) => acc > current.name.length ? acc : current.name.length, 0);
const launcherTitle = 'Launcher'.padEnd(launcherColumnWidth, ' ');
const launcherTitleDivider = ''.padEnd(launcherColumnWidth, '-');
console.log('Have ' + launchers.length + ' launchers available; auto-launch info displayed on the right.');
console.log(); // newline
console.log(`${launcherTitle} Type CI Dev`);
console.log(`${launcherTitleDivider} ------------ -- ---`);
console.log(launchers.map(launcher => {
let protocol = launcher.settings.protocol;
let kind = protocol === 'browser' ?
'browser' : (
protocol === 'tap' ?
'process(TAP)' : 'process');
let dev = launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1 ?
Chars.mark :
' ';
let ci = !launch_in_ci || launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1 ?
Chars.mark :
' ';
return (pad(launcher.name, launcherColumnWidth + 2, ' ', 1) +
pad(kind, 12, ' ', 1) +
' ' + ci + ' ' + dev + ' ');
}).join('\n'));
});
}
getFileSet(want, dontWant, callback) {
if (isa(want, String)) {
want = [want]; // want is an Array
}
if (isa(dontWant, String)) {
dontWant = [dontWant]; // dontWant is an Array
}
// Filter glob < 6 negation patterns to still support them
// See https://github.com/isaacs/node-glob/tree/3f883c43#comments-and-negation
let positiveWants = [];
want.forEach(patternEntry => {
let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
if (pattern.indexOf('!') === 0) {
return dontWant.push(pattern.substring(1));
}
positiveWants.push(patternEntry);
});
dontWant = dontWant.map(p => p ? this.resolvePath(p) : p);
Bluebird.reduce(positiveWants, (allThatIWant, patternEntry) => {
let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
let attrs = patternEntry.attrs || [];
let patternUrl = url.parse(pattern);
if (patternUrl.protocol === 'file:') {
pattern = patternUrl.hostname + patternUrl.path;
} else if (patternUrl.protocol) {
return allThatIWant.concat({ src: pattern, attrs: attrs });
}
return globAsync(this.resolvePath(pattern), { ignore: dontWant }).then(files => allThatIWant.concat(files.map(f => {
f = this.reverseResolvePath(f);
return { src: f, attrs: attrs };
})));
}, [])
.then(result => _.uniqBy(result, 'src'))
.asCallback(callback);
}
getSrcFiles(callback) {
let srcFiles = this.get('src_files') || '*.js';
let srcFilesIgnore = this.get('src_files_ignore') || '';
this.getFileSet(srcFiles, srcFilesIgnore, callback);
}
getFooterScripts(callback) {
var want = this.get('footer_scripts') || [];
var dontWant = this.get('src_files_ignore') || '';
this.getFileSet(want, dontWant, callback);
}
getServeFiles(callback) {
let want = this.get('serve_files') || this.get('src_files') || '*.js';
let dontWant = this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
this.getFileSet(want, dontWant, callback);
}
getUserDataDir() {
if (this.get('user_data_dir')) {
return path.resolve(this.cwd(), this.get('user_data_dir'));
}
return os.tmpdir();
}
getHomeDir() {
return process.env.HOME || process.env.USERPROFILE;
}
getCSSFiles(callback) {
let want = this.get('css_files') || '';
this.getFileSet(want, '', callback);
}
getAllOptions() {
let options = [];
function getOptions(o) {
if (!o) {
return;
}
if (o.options) {
o.options.forEach(o => {
options.push(o.name());
});
}
getOptions(o.parent);
}
getOptions(this.progOptions);
return options;
}
getTemplateData(cb) {
let ret = {};
let options = this.getAllOptions();
let key;
for (key in this.progOptions) {
if (options.indexOf(key) !== -1) {
ret[key] = this.progOptions[key];
}
}
for (key in this.fileOptions) {
ret[key] = this.fileOptions[key];
}
for (key in this.config) {
ret[key] = this.config[key];
}
this.getServeFiles((err, files) => {
let replaceSlashes = f => ({
src: f.src.replace(/\\/g, '/'),
attrs: f.attrs
});
ret.serve_files = files.map(replaceSlashes);
this.getCSSFiles((err, files) => {
ret.css_files = files.map(replaceSlashes);
this.getFooterScripts((err, files) => {
ret.footer_scripts = files;
if (cb) {
cb(err, ret);
}
});
});
});
}
}
Config.prototype.defaults = {
host: 'localhost',
port: 7357,
url() {
let scheme = 'http';
if (this.get('key') || this.get('pfx')) {
scheme = 'https';
}
return scheme + '://' + this.get('host') + ':' + this.get('port') + '/';
},
parallel: 1,
reporter: 'tap',
bail_on_uncaught_error: true,
browser_start_timeout: 30,
browser_disconnect_timeout: 10,
browser_reconnect_limit: 3,
client_decycle_depth: 5,
socket_heartbeat_timeout() {
let browserDisconnectTimeout = this.get('browser_disconnect_timeout');
let defaultBrowserDisconnectTimeout = this.defaults.browser_disconnect_timeout;
return (browserDisconnectTimeout !== defaultBrowserDisconnectTimeout) ? browserDisconnectTimeout : 5;
}
};
module.exports = Config;