mimik
Version:
Write end-to-end automation tests in natural language
238 lines (231 loc) • 9.3 kB
JavaScript
/*jshint node:true*/
/**
* Generate step definitions from a feature object or file
*
* Usage:
* var generator = new StepDefinitionGenerator();
* generator.generate(feature, '{keyword}(\/{step}\/, function(done) {\n done();\n});\n');
*
*/
;
var Yadda = require('yadda'),
path = require('path'),
utils = require('./utils'),
tint = require('./tint'),
fs = require('fs'),
prompt = '\u203A',
StepDefinitionGenerator = function(config) {
config = config || {};
this.stdin = config.stdin || process.stdin;
this.stdout = config.stdout || process.stdout;
this.language = config.language || 'English';
};
StepDefinitionGenerator.prototype.matchStep = function(step, previous, language) {
var prefix = '^(',
suffix = ')\\s+(.+)$',
keyword,
matchSequence = {
given: ['given', 'when', 'then'], // used by default or when previous step resolves to a given
when: ['when', 'then', 'given'], // use when previous step resolves to a when
then: ['then', 'given', 'when'] // use when previous step resolve to a then
},
_steps = matchSequence[previous || 'given'];
for(var i = 0, len = _steps.length; i < len; i++) {
keyword = _steps[i];
var re = new RegExp(prefix + Yadda.localisation[language || this.language].localise(keyword) + suffix);
var match = re.exec(step);
if(match) {
return { type: keyword, keyword: match[1], step: match[2], raw: step};
}
}
return null;
};
StepDefinitionGenerator.prototype.matchSteps = function(steps, language) {
var me = this,
matches = [], previous;
(steps||[]).forEach(function(step) {
var match = me.matchStep(step, previous, language);
if(match) {
matches.push(match);
previous = match.type;
}
});
return matches;
};
StepDefinitionGenerator.prototype.getFeatureSteps = function(feature, language) {
var me = this,
steps = [];
if(!feature) {
throw new Error('StepDefinitionGenerator: getFeatureSteps() requires a valid argument.');
}
feature.scenarios.forEach(function(scenario) {
steps = steps.concat(me.matchSteps(scenario.steps, language));
});
return steps;
};
StepDefinitionGenerator.prototype.generate = function(feature, language, stepTemplate) {
var me = this,
steps = me.getFeatureSteps(feature, language),
output = '';
steps.forEach(function(step) {
output += stepTemplate.replace('{type}', step.type).replace('{keyword}', step.keyword).replace('{step}', utils.escapeRE(step.step)).replace('{raw}', step.raw) + '\n';
});
return output;
};
/**
* Generate step definitions and return output.
* @param {String} file The feature file to generate steps for.
* @param {String} language The language that the feature file is written in. Defaults to English.
* @param {String} type The output type. Available options are javascript, coffeescript. Defaults to javascript.
* @param {String} target A file path (optional). Save to a file if a target path is supplied.
* @param {Function} cb The callback function returned after processing output. The callback signature is cb(err, output).
*/
StepDefinitionGenerator.prototype.generateFromFile = function(file, language, type, target, cb) {
var me = this,
jsTpl = '// {raw}\n{keyword}(\/{step}\/, function(done) {\n done();\n});\n',
coffeeTpl = '# {raw}\n{keyword} \/{step}\/, (done) ->\n done()\n',
context = {
describe: function(text, next) {
next();
}
};
if(!file || !~file.indexOf('.feature')) {
return cb(new Error('The supplied feature file is invalid'));
}
var tpl = type === 'coffeescript' ? coffeeTpl : jsTpl;
fs.exists(file, function(exists) {
if(!exists) {
return cb(new Error('Could not locate the feature file ' + file));
}
Yadda.plugins.mocha.AsyncStepLevelPlugin.init({
container: context
});
context.featureFile(file, function(feature) {
var output = me.generate(feature, language, tpl);
if(target) {
require('fs-tools').mkdir(path.dirname(target), function(err) {
if(err) {
return cb(err);
}
fs.writeFile(target, output, function(err) {
cb(err, output);
});
});
} else {
cb(null, output);
}
});
});
};
StepDefinitionGenerator.prototype.getMatchingStepFileName = function(file, type, suffix, relative) {
var basename = path.basename(file).split('.'),
types = {
'javascript': 'js',
'coffeescript': 'coffee'
},
target;
// remove the .feature file extension
basename.pop();
suffix = (suffix || (this.isCamelCase(basename.join('')) ? 'Steps' : '-steps')) + '.' + (types[type] || 'js');
target = path.join(path.dirname(file), '..', 'steps', basename.join('.') + suffix);
return relative === true ? path.relative(process.cwd(), target) : target;
};
StepDefinitionGenerator.prototype.isCamelCase = function(str) {
return str === utils.toCamelCase(str, true);
};
StepDefinitionGenerator.prototype.prompt = function(file, cb) {
var me = this,
callback = function(data) {
me.stdin.removeListener('data', onData);
cb(data);
},
onData = me.onData.bind(me, file, callback);
me.step = 0;
me.stdin.resume();
me.stdin.setEncoding('utf8');
me.showStep1(file);
me.stdin.on('data', onData);
};
StepDefinitionGenerator.prototype.onData = function(file, cb, text) {
var me = this;
text = text.trim().replace(/\r\n|\n/, '');
me.stdout.write('\u001B[1A ' + prompt + ' \u001B[K');
switch (me.step) {
case 0:
if (~ ['1', '2', ''].indexOf(text)) {
me._type = text === '2' ? 'coffeescript' : 'javascript';
me.stdout.write(tint.green(' ' + me._type) + '\n\n');
me.showStep2();
me.step++;
}
break;
case 1:
if (~ ['1', '2', '3', '4', '5', ''].indexOf(text)) {
me._language = ['English', 'French', 'Norwegian', 'Polish', 'Spanish'][text === '' ? 0 : +text - 1];
me.stdout.write(tint.green(' ' + me._language) + '\n\n');
me.showStep3();
me.step++;
}
break;
case 2:
if (~ ['1', '2', ''].indexOf(text)) {
me._outputType = text === '2' ? 'file' : 'display';
var outputTypeText = {'display': 'Display output', 'file': 'Save to a file'}[me._outputType];
me.stdout.write(tint.green(' ' + outputTypeText) + '\n\n');
if(me._outputType === 'file') {
me.showStep4(file, me._type);
}
me.step++;
}
break;
case 3:
if(me._outputType === 'file') {
me._target = text ? text : me.getMatchingStepFileName(file, me._type, null, true);
me.stdout.write(tint.gray('Saving to') + ' ' +tint.green(me._target) + '\n\n');
me.step++;
}
break;
}
if ((me._outputType === 'display' && me.step === 3) || me.step === 4) {
if(typeof cb === 'function') {
cb({
language: me._language,
type: me._type,
target: me._target
});
}
delete me._language;
delete me._type;
delete me._outputType;
delete me._target;
}
};
StepDefinitionGenerator.prototype.showStep1 = function(file) {
this.stdout.write(tint.gray('Feature file: ') + file + '\n');
this.stdout.write(tint.gray('Press Ctrl+C to abort') + '\n');
this.stdout.write(tint.gray('\nChoose output') + '\n');
this.stdout.write(' 1. javascript (default)\n');
this.stdout.write(' 2. coffeescript\n');
this.stdout.write(' ' + prompt + ' ');
};
StepDefinitionGenerator.prototype.showStep2 = function() {
this.stdout.write(tint.gray('Specify the feature file language') + '\n');
this.stdout.write(' 1. English (default)\n');
this.stdout.write(' 2. French\n');
this.stdout.write(' 3. Norwegian\n');
this.stdout.write(' 4. Polish\n');
this.stdout.write(' 5. Spanish\n');
this.stdout.write(' ' + prompt + ' ');
};
StepDefinitionGenerator.prototype.showStep3 = function() {
this.stdout.write(tint.gray('Generate output') + '\n');
this.stdout.write(' 1. Display output (default)\n');
this.stdout.write(' 2. Save to a file\n');
this.stdout.write(' ' + prompt + ' ');
};
StepDefinitionGenerator.prototype.showStep4 = function(file, type) {
this.stdout.write(tint.gray('Specify a path:') + '\n');
this.stdout.write(' ' + this.getMatchingStepFileName(file, type, null, true) + ' (default)\n');
this.stdout.write(' ' + prompt + ' ');
};
module.exports = StepDefinitionGenerator;