src/lib/cucumberjs/cucumber.js
const path = require('path');
const cp = require('child-process-debug');
const processHelper = require('./../process-helper.js');
const log = require('./../log');
const _ = require('underscore');
const booleanHelper = require('../boolean-helper');
const fs = require('fs-extra');
/**
* Run Cucumber specs
*
* @param {Function} callback
* @api public
*/
class Cucumber {
constructor(options) {
this.options = options;
this.cucumberChild = null;
}
start = (callback) => {
const args = this._getExecOptions(this.options);
if (!fs.existsSync(this.options.path)) {
const infoMessage = `[chimp][cucumber] Directory ${this.options.path} does not exist. Not running`;
if (booleanHelper.isTruthy(this.options['fail-when-no-tests-run'])) {
callback(infoMessage);
}
else {
log.info(infoMessage);
callback();
}
return;
}
log.debug('[chimp][cucumber] Running with', args);
const opts = {
env: process.env,
silent: true,
};
let port;
if (booleanHelper.isTruthy(this.options.debugCucumber)) {
port = parseInt(this.options.debugCucumber, 10);
if (port > 1) {
opts.execArgv = ['--debug=' + port];
} else {
opts.execArgv = ['--debug'];
}
}
if (booleanHelper.isTruthy(this.options.debugBrkCucumber)) {
port = parseInt(this.options.debugBrkCucumber, 10);
if (port > 1) {
opts.execArgv = ['--debug-brk=' + port];
} else {
opts.execArgv = ['--debug-brk'];
}
}
this.cucumberChild = cp.fork(path.join(__dirname, 'cucumber-wrapper.js'), args, opts);
if (booleanHelper.isTruthy(this.options.conditionOutput)) {
this.cucumberChild.stdout.on('data', (data) => {
this._conditionMessage(data.toString());
});
} else {
this.cucumberChild.stdout.pipe(process.stdout);
}
this.cucumberChild.stderr.pipe(process.stderr);
process.stdin.pipe(this.cucumberChild.stdin);
let jsonResults = null;
this.cucumberChild.on('message', (res) => {
log.debug('[chimp][cucumber] Received message from cucumber child. Result:', res);
jsonResults = res;
});
this.cucumberChild.on('close', (code) => {
log.debug('[chimp][cucumber] Closed with code', code);
if (!this.cucumberChild.stopping) {
log.debug('[chimp][cucumber] Cucumber not in a stopping state');
if (this.options.jsonOutput && jsonResults && JSON.parse(jsonResults).length) {
const dir = path.dirname(this.options.jsonOutput);
log.debug('[chimp][cucumber] Ensuring directory exists', dir);
fs.mkdirsSync(dir);
log.debug('[chimp][cucumber] Writing json results to', this.options.jsonOutput);
fs.writeFileSync(this.options.jsonOutput, jsonResults);
log.debug('[chimp][cucumber] Finished writing results');
}
const failWhenNoTestsRun = booleanHelper.isTruthy(this.options['fail-when-no-tests-run']);
const noTestsFound = jsonResults === null || JSON.parse(jsonResults).length === 0;
callback(code !== 0 || (code === 0 && noTestsFound && failWhenNoTestsRun) ? 'Cucumber steps failed' : null, jsonResults);
}
});
}
interrupt = (callback) => {
log.debug('[chimp][cucumber] interrupting cucumber');
if (!this.cucumberChild) {
log.debug('[chimp][cucumber] no child to interrupt');
return callback();
}
this.cucumberChild.stopping = true;
const options = {
child: this.cucumberChild,
prefix: 'cucumber',
};
processHelper.kill(options, (err, res) => {
this.cucumberChild = null;
if (callback) {
callback(err, res);
}
});
}
_getRecommendedFilename = (line) => {
const stepType = line.match(/this\.(Given|When|Then)/)[1];
let recommendedFilename = stepType + ' ' + line.match(/\^(.*)\$/)[1];
recommendedFilename = recommendedFilename.replace(/".*"/g, '#');
recommendedFilename = recommendedFilename.replace(/\(.*\)/g, '#');
recommendedFilename = recommendedFilename.replace(/\\/g, '');
recommendedFilename = recommendedFilename.replace(/\$/g, '');
recommendedFilename = recommendedFilename.replace(/ /g, this.options.recommendedFilenameSeparator || ' ');
return recommendedFilename;
}
_conditionOutput = (message) => {
if (message.indexOf('callback.pending()') === -1) {
process.stdout.write(message);
return;
}
try {
const defaultText = 'Write code here that turns the phrase above into concrete actions';
const replacementText = 'Write the automation code here';
let tab = '';
const self = this;
_.each(message.split('\n'), (eachLine) => {
if (booleanHelper.isTruthy(self.options.singleSnippetPerFile) && eachLine.match(/this\./)) {
process.stdout.write('// Recommended filename: '.gray + (self._getRecommendedFilename(eachLine) + '.js\n').cyan);
process.stdout.write('module.exports = function() {\n'.yellow);
tab = ' ';
}
let line = eachLine;
line = line.replace(defaultText, replacementText);
line = line.replace('callback.pending()', 'pending()');
line = line.replace(', callback', '');
line = line.replace('callback', '');
process.stdout.write(tab + line + '\n');
if (booleanHelper.isTruthy(self.options.singleSnippetPerFile) && line.match(/}\);/)) {
process.stdout.write('};\n'.yellow);
tab = '';
}
});
} catch (e) {
log.debug('[chimp][cucumber] Error conditioning message', e);
process.stdout.write(message);
}
}
_conditionMessage = (message) => {
if (this.options.debug) {
log.debug(message);
return;
}
// output any strings that don't contain a stack trace
if (message.indexOf(' at') === -1) {
this._conditionOutput(message);
return;
}
let msg = '';
const basePath = path.resolve('.', this.options.path);
const basePathParent = path.resolve(basePath, '..');
const TAB = ' ';
try {
_.each(message.split('\n'), (line) => {
const trimmedLine = line.trim();
const relativePathLine = line.replace(basePathParent + path.sep, '');
// filter out some known unnecessary lines
// console.error('[' + line + ']');
if (trimmedLine.indexOf('node_modules') !== -1) {
return;
}
// for stack trace lines
if (trimmedLine.indexOf('at') === 0) {
msg += relativePathLine.yellow + '\n';
} else {
// or other lines that start with a tab (cucumber repeats errors at the end)
if (line.indexOf(TAB) !== -1) {
msg += relativePathLine.yellow + '\n';
} else {
msg += relativePathLine.magenta + '\n';
}
}
});
process.stdout.write(msg);
} catch (e) {
log.debug('[chimp][cucumber] Error conditioning console out', e);
process.stdout.write(message);
}
}
_getExecOptions = (options) => {
const execOptions = ['node', path.resolve(__dirname,
path.join('..', '..', '..', 'node_modules', '.bin', 'cucumber.js')
)];
// XXX a feature may be defined at the start or end
// XXX do other options also get passed with this command?
let features = options._.splice(2).toString() || options.features || options.path;
if (features.indexOf(',') !== -1) {
features = features.split(',');
_.each(features, (feature) => {
execOptions.push(feature);
});
} else {
execOptions.push(features);
}
execOptions.push('-r');
execOptions.push(path.resolve(__dirname, path.join('../chimp-helper.js')));
execOptions.push('-r');
execOptions.push(path.resolve(__dirname, path.join('world.js')));
if (!options.domainOnly) {
execOptions.push('-r');
execOptions.push(path.resolve(__dirname, path.join('hooks.js')));
}
if (!options.r && !options.require) {
execOptions.push('-r');
execOptions.push(options.path);
}
// See: https://github.com/cucumber/cucumber-js/blob/v0.9.2/lib/cucumber/cli.js
const allowedCucumberJsOptions = {
long: [
'version', 'backtrace', 'compiler', 'dry-run', 'fail-fast', 'format',
'no-colors', 'no-snippets', 'no-source', 'profile', 'require', 'snippet-syntax',
'strict', 'tags', 'help', 'name',
],
short: ['v', 'b', 'd', 'f', 'p', 'r', 'S', 't'],
};
_.forEach(options, (eachOptionValues, optionName) => {
const optionValues = _.isArray(eachOptionValues) ? eachOptionValues : [eachOptionValues];
if (_.contains(allowedCucumberJsOptions.long, optionName)) {
_.forEach(optionValues, (optionValue) => {
execOptions.push('--' + optionName);
if (['dry-run', 'fail-fast', 'no-colors', 'no-snippets', 'no-source', 'strict', 'backtrace'].indexOf(optionName) === -1) {
execOptions.push(optionValue.toString());
}
});
} else if (_.contains(allowedCucumberJsOptions.short, optionName)) {
_.forEach(optionValues, (optionValue) => {
execOptions.push('-' + optionName);
if (['d', 'S', 'b'].indexOf(optionName) === -1) {
execOptions.push(optionValue.toString());
}
});
} else if (_.last(optionValues) === false &&
_.contains(allowedCucumberJsOptions.long, 'no-' + optionName)
) {
execOptions.push('--no-' + optionName);
}
});
log.debug('[chimp][cucumber] Cucumber exec options are: ', execOptions);
return execOptions;
}
}
module.exports = Cucumber;