typespec-bdd
Version:
BDD framework for TypeScript.
259 lines • 12.2 kB
JavaScript
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./RegEx", "./State"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const RegEx_1 = require("./RegEx");
const State_1 = require("./State");
class FeatureParser {
constructor(testReporter, testHooks, steps, tagsToExclude) {
this.testReporter = testReporter;
this.tagsToExclude = tagsToExclude;
this.scenarios = [];
this.scenarioIndex = 0;
this.hasParsed = false;
this.scenarios[this.scenarioIndex] = new State_1.InitializedState(this.tagsToExclude);
this.featureRunner = new FeatureRunner(steps, testReporter, testHooks);
}
run(spec, afterFeatureHandler) {
this.parseSpecification(spec);
this.runFeature(afterFeatureHandler);
}
parseSpecification(spec) {
this.hasParsed = true;
/* Normalise line endings before splitting */
const lines = spec.replace('\r\n', '\n').split('\n');
/* Parse the steps */
for (const line of lines) {
try {
this.process(line);
}
catch (ex) {
this.hasParsed = false;
const state = this.scenarios[0] || { featureTitle: 'Unknown' };
this.testReporter.error(state.featureTitle, line, ex);
}
}
}
process(line) {
if (this.scenarios[this.scenarioIndex].isNewScenario(line)) {
// This is an additional scenario within the same feature file.
const existingFeatureTitle = this.scenarios[this.scenarioIndex].featureTitle;
const existingFeatureDescription = this.scenarios[this.scenarioIndex].featureDescription;
this.scenarioIndex++;
this.scenarios[this.scenarioIndex] = new State_1.FeatureState(null);
this.scenarios[this.scenarioIndex].featureTitle = existingFeatureTitle;
this.scenarios[this.scenarioIndex].featureDescription = existingFeatureDescription;
this.scenarios[this.scenarioIndex].tagsToExclude = this.tagsToExclude;
}
// Process the new line
this.scenarios[this.scenarioIndex] = this.scenarios[this.scenarioIndex].process(line);
}
runFeature(afterFeatureHandler) {
if (this.hasParsed) {
this.featureRunner.run(this.scenarios, afterFeatureHandler);
}
else {
afterFeatureHandler();
}
}
}
exports.FeatureParser = FeatureParser;
class FeatureRunner {
constructor(steps, testReporter, testHooks) {
this.steps = steps;
this.testReporter = testReporter;
this.testHooks = testHooks;
this.scenarios = [];
this.currentCondition = '';
this.asyncTimeout = 1000; // TODO: Make user configurable
}
// HOOK BEFORE / AFTER FEATURE
run(scenarios, afterFeatureHandler) {
this.testHooks.beforeFeature();
this.scenarios = scenarios;
let completedScenarios = 0;
const afterScenarioHandler = () => {
this.testHooks.afterScenario();
completedScenarios++;
if (completedScenarios === this.scenarios.length) {
afterFeatureHandler();
}
};
// Each Scenario
for (const scenario of this.scenarios) {
this.testHooks.beforeScenario();
if (!scenario.scenarioTitle) {
this.testReporter.summary(scenario.featureTitle, 'Ignored', true);
afterScenarioHandler();
continue;
}
this.runScenario(scenario, afterScenarioHandler);
}
}
// HOOK BEFORE / AFTER SCENARIO
runScenario(scenario, scenarioCompleteHandler) {
const tableRowCount = (scenario.tableRows.length > 0) ? scenario.tableRows.length : 1;
let completedExamples = 0;
const examplesCompleteHandler = () => {
completedExamples++;
if (completedExamples === tableRowCount) {
scenarioCompleteHandler();
}
};
// Each Example Row
for (let exampleIndex = 0; exampleIndex < tableRowCount; exampleIndex++) {
try {
const context = {};
this.testReporter.information('--------------------------------------');
this.testReporter.information(scenario.featureTitle);
this.testReporter.information('\t' + scenario.featureDescription.join('\r\n\t') + '\r\n\r\n');
// Process the scenario steps
const conditions = scenario.getAllConditions();
this.runNextCondition(conditions, 0, context, scenario, exampleIndex, true, examplesCompleteHandler);
}
catch (ex) {
this.testReporter.error(scenario.featureTitle, this.currentCondition, ex);
}
}
}
// HOOK BEFORE / AFTER CONDITION
runNextCondition(conditions, conditionIndex, context, scenario, exampleIndex, passing, examplesCompleteHandler) {
try {
const next = conditions[conditionIndex];
const nextConditionIndex = conditionIndex + 1;
let completionHandled = false;
let timer = null;
this.testHooks.beforeCondition();
this.currentCondition = next.condition;
/* Handler to run after the condition completes... */
context.done = () => {
if (completionHandled) {
return;
}
completionHandled = true;
if (timer) {
clearTimeout(timer);
}
this.testHooks.afterCondition();
if (nextConditionIndex < conditions.length) {
this.runNextCondition(conditions, nextConditionIndex, context, scenario, exampleIndex, passing, examplesCompleteHandler);
}
else {
this.testReporter.summary(scenario.featureTitle, scenario.scenarioTitle, passing);
examplesCompleteHandler();
}
};
const condition = scenario.prepareCondition(next.condition, exampleIndex);
this.testReporter.information('\t' + condition);
const stepExecution = this.steps.find(condition, next.type);
if (stepExecution === null) {
const stepMethodBuilder = new StepMethodBuilder(condition);
throw new Error('No step definition defined.\n\nSee https://github.com/Steve-Fenton/TypeSpec/wiki/Hints#user-content-no-step-definition-defined\n\n' + stepMethodBuilder.getSuggestedStepMethod());
}
const isAsync = stepExecution.isAsync;
if (stepExecution.parameters) {
// Add the context container as the first argument
stepExecution.parameters.unshift(context);
// Call the step method
stepExecution.method.apply(null, stepExecution.parameters);
}
else {
// Call the step method
stepExecution.method.call(null, context);
}
if (isAsync) {
timer = setTimeout(() => {
if (completionHandled) {
return;
}
completionHandled = true;
passing = false;
this.testReporter.error('Async Exception', condition, new Error('Async step timed out'));
this.testReporter.summary(scenario.featureTitle, scenario.scenarioTitle, passing);
examplesCompleteHandler();
}, this.asyncTimeout);
}
else {
context.done();
}
}
catch (ex) {
passing = false;
this.testReporter.error(scenario.featureTitle, this.currentCondition, ex);
this.testReporter.summary(scenario.featureTitle, scenario.scenarioTitle, passing);
examplesCompleteHandler();
}
}
}
class StepMethodBuilder {
constructor(originalCondition) {
this.originalCondition = originalCondition;
}
getSuggestedStepMethod() {
const argumentParser = new ArgumentParser(this.originalCondition);
/* Template for step method */
const params = argumentParser.getParameters();
const comma = (params.length > 0) ? ', ' : '';
const suggestion = ' @step(/^' + argumentParser.getCondition() + '$/i)\n' +
' stepName(context: any' + comma + params + ') {\n' +
' throw new Error(\'Not implemented.\');\n' +
' }';
return suggestion;
}
}
class ArgumentParser {
constructor(originalCondition) {
this.originalCondition = originalCondition;
this.arguments = [];
this.condition = originalCondition;
this.parseArguments();
}
getCondition() {
return this.condition;
}
getParameters() {
return this.arguments.join(', ');
}
parseArguments() {
const foundArguments = this.originalCondition.match(RegEx_1.ExpressionLibrary.quotedArgumentsRegExp);
if (!foundArguments || foundArguments.length === 0) {
return;
}
for (let i = 0; i < foundArguments.length; i++) {
const foundArgument = foundArguments[i];
this.replaceArgumentWithExpression(foundArgument, i);
}
}
replaceArgumentWithExpression(quotedArgument, position) {
const trimmedArgument = quotedArgument.replace(/"/g, '');
let argumentExpression = '';
if (this.isBooleanArgument(trimmedArgument)) {
this.arguments.push('p' + position + ': boolean');
argumentExpression = RegEx_1.ExpressionLibrary.trueFalseString;
}
else if (this.isNumericArgument(trimmedArgument)) {
this.arguments.push('p' + position + ': number');
argumentExpression = RegEx_1.ExpressionLibrary.numberString;
}
else {
this.arguments.push('p' + position + ': string');
argumentExpression = RegEx_1.ExpressionLibrary.defaultString;
}
this.condition = this.condition.replace(quotedArgument, argumentExpression);
}
isBooleanArgument(argument) {
return (argument.toLowerCase() === 'true' || argument.toLowerCase() === 'false');
}
isNumericArgument(argument) {
return (parseFloat(argument).toString() === argument);
}
}
});
//# sourceMappingURL=Parser.js.map