UNPKG

typespec-bdd

Version:

BDD framework for TypeScript.

259 lines 12.2 kB
(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