From 5d05a3d2b9a2fe9525c830ade70028fc169184db Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Wed, 17 Mar 2021 21:17:52 +0100 Subject: [PATCH 01/29] no msg --- .vscode/launch.json | 16 +++- .../beverage-vending-machine.feature | 2 + .../snack-vending-machine.feature | 2 + .../using-latest-gherkin-keywords.steps.ts | 2 + package-lock.json | 2 +- package.json | 2 +- src/automatic-step-binding.ts | 76 ++++++++++++++++--- src/feature-definition-creation.ts | 1 + src/models.ts | 8 ++ src/parsed-feature-loading.ts | 31 +++++--- src/validation/scenario-validation.ts | 7 ++ 11 files changed, 125 insertions(+), 24 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4adfb11..a856985 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,23 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch via NPM", + "type": "pwa-node", + "request": "launch", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run-script", "test", + "--", + "examples/typescript/specs/step-definitions/auto-step-binding.steps.ts" + ], +}, { "type": "node", "request": "launch", "name": "Launch Program", - "preLaunchTask": build, + "preLaunchTask": "build", "program": "${workspaceFolder}/dist/code-generation-test.js" }, { @@ -19,7 +31,7 @@ "args": [ "-i" ], - "preLaunchTask": build, + "preLaunchTask": "build", "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceRoot}/examples/typescript/dist/**/*" diff --git a/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature b/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature index 14e3a41..8923815 100644 --- a/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature +++ b/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature @@ -1,5 +1,7 @@ Feature: Beverage vending machine + Rule: Dispense purchased beverage + Scenario Outline: Purchasing a beverage Given the vending machine has "" in stock And I have inserted the correct amount of money diff --git a/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature b/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature index e0f676b..0e2ef92 100644 --- a/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature +++ b/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature @@ -1,5 +1,7 @@ Feature: Snack vending machine + Rule: Dispenses purchased snack + Scenario: Purchasing a snack Given the vending machine has "Maltesers" in stock And I have inserted the correct amount of money diff --git a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts index cb76718..c8cf2a0 100644 --- a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts +++ b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts @@ -46,6 +46,8 @@ defineFeature(feature, (test) => { }); }; + defineRule('When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed' + test('Subtracting two numbers', ({ given, and, when, then }) => { givenIHaveEnteredXAsTheFirstOperand(given); andIHaveEnteredXAsTheOperator(and); diff --git a/package-lock.json b/package-lock.json index 917a69a..7789514 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jest-cucumber", - "version": "2.0.11", + "version": "3.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9faecd6..6d32afd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "jest": "jest --verbose", - "test": "npm run build & npm run lint & jest --color", + "test": "npm run build & jest --color", "lint": "tslint --project ./" }, "repository": { diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 58085d6..7b0011e 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -1,9 +1,9 @@ import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models'; -import { matchSteps } from './validation/step-definition-validation'; +import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, matchSteps } from './validation/step-definition-validation'; import { StepsDefinitionCallbackFunction, defineFeature } from './feature-definition-creation'; import { generateStepCode } from './code-generation/step-generation'; -const globalSteps: Array<{ stepMatcher: string | RegExp, stepFunction: () => any }> = []; +const globalSteps: Array<{ stepMatcher: string | RegExp; stepFunction: () => any }> = []; const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) => { globalSteps.push({ stepMatcher, stepFunction }); @@ -20,7 +20,7 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD but: registerStep, pending: () => { // Nothing to do - }, + } }); }); @@ -28,16 +28,19 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD features.forEach((feature) => { defineFeature(feature, (test) => { - const scenarioOutlineScenarios = feature.scenarioOutlines - .map((scenarioOutline) => scenarioOutline.scenarios[0]); - const scenarios = [...feature.scenarios, ...scenarioOutlineScenarios]; + const scenarioOutlineScenarios = feature.scenarioOutlines.map( + (scenarioOutline) => scenarioOutline.scenarios[0] + ); + + const scenarios = [ ...feature.scenarios, ...scenarioOutlineScenarios ]; scenarios.forEach((scenario) => { test(scenario.title, (options) => { scenario.steps.forEach((step, stepIndex) => { - const matches = globalSteps - .filter((globalStep) => matchSteps(step.stepText, globalStep.stepMatcher)); + const matches = globalSteps.filter((globalStep) => + matchSteps(step.stepText, globalStep.stepMatcher) + ); if (matches.length === 1) { const match = matches[0]; @@ -46,14 +49,65 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD } else if (matches.length === 0) { const stepCode = generateStepCode(scenario.steps, stepIndex, false); // tslint:disable-next-line:max-line-length - errors.push(`No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}`); + errors.push( + `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` + ); } else { - const matchingCode = matches.map((match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}`); - errors.push(`${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join('\n\n')}`); + const matchingCode = matches.map( + (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` + ); + errors.push( + `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( + '\n\n' + )}` + ); } }); }); }); + + feature.rules.forEach((rule) => { + + describe(rule.title, () => { + const scenarioPath = feature.title + ' -> ' + rule.title; + const scenarioOutlineScenarios = rule.scenarioOutlines.map( + (scenarioOutline) => scenarioOutline.scenarios[0] + ); + + const ruleScenarios = [ ...rule.scenarios, ...scenarioOutlineScenarios ]; + + ruleScenarios.forEach((scenario) => { + test(scenario.title, (options) => { + scenario.steps.forEach((step, stepIndex) => { + const matches = globalSteps.filter((globalStep) => + matchSteps(step.stepText, globalStep.stepMatcher) + ); + + if (matches.length === 1) { + const match = matches[0]; + + options.defineStep(match.stepMatcher, match.stepFunction); + } else if (matches.length === 0) { + const stepCode = generateStepCode(scenario.steps, stepIndex, false); + // tslint:disable-next-line:max-line-length + errors.push( + `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" at "${scenarioPath}". Please add the following step code: \n\n${stepCode}` + ); + } else { + const matchingCode = matches.map( + (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` + ); + errors.push( + `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" "${scenarioPath}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( + '\n\n' + )}` + ); + } + }); + }); + }); + }); + }); }); }); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index f95dd89..0058466 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -279,6 +279,7 @@ export function defineFeature( if ( parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 + && parsedFeatureWithTagFiltersApplied.rules.length === 0 ) { return; } diff --git a/src/models.ts b/src/models.ts index d1c4747..debc032 100644 --- a/src/models.ts +++ b/src/models.ts @@ -41,10 +41,18 @@ export type ParsedFeature = { title: string; scenarios: ParsedScenario[]; scenarioOutlines: ParsedScenarioOutline[]; + rules: ParsedRule[]; options: Options; tags: string[]; }; +export type ParsedRule = { + title: string; + scenarios: ParsedScenario[]; + scenarioOutlines: ParsedScenarioOutline[]; + tags: string[]; +}; + export type ScenarioNameTemplateVars = { featureTitle: string; scenarioTitle: string; diff --git a/src/parsed-feature-loading.ts b/src/parsed-feature-loading.ts index 6cb8a59..c1df205 100644 --- a/src/parsed-feature-loading.ts +++ b/src/parsed-feature-loading.ts @@ -8,7 +8,7 @@ import AstBuilder from 'gherkin/dist/src/AstBuilder'; import { v4 as uuidv4 } from 'uuid'; import { getJestCucumberConfiguration } from './configuration'; -import { ParsedFeature, ParsedScenario, ParsedStep, ParsedScenarioOutline, Options } from './models'; +import { ParsedFeature, ParsedScenario, ParsedStep, ParsedScenarioOutline, Options, ParsedRule } from './models'; import Dialect from 'gherkin/dist/src/Dialect'; const parseDataTableRow = (astDataTableRow: any) => { @@ -88,6 +88,15 @@ const parseScenario = (astScenario: any) => { } as ParsedScenario; }; +const parseRule = (astRule: any) => { + return { + title: astRule.name, + scenarios: parseScenarios(astRule), + scenarioOutlines: parseScenarioOutlines(astRule), + tags: parseTags(astRule), + } as ParsedRule; +} + const parseScenarioOutlineExampleSteps = (exampleTableRow: any, scenarioSteps: ParsedStep[]) => { return scenarioSteps.map((scenarioStep) => { const stepText = Object.keys(exampleTableRow).reduce((processedStepText, nextTableColumn) => { @@ -199,6 +208,12 @@ const parseScenarios = (astFeature: any) => { .map((astScenario: any) => parseScenario(astScenario.scenario)); }; +const parseRules = (astFeature: any) => { + return astFeature.children + .filter((child: any) => child.rule) + .map((astRule: any) => parseRule(astRule.rule)); +}; + const parseScenarioOutlines = (astFeature: any) => { return astFeature.children .filter((child: any) => { @@ -233,7 +248,7 @@ const parseBackgrounds = (ast: any) => { .map((child: any) => child.background); }; -const collapseRulesAndBackgrounds = (astFeature: any) => { +const collapseBackgroundsOfFeature = (astFeature: any) => { const featureBackgrounds = parseBackgrounds(astFeature); const children = collapseBackgrounds(astFeature.children, featureBackgrounds) @@ -242,13 +257,10 @@ const collapseRulesAndBackgrounds = (astFeature: any) => { const rule = nextChild.rule; const ruleBackgrounds = parseBackgrounds(rule); - return [ - ...newChildren, - ...collapseBackgrounds(rule.children, [...featureBackgrounds, ...ruleBackgrounds]), - ]; - } else { - return [...newChildren, nextChild]; + rule.children = [...collapseBackgrounds(rule.children, [...featureBackgrounds, ...ruleBackgrounds])] } + + return [...newChildren, nextChild]; }, []); return { @@ -330,7 +342,7 @@ export const parseFeature = (featureText: string, options?: Options): ParsedFeat throw new Error(`Error parsing feature Gherkin: ${err.message}`); } - let astFeature = collapseRulesAndBackgrounds(ast.feature); + let astFeature = collapseBackgroundsOfFeature(ast.feature); if (astFeature.language !== 'en') { astFeature = translateKeywords(astFeature); @@ -340,6 +352,7 @@ export const parseFeature = (featureText: string, options?: Options): ParsedFeat title: astFeature.name, scenarios: parseScenarios(astFeature), scenarioOutlines: parseScenarioOutlines(astFeature), + rules: parseRules(astFeature), tags: parseTags(astFeature), options, } as ParsedFeature; diff --git a/src/validation/scenario-validation.ts b/src/validation/scenario-validation.ts index dbd6481..df4a967 100644 --- a/src/validation/scenario-validation.ts +++ b/src/validation/scenario-validation.ts @@ -76,6 +76,13 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( parsedScenarios = parsedScenarios.concat(parsedFeature.scenarioOutlines); } + if (parsedFeature && parsedFeature.rules && parsedFeature.rules.length) { + parsedFeature.rules.forEach( (rule) => { + parsedScenarios = parsedScenarios.concat(rule.scenarios); + parsedScenarios = parsedScenarios.concat(rule.scenarioOutlines); + }) + } + if (parsedFeature.options && parsedFeature.options.errors === false) { return; } From 44bf293d334a5c1eb3b0ad19eb311a33505667bd Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 12:16:03 +0100 Subject: [PATCH 02/29] introduce scenario group --- .../using-latest-gherkin-keywords.steps.ts | 54 ++--- src/feature-definition-creation.ts | 199 ++++++++++++------ src/index.ts | 2 +- src/models.ts | 14 +- src/parsed-feature-loading.ts | 4 +- src/tag-filtering.ts | 19 +- src/validation/scenario-validation.ts | 18 +- 7 files changed, 183 insertions(+), 127 deletions(-) diff --git a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts index c8cf2a0..69eed97 100644 --- a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts +++ b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts @@ -1,10 +1,11 @@ -import { loadFeature, defineFeature, DefineStepFunction } from '../../../../src/'; +import { loadFeature, defineRuleBasedFeature, DefineStepFunction } from '../../../../src/'; import { Calculator, CalculatorOperator } from '../../src/calculator'; const feature = loadFeature('./examples/typescript/specs/features/using-latest-gherkin-keywords.feature'); -defineFeature(feature, (test) => { +defineRuleBasedFeature(feature, (rule) => { + let calculator: Calculator; let output: number | undefined; @@ -46,36 +47,37 @@ defineFeature(feature, (test) => { }); }; - defineRule('When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed' + rule('When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed', (test) => { - test('Subtracting two numbers', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); - andIHaveEnteredXAsTheSecondOperand(and); - whenIPressTheEnterKey(when); - thenTheOutputOfXShouldBeDisplayed(then); - }); + test('Subtracting two numbers', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); + andIHaveEnteredXAsTheSecondOperand(and); + whenIPressTheEnterKey(when); + thenTheOutputOfXShouldBeDisplayed(then); + }); - test('Attempting to subtract without entering a second number', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); + test('Attempting to subtract without entering a second number', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); - and('I have not entered a second operand', () => { - // Nothing to do here - }); + and('I have not entered a second operand', () => { + // Nothing to do here + }); - whenIPressTheEnterKey(when); + whenIPressTheEnterKey(when); - then('no output should be displayed', () => { - expect(output).toBeFalsy(); + then('no output should be displayed', () => { + expect(output).toBeFalsy(); + }); }); - }); - test('Division operations', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); - andIHaveEnteredXAsTheSecondOperand(and); - whenIPressTheEnterKey(when); - thenTheOutputOfXShouldBeDisplayed(then); + test('Division operations', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); + andIHaveEnteredXAsTheSecondOperand(and); + whenIPressTheEnterKey(when); + thenTheOutputOfXShouldBeDisplayed(then); + }); }); }); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index 0058466..f45c301 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -3,12 +3,15 @@ import { ScenarioFromStepDefinitions, FeatureFromStepDefinitions, StepFromStepDefinitions, - ParsedFeature, ParsedScenario, - Options, ParsedScenarioOutline, + ParsedFeature, + ParsedScenario, + Options, + ParsedScenarioOutline, + ScenarioGroup } from './models'; import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, - matchSteps, + matchSteps } from './validation/step-definition-validation'; import { applyTagFilters } from './tag-filtering'; @@ -24,10 +27,17 @@ export type StepsDefinitionCallbackOptions = { export type ScenariosDefinitionCallbackFunction = (defineScenario: DefineScenarioFunctionWithAliases) => void; +export type RulesDefinitionCallbackFunction = (defineRule: DefineRuleFunction) => void; + +export type DefineRuleFunction = ( + ruleTitle: string, + scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction +) => void; + export type DefineScenarioFunction = ( scenarioTitle: string, stepsDefinitionCallback: StepsDefinitionCallbackFunction, - timeout?: number, + timeout?: number ) => void; export type DefineScenarioFunctionWithAliases = DefineScenarioFunction & { @@ -41,23 +51,26 @@ export type DefineStepFunction = (stepMatcher: string | RegExp, stepDefinitionCa const processScenarioTitleTemplate = ( scenarioTitle: string, - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, options: Options, parsedScenario: ParsedScenario, - parsedScenarioOutline: ParsedScenarioOutline, + parsedScenarioOutline: ParsedScenarioOutline ) => { if (options && options.scenarioNameTemplate) { try { - return options && options.scenarioNameTemplate({ - featureTitle: parsedFeature.title, - scenarioTitle: scenarioTitle.toString(), - featureTags: parsedFeature.tags, - scenarioTags: (parsedScenario || parsedScenarioOutline).tags, - }); + return ( + options && + options.scenarioNameTemplate({ + featureTitle: parsedFeature.title, + scenarioTitle: scenarioTitle.toString(), + featureTags: parsedFeature.tags, + scenarioTags: (parsedScenario || parsedScenarioOutline).tags + }) + ); } catch (err) { throw new Error( // tslint:disable-next-line:max-line-length - `An error occurred while executing a scenario name template. \nTemplate:\n${options.scenarioNameTemplate}\nError:${err.message}`, + `An error occurred while executing a scenario name template. \nTemplate:\n${options.scenarioNameTemplate}\nError:${err.message}` ); } } @@ -112,45 +125,50 @@ const defineScenario = ( only: boolean = false, skip: boolean = false, concurrent: boolean = false, - timeout: number | undefined = undefined, + timeout: number | undefined = undefined ) => { const testFunction = getTestFunction(parsedScenario.skippedViaTagFilter, only, skip, concurrent); - testFunction(scenarioTitle, () => { - return scenarioFromStepDefinitions.steps.reduce((promiseChain, nextStep, index) => { - const stepArgument = parsedScenario.steps[index].stepArgument; - const matches = matchSteps( - parsedScenario.steps[index].stepText, - scenarioFromStepDefinitions.steps[index].stepMatcher, - ); - let matchArgs: string[] = []; + testFunction( + scenarioTitle, + () => { + return scenarioFromStepDefinitions.steps.reduce((promiseChain, nextStep, index) => { + const stepArgument = parsedScenario.steps[index].stepArgument; + const matches = matchSteps( + parsedScenario.steps[index].stepText, + scenarioFromStepDefinitions.steps[index].stepMatcher + ); + let matchArgs: string[] = []; - if (matches && (matches as RegExpMatchArray).length) { - matchArgs = (matches as RegExpMatchArray).slice(1); - } + if (matches && (matches as RegExpMatchArray).length) { + matchArgs = (matches as RegExpMatchArray).slice(1); + } - const args = [...matchArgs, stepArgument]; + const args = [ ...matchArgs, stepArgument ]; - return promiseChain.then(() => nextStep.stepFunction(...args)); - }, Promise.resolve()); - }, timeout); + return promiseChain.then(() => nextStep.stepFunction(...args)); + }, Promise.resolve()); + }, + timeout + ); }; const createDefineScenarioFunction = ( featureFromStepDefinitions: FeatureFromStepDefinitions, - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, + options: Options, only: boolean = false, skip: boolean = false, - concurrent: boolean = false, + concurrent: boolean = false ) => { const defineScenarioFunction: DefineScenarioFunction = ( scenarioTitle: string, stepsDefinitionFunctionCallback: StepsDefinitionCallbackFunction, - timeout?: number, + timeout?: number ) => { const scenarioFromStepDefinitions: ScenarioFromStepDefinitions = { title: scenarioTitle, - steps: [], + steps: [] }; featureFromStepDefinitions.scenarios.push(scenarioFromStepDefinitions); @@ -164,55 +182,51 @@ const createDefineScenarioFunction = ( but: createDefineStepFunction(scenarioFromStepDefinitions), pending: () => { // Nothing to do - }, + } }); - const parsedScenario = parsedFeature.scenarios - .filter((s) => s.title.toLowerCase() === scenarioTitle.toLowerCase())[0]; - - const parsedScenarioOutline = parsedFeature.scenarioOutlines - .filter((s) => s.title.toLowerCase() === scenarioTitle.toLowerCase())[0]; + const parsedScenario = parsedFeature.scenarios.filter( + (s) => s.title.toLowerCase() === scenarioTitle.toLowerCase() + )[0]; - const options = parsedFeature.options; + const parsedScenarioOutline = parsedFeature.scenarioOutlines.filter( + (s) => s.title.toLowerCase() === scenarioTitle.toLowerCase() + )[0]; scenarioTitle = processScenarioTitleTemplate( scenarioTitle, parsedFeature, options, parsedScenario, - parsedScenarioOutline, + parsedScenarioOutline ); ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps( options, parsedScenario || parsedScenarioOutline, - scenarioFromStepDefinitions, + scenarioFromStepDefinitions ); if (checkForPendingSteps(scenarioFromStepDefinitions)) { - xtest(scenarioTitle, () => { - // Nothing to do - }, undefined); - } else if (parsedScenario) { - defineScenario( + xtest( scenarioTitle, - scenarioFromStepDefinitions, - parsedScenario, - only, - skip, - concurrent, - timeout, + () => { + // Nothing to do + }, + undefined ); + } else if (parsedScenario) { + defineScenario(scenarioTitle, scenarioFromStepDefinitions, parsedScenario, only, skip, concurrent, timeout); } else if (parsedScenarioOutline) { parsedScenarioOutline.scenarios.forEach((scenario) => { defineScenario( - (scenario.title || scenarioTitle), + scenario.title || scenarioTitle, scenarioFromStepDefinitions, scenario, only, skip, concurrent, - timeout, + timeout ); }); } @@ -223,32 +237,36 @@ const createDefineScenarioFunction = ( const createDefineScenarioFunctionWithAliases = ( featureFromStepDefinitions: FeatureFromStepDefinitions, - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, + options: Options ) => { - const defineScenarioFunctionWithAliases = createDefineScenarioFunction(featureFromStepDefinitions, parsedFeature); + const defineScenarioFunctionWithAliases = createDefineScenarioFunction(featureFromStepDefinitions, parsedFeature, options); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).only = createDefineScenarioFunction( featureFromStepDefinitions, parsedFeature, + options, true, false, - false, + false ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).skip = createDefineScenarioFunction( featureFromStepDefinitions, parsedFeature, + options, false, true, - false, + false ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).concurrent = createDefineScenarioFunction( featureFromStepDefinitions, parsedFeature, + options, false, false, - true, + true ); return defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases; @@ -258,7 +276,7 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD return (stepMatcher: string | RegExp, stepFunction: () => any) => { const stepDefinition: StepFromStepDefinitions = { stepMatcher, - stepFunction, + stepFunction }; scenarioFromStepDefinitions.steps.push(stepDefinition); @@ -267,31 +285,74 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD export function defineFeature( featureFromFile: ParsedFeature, - scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction, + scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction ) { const featureFromDefinedSteps: FeatureFromStepDefinitions = { title: featureFromFile.title, - scenarios: [], + scenarios: [] }; - const parsedFeatureWithTagFiltersApplied = applyTagFilters(featureFromFile); + const parsedFeatureWithTagFiltersApplied = applyTagFilters(featureFromFile, featureFromFile.options.tagFilter); if ( - parsedFeatureWithTagFiltersApplied.scenarios.length === 0 - && parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 - && parsedFeatureWithTagFiltersApplied.rules.length === 0 + parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && + parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 ) { return; } describe(featureFromFile.title, () => { scenariosDefinitionCallback( - createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied), + createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied, featureFromFile.options) ); checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( parsedFeatureWithTagFiltersApplied, - featureFromDefinedSteps, + featureFromDefinedSteps ); }); } + +export function defineRuleBasedFeature( + featureFromFile: ParsedFeature, + rulesDefinitionCallback: RulesDefinitionCallbackFunction +) { + describe(featureFromFile.title, () => { + rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { + + const matchingRules = featureFromFile.rules.filter((rule) => rule.title.toLocaleLowerCase() === ruleText) + if(matchingRules.length != 1) { + // TODO: error + return; + } + + const rule = matchingRules[0]; + + const scenarioGroupFromDefinedSteps: FeatureFromStepDefinitions = { + title: rule.title, + scenarios: [] + }; + + const parsedFeatureWithTagFiltersApplied = applyTagFilters(rule, featureFromFile.options.tagFilter); + + if ( + parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && + parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 + ) { + return; + } + + describe(ruleText, () => { + callback( + createDefineScenarioFunctionWithAliases(scenarioGroupFromDefinedSteps, parsedFeatureWithTagFiltersApplied, featureFromFile.options) + ); + }); + + checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( + parsedFeatureWithTagFiltersApplied, + scenarioGroupFromDefinedSteps, + featureFromFile.options + ); + }); + }); +} diff --git a/src/index.ts b/src/index.ts index b10042d..abef886 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { loadFeature, loadFeatures, parseFeature } from './parsed-feature-loading'; -export { defineFeature, DefineStepFunction } from './feature-definition-creation'; +export { defineFeature, defineRuleBasedFeature, DefineStepFunction } from './feature-definition-creation'; export { setJestCucumberConfiguration } from './configuration'; export { generateCodeFromFeature, diff --git a/src/models.ts b/src/models.ts index debc032..6e1b4a5 100644 --- a/src/models.ts +++ b/src/models.ts @@ -37,20 +37,16 @@ export type ParsedScenarioOutline = { skippedViaTagFilter: boolean; }; -export type ParsedFeature = { +export type ScenarioGroup = { title: string; scenarios: ParsedScenario[]; scenarioOutlines: ParsedScenarioOutline[]; - rules: ParsedRule[]; - options: Options; tags: string[]; -}; +} -export type ParsedRule = { - title: string; - scenarios: ParsedScenario[]; - scenarioOutlines: ParsedScenarioOutline[]; - tags: string[]; +export interface ParsedFeature extends ScenarioGroup { + rules: ScenarioGroup[]; + options: Options; }; export type ScenarioNameTemplateVars = { diff --git a/src/parsed-feature-loading.ts b/src/parsed-feature-loading.ts index c1df205..fb8056c 100644 --- a/src/parsed-feature-loading.ts +++ b/src/parsed-feature-loading.ts @@ -8,7 +8,7 @@ import AstBuilder from 'gherkin/dist/src/AstBuilder'; import { v4 as uuidv4 } from 'uuid'; import { getJestCucumberConfiguration } from './configuration'; -import { ParsedFeature, ParsedScenario, ParsedStep, ParsedScenarioOutline, Options, ParsedRule } from './models'; +import { ParsedFeature, ParsedScenario, ParsedStep, ParsedScenarioOutline, Options, ScenarioGroup} from './models'; import Dialect from 'gherkin/dist/src/Dialect'; const parseDataTableRow = (astDataTableRow: any) => { @@ -94,7 +94,7 @@ const parseRule = (astRule: any) => { scenarios: parseScenarios(astRule), scenarioOutlines: parseScenarioOutlines(astRule), tags: parseTags(astRule), - } as ParsedRule; + } as ScenarioGroup; } const parseScenarioOutlineExampleSteps = (exampleTableRow: any, scenarioSteps: ParsedStep[]) => { diff --git a/src/tag-filtering.ts b/src/tag-filtering.ts index ca502d1..d66330f 100644 --- a/src/tag-filtering.ts +++ b/src/tag-filtering.ts @@ -1,4 +1,4 @@ -import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models'; +import { ParsedFeature, ParsedScenario, ParsedScenarioOutline, ScenarioGroup } from './models'; type TagFilterFunction = (tags: string[]) => boolean; @@ -42,7 +42,7 @@ const convertTagFilterExpressionToFunction = (tagFilterExpression: string) => { const checkIfScenarioMatchesTagFilter = ( tagFilterExpression: string, - feature: ParsedFeature, + feature: ScenarioGroup, scenario: ParsedScenario | ParsedScenarioOutline, ) => { const featureAndScenarioTags = [ @@ -60,9 +60,9 @@ const checkIfScenarioMatchesTagFilter = ( return tagFilterFunction(featureAndScenarioTags); }; -const setScenarioSkipped = (parsedFeature: ParsedFeature, scenario: ParsedScenario) => { +const setScenarioSkipped = (parsedFeature: ScenarioGroup, scenario: ParsedScenario, tagFilter: string) => { const skippedViaTagFilter = !checkIfScenarioMatchesTagFilter( - parsedFeature.options.tagFilter as string, + tagFilter, parsedFeature, scenario, ); @@ -74,18 +74,19 @@ const setScenarioSkipped = (parsedFeature: ParsedFeature, scenario: ParsedScenar }; export const applyTagFilters = ( - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, + tagFilter: string | undefined ) => { - if (parsedFeature.options.tagFilter === undefined) { + if (tagFilter === undefined) { return parsedFeature; } - const scenarios = parsedFeature.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario)); + const scenarios = parsedFeature.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario, tagFilter)); const scenarioOutlines = parsedFeature.scenarioOutlines .map((scenarioOutline) => { return { - ...setScenarioSkipped(parsedFeature, scenarioOutline), - scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario)), + ...setScenarioSkipped(parsedFeature, scenarioOutline, tagFilter), + scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario, tagFilter)), }; }); diff --git a/src/validation/scenario-validation.ts b/src/validation/scenario-validation.ts index df4a967..abd01f6 100644 --- a/src/validation/scenario-validation.ts +++ b/src/validation/scenario-validation.ts @@ -5,6 +5,8 @@ import { ParsedScenario, ParsedScenarioOutline, ErrorOptions, + ScenarioGroup, + Options, } from '../models'; import { generateScenarioCode } from '../code-generation/scenario-generation'; @@ -61,8 +63,9 @@ const findScenarioFromStepDefinitions = ( }; export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, featureFromStepDefinitions: FeatureFromStepDefinitions, + options?: Options ) => { const errors: string[] = []; @@ -76,14 +79,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( parsedScenarios = parsedScenarios.concat(parsedFeature.scenarioOutlines); } - if (parsedFeature && parsedFeature.rules && parsedFeature.rules.length) { - parsedFeature.rules.forEach( (rule) => { - parsedScenarios = parsedScenarios.concat(rule.scenarios); - parsedScenarios = parsedScenarios.concat(rule.scenarioOutlines); - }) - } - - if (parsedFeature.options && parsedFeature.options.errors === false) { + if (options && options.errors === false) { return; } @@ -96,7 +92,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, parsedScenarios, scenarioFromStepDefinitions.title, - parsedFeature.options.errors as ErrorOptions, + options!.errors as ErrorOptions, ); }); } @@ -109,7 +105,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, featureFromStepDefinitions && featureFromStepDefinitions.scenarios, parsedScenario, - parsedFeature.options.errors as ErrorOptions, + options!.errors as ErrorOptions, ); }); } From 848d8343ebf672ae6f1b2dc4cb05a2c5a3e414fc Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 12:28:47 +0100 Subject: [PATCH 03/29] fix scenario file --- .vscode/launch.json | 3 ++- .../specs/features/using-latest-gherkin-keywords.feature | 6 ++---- .../step-definitions/using-latest-gherkin-keywords.steps.ts | 3 +++ src/feature-definition-creation.ts | 5 ++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a856985..a7585b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,8 @@ "runtimeArgs": [ "run-script", "test", "--", - "examples/typescript/specs/step-definitions/auto-step-binding.steps.ts" + //"examples/typescript/specs/step-definitions/auto-step-binding.steps.ts" + "examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts" ], }, { diff --git a/examples/typescript/specs/features/using-latest-gherkin-keywords.feature b/examples/typescript/specs/features/using-latest-gherkin-keywords.feature index 48407fa..c31ca0a 100644 --- a/examples/typescript/specs/features/using-latest-gherkin-keywords.feature +++ b/examples/typescript/specs/features/using-latest-gherkin-keywords.feature @@ -1,7 +1,6 @@ Feature: Using latest Gherkin keywords - Rule: When a number, a minus sign, a number, and equals is entered into the calculator, - the sum should be calculated and displayed + Rule: When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed Example: Subtracting two numbers Given I have entered "4" as the first operand @@ -17,8 +16,7 @@ Feature: Using latest Gherkin keywords When I press the equals key Then no output should be displayed - Rule: When a number, a division sign, a number, and equals is entered into the calculator, - the quotient should be calculated and displayed + Rule: When a number, a division sign, a number, and equals is entered into the calculator, the quotient should be calculated and displayed Scenario Template: Division operations Given I have entered "" as the first operand diff --git a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts index 69eed97..a6427e8 100644 --- a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts +++ b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts @@ -72,6 +72,9 @@ defineRuleBasedFeature(feature, (rule) => { }); }); + }); + + rule("When a number, a division sign, a number, and equals is entered into the calculator, the quotient should be calculated and displayed", (test) => { test('Division operations', ({ given, and, when, then }) => { givenIHaveEnteredXAsTheFirstOperand(given); andIHaveEnteredXAsTheOperator(and); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index f45c301..fa20fc6 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -320,10 +320,9 @@ export function defineRuleBasedFeature( describe(featureFromFile.title, () => { rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { - const matchingRules = featureFromFile.rules.filter((rule) => rule.title.toLocaleLowerCase() === ruleText) + const matchingRules = featureFromFile.rules.filter((rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase()) if(matchingRules.length != 1) { - // TODO: error - return; + throw new Error(`no matching rule found for '${ruleText}'"`) } const rule = matchingRules[0]; From 11598b0de092557692210745561dc01e83187247 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 12:42:01 +0100 Subject: [PATCH 04/29] add auto binding for rules --- .../auto-step-binding.steps.ts | 4 +- src/automatic-step-binding.ts | 71 ++++++++++++++++++- src/index.ts | 2 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts index 06a7d1c..09cd13b 100644 --- a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts +++ b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts @@ -1,4 +1,4 @@ -import { StepDefinitions, loadFeatures, autoBindSteps } from '../../../../src'; +import { StepDefinitions, loadFeatures, autoBindStepsWithRules } from '../../../../src'; import { VendingMachine } from '../../src/vending-machine'; export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) => { @@ -25,4 +25,4 @@ export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) const features = loadFeatures('./examples/typescript/specs/features/auto-binding/**/*.feature'); -autoBindSteps(features, [ vendingMachineSteps ]); +autoBindStepsWithRules(features, [ vendingMachineSteps ]); diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 7b0011e..5265941 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -1,6 +1,6 @@ import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models'; import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, matchSteps } from './validation/step-definition-validation'; -import { StepsDefinitionCallbackFunction, defineFeature } from './feature-definition-creation'; +import { StepsDefinitionCallbackFunction, defineFeature, defineRuleBasedFeature } from './feature-definition-creation'; import { generateStepCode } from './code-generation/step-generation'; const globalSteps: Array<{ stepMatcher: string | RegExp; stepFunction: () => any }> = []; @@ -115,3 +115,72 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD throw new Error(errors.join('\n\n')); } }; + +export const autoBindStepsWithRules = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { + stepDefinitions.forEach((stepDefinitionCallback) => { + stepDefinitionCallback({ + defineStep: registerStep, + given: registerStep, + when: registerStep, + then: registerStep, + and: registerStep, + but: registerStep, + pending: () => { + // Nothing to do + } + }); + }); + + const errors: string[] = []; + + features.forEach((feature) => { + defineRuleBasedFeature(feature, (rule) => { + + feature.rules.forEach((r) => { + rule(r.title, (test) => { + + const scenarioOutlineScenarios = r.scenarioOutlines.map( + (scenarioOutline) => scenarioOutline.scenarios[0] + ); + + const scenarios = [ ...r.scenarios, ...scenarioOutlineScenarios ]; + + scenarios.forEach((scenario) => { + test(scenario.title, (options) => { + scenario.steps.forEach((step, stepIndex) => { + const matches = globalSteps.filter((globalStep) => + matchSteps(step.stepText, globalStep.stepMatcher) + ); + + if (matches.length === 1) { + const match = matches[0]; + + options.defineStep(match.stepMatcher, match.stepFunction); + } else if (matches.length === 0) { + const stepCode = generateStepCode(scenario.steps, stepIndex, false); + // tslint:disable-next-line:max-line-length + errors.push( + `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` + ); + } else { + const matchingCode = matches.map( + (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` + ); + errors.push( + `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( + '\n\n' + )}` + ); + } + }); + }); + }); + }) + }) + }); + }); + + if (errors.length) { + throw new Error(errors.join('\n\n')); + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index abef886..6b44424 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,5 +5,5 @@ export { generateCodeFromFeature, generateCodeWithSeparateFunctionsFromFeature, } from './code-generation/generate-code-by-line-number'; -export { autoBindSteps } from './automatic-step-binding'; +export { autoBindSteps, autoBindStepsWithRules } from './automatic-step-binding'; export { StepsDefinitionCallbackFunction as StepDefinitions } from './feature-definition-creation'; From 6222c12364bed28ea670a9b50581b7f80d5ac13f Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 12:47:20 +0100 Subject: [PATCH 05/29] clean up --- src/automatic-step-binding.ts | 68 +++++------------------------------ 1 file changed, 8 insertions(+), 60 deletions(-) diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 5265941..f6cda85 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -1,5 +1,5 @@ -import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models'; -import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, matchSteps } from './validation/step-definition-validation'; +import { ParsedFeature } from './models'; +import { matchSteps } from './validation/step-definition-validation'; import { StepsDefinitionCallbackFunction, defineFeature, defineRuleBasedFeature } from './feature-definition-creation'; import { generateStepCode } from './code-generation/step-generation'; @@ -9,8 +9,7 @@ const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) => globalSteps.push({ stepMatcher, stepFunction }); }; -export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { - stepDefinitions.forEach((stepDefinitionCallback) => { +const registerSteps = (stepDefinitionCallback: StepsDefinitionCallbackFunction) => { stepDefinitionCallback({ defineStep: registerStep, given: registerStep, @@ -22,7 +21,10 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD // Nothing to do } }); - }); + } + +export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { + stepDefinitions.forEach(registerSteps); const errors: string[] = []; @@ -66,48 +68,6 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD }); }); - feature.rules.forEach((rule) => { - - describe(rule.title, () => { - const scenarioPath = feature.title + ' -> ' + rule.title; - const scenarioOutlineScenarios = rule.scenarioOutlines.map( - (scenarioOutline) => scenarioOutline.scenarios[0] - ); - - const ruleScenarios = [ ...rule.scenarios, ...scenarioOutlineScenarios ]; - - ruleScenarios.forEach((scenario) => { - test(scenario.title, (options) => { - scenario.steps.forEach((step, stepIndex) => { - const matches = globalSteps.filter((globalStep) => - matchSteps(step.stepText, globalStep.stepMatcher) - ); - - if (matches.length === 1) { - const match = matches[0]; - - options.defineStep(match.stepMatcher, match.stepFunction); - } else if (matches.length === 0) { - const stepCode = generateStepCode(scenario.steps, stepIndex, false); - // tslint:disable-next-line:max-line-length - errors.push( - `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" at "${scenarioPath}". Please add the following step code: \n\n${stepCode}` - ); - } else { - const matchingCode = matches.map( - (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` - ); - errors.push( - `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" "${scenarioPath}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( - '\n\n' - )}` - ); - } - }); - }); - }); - }); - }); }); }); @@ -117,19 +77,7 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD }; export const autoBindStepsWithRules = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { - stepDefinitions.forEach((stepDefinitionCallback) => { - stepDefinitionCallback({ - defineStep: registerStep, - given: registerStep, - when: registerStep, - then: registerStep, - and: registerStep, - but: registerStep, - pending: () => { - // Nothing to do - } - }); - }); + stepDefinitions.forEach(registerSteps); const errors: string[] = []; From 206478ee6fe3ec464b9c12370ea07df66140661c Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 13:07:20 +0100 Subject: [PATCH 06/29] fix tests --- .../step-definitions/backgrounds.steps.ts | 69 +++++---- .../specs/step-definitions/language.steps.ts | 138 +++++++++--------- src/feature-definition-creation.ts | 3 +- src/validation/scenario-validation.ts | 6 +- 4 files changed, 112 insertions(+), 104 deletions(-) diff --git a/examples/typescript/specs/step-definitions/backgrounds.steps.ts b/examples/typescript/specs/step-definitions/backgrounds.steps.ts index eb1840e..963e130 100644 --- a/examples/typescript/specs/step-definitions/backgrounds.steps.ts +++ b/examples/typescript/specs/step-definitions/backgrounds.steps.ts @@ -1,9 +1,9 @@ -import { loadFeature, defineFeature, DefineStepFunction } from '../../../../src/'; +import { loadFeature, DefineStepFunction, defineRuleBasedFeature } from '../../../../src/'; import { ArcadeMachine, COIN_TYPES, CoinStatus } from '../../src/arcade-machine'; const feature = loadFeature('./examples/typescript/specs/features/backgrounds.feature'); -defineFeature(feature, (test) => { +defineRuleBasedFeature(feature, (rule) => { let arcadeMachine: ArcadeMachine; beforeEach(() => { @@ -22,51 +22,56 @@ defineFeature(feature, (test) => { }); }; - test('Successfully inserting coins', ({ given, when, then }) => { - givenMyMachineIsConfiguredToRequireCoins(given); + rule('When a coin is inserted, the balance should increase by the amount of the coin', (test) => { + test('Successfully inserting coins', ({ given, when, then }) => { + givenMyMachineIsConfiguredToRequireCoins(given); - given('I have not inserted any coins', () => { - arcadeMachine.balance = 0; - }); + given('I have not inserted any coins', () => { + arcadeMachine.balance = 0; + }); - when('I insert one US quarter', () => { - arcadeMachine.insertCoin(COIN_TYPES.USQuarter); - }); + when('I insert one US quarter', () => { + arcadeMachine.insertCoin(COIN_TYPES.USQuarter); + }); - then(/^I should have a balance of (\d+) cents$/, (balance) => { - arcadeMachine.balance = balance / 100; + then(/^I should have a balance of (\d+) cents$/, (balance) => { + arcadeMachine.balance = balance / 100; + }); }); }); - test('Inserting a Canadian coin', ({ given, when, then }) => { - let coinStatus: CoinStatus; + rule('When a coin is not recognized as valid, it should be returned', (test) => { + test('Inserting a Canadian coin', ({ given, when, then }) => { + let coinStatus: CoinStatus; - givenMyMachineIsConfiguredToRequireCoins(given); + givenMyMachineIsConfiguredToRequireCoins(given); - givenMyMachineIsConfiguredToAcceptUsQuarters(given); + givenMyMachineIsConfiguredToAcceptUsQuarters(given); - when('I insert a Canadian Quarter', () => { - coinStatus = arcadeMachine.insertCoin(COIN_TYPES.CanadianQuarter); - }); + when('I insert a Canadian Quarter', () => { + coinStatus = arcadeMachine.insertCoin(COIN_TYPES.CanadianQuarter); + }); - then('my coin should be returned', () => { - expect(coinStatus).toBe('CoinReturned'); + then('my coin should be returned', () => { + expect(coinStatus).toBe('CoinReturned'); + }); }); - }); - test('Inserting a badly damaged coin', ({ given, when, then }) => { - let coinStatus: CoinStatus; + test('Inserting a badly damaged coin', ({ given, when, then }) => { + let coinStatus: CoinStatus; - givenMyMachineIsConfiguredToRequireCoins(given); + givenMyMachineIsConfiguredToRequireCoins(given); - givenMyMachineIsConfiguredToAcceptUsQuarters(given); + givenMyMachineIsConfiguredToAcceptUsQuarters(given); - when('I insert a US Quarter that is badly damaged', () => { - coinStatus = arcadeMachine.insertCoin(COIN_TYPES.Unknown); - }); + when('I insert a US Quarter that is badly damaged', () => { + coinStatus = arcadeMachine.insertCoin(COIN_TYPES.Unknown); + }); - then('my coin should be returned', () => { - expect(coinStatus).toBe('CoinReturned'); + then('my coin should be returned', () => { + expect(coinStatus).toBe('CoinReturned'); + }); }); - }); + + }) }); diff --git a/examples/typescript/specs/step-definitions/language.steps.ts b/examples/typescript/specs/step-definitions/language.steps.ts index 60cfff3..faf3af6 100644 --- a/examples/typescript/specs/step-definitions/language.steps.ts +++ b/examples/typescript/specs/step-definitions/language.steps.ts @@ -1,4 +1,4 @@ -import { loadFeature, defineFeature } from '../../../../src/'; +import { loadFeature, defineRuleBasedFeature } from '../../../../src/'; import { PasswordValidator } from '../../src/password-validator'; import { OnlineSales } from '../../src/online-sales'; import { BankAccount } from '../../src/bank-account'; @@ -6,101 +6,103 @@ import { TodoList } from '../../src/todo-list'; const feature = loadFeature('./examples/typescript/specs/features/language.feature'); -defineFeature(feature, (test) => { - describe('basic-scenarios', () => { - let passwordValidator = new PasswordValidator(); - let accessGranted = false; +defineRuleBasedFeature(feature, (rule) => { + rule('Regel is niet vertaalt', (test) => { + describe('basic-scenarios', () => { + let passwordValidator = new PasswordValidator(); + let accessGranted = false; - beforeEach(() => { - passwordValidator = new PasswordValidator(); - }); - - test('Invullen van een correct wachtwoord', ({ given, when, then }) => { - given('ik heb voorheen een wachtwoord aangemaakt', () => { - passwordValidator.setPassword('1234'); + beforeEach(() => { + passwordValidator = new PasswordValidator(); }); - when('ik het correcte wachtwoord invoer', () => { - accessGranted = passwordValidator.validatePassword('1234'); - }); + test('Invullen van een correct wachtwoord', ({ given, when, then }) => { + given('ik heb voorheen een wachtwoord aangemaakt', () => { + passwordValidator.setPassword('1234'); + }); - then('krijg ik toegang', () => { - expect(accessGranted).toBe(true); + when('ik het correcte wachtwoord invoer', () => { + accessGranted = passwordValidator.validatePassword('1234'); + }); + + then('krijg ik toegang', () => { + expect(accessGranted).toBe(true); + }); }); }); - }); - describe('scenario-outlines', () => { - test('Verkoop voor €', ({ given, when, then }) => { - const onlineSales = new OnlineSales(); - let salesPrice: number | null; + describe('scenario-outlines', () => { + test('Verkoop voor €', ({ given, when, then }) => { + const onlineSales = new OnlineSales(); + let salesPrice: number | null; - given(/^ik heb een (.*)$/, (item) => { - onlineSales.listItem(item); - }); + given(/^ik heb een (.*)$/, (item) => { + onlineSales.listItem(item); + }); - when(/^ik (.*) verkoop$/, (item) => { - salesPrice = onlineSales.sellItem(item); - }); + when(/^ik (.*) verkoop$/, (item) => { + salesPrice = onlineSales.sellItem(item); + }); - then(/^zou ik €(\d+) ontvangen$/, (expectedSalesPrice) => { - expect(salesPrice).toBe(parseInt(expectedSalesPrice, 10)); + then(/^zou ik €(\d+) ontvangen$/, (expectedSalesPrice) => { + expect(salesPrice).toBe(parseInt(expectedSalesPrice, 10)); + }); }); }); - }); - - describe('using-dynamic-values', () => { - let myAccount: BankAccount; - beforeEach(() => { - myAccount = new BankAccount(); - }); + describe('using-dynamic-values', () => { + let myAccount: BankAccount; - test('Mijn salaris storten', ({ given, when, then, pending }) => { - given(/^mijn account balans is \€(\d+)$/, (balance) => { - myAccount.deposit(parseInt(balance, 10)); + beforeEach(() => { + myAccount = new BankAccount(); }); - when(/^ik \€(\d+) krijg betaald voor het schrijven van geweldige code$/, (paycheck) => { - myAccount.deposit(parseInt(paycheck, 10)); - }); + test('Mijn salaris storten', ({ given, when, then, pending }) => { + given(/^mijn account balans is \€(\d+)$/, (balance) => { + myAccount.deposit(parseInt(balance, 10)); + }); + + when(/^ik \€(\d+) krijg betaald voor het schrijven van geweldige code$/, (paycheck) => { + myAccount.deposit(parseInt(paycheck, 10)); + }); - then(/^zou mijn account balans \€(\d+) zijn$/, (expectedBalance) => { - expect(myAccount.balance).toBe(parseInt(expectedBalance, 10)); + then(/^zou mijn account balans \€(\d+) zijn$/, (expectedBalance) => { + expect(myAccount.balance).toBe(parseInt(expectedBalance, 10)); + }); }); }); - }); - describe('using-gherkin-tables', () => { - let todoList: TodoList; + describe('using-gherkin-tables', () => { + let todoList: TodoList; - beforeEach(() => { - todoList = new TodoList(); - }); + beforeEach(() => { + todoList = new TodoList(); + }); - test('een artikel toevoegen aan mijn takenlijst', ({ given, when, then }) => { - given('mijn ziet mijn takenlijst er zo uit:', (table: any[]) => { - table.forEach((row: any) => { - todoList.add({ - name: row.TaakNaam, - priority: row.Prioriteit, + test('een artikel toevoegen aan mijn takenlijst', ({ given, when, then }) => { + given('mijn ziet mijn takenlijst er zo uit:', (table: any[]) => { + table.forEach((row: any) => { + todoList.add({ + name: row.TaakNaam, + priority: row.Prioriteit + }); }); }); - }); - when('ik de volgende taken toevoeg:', (table: any) => { - todoList.add({ - name: table[0].TaakNaam, - priority: table[0].Prioriteit, + when('ik de volgende taken toevoeg:', (table: any) => { + todoList.add({ + name: table[0].TaakNaam, + priority: table[0].Prioriteit + }); }); - }); - then('zou ik de volgende takenlijst zien:', (table: any[]) => { - expect(todoList.items.length).toBe(table.length); + then('zou ik de volgende takenlijst zien:', (table: any[]) => { + expect(todoList.items.length).toBe(table.length); - table.forEach((row: any, index) => { - expect(todoList.items[index].name).toBe(table[index].TaakNaam); - expect(todoList.items[index].priority).toBe(table[index].Prioriteit); + table.forEach((row: any, index) => { + expect(todoList.items[index].name).toBe(table[index].TaakNaam); + expect(todoList.items[index].priority).toBe(table[index].Prioriteit); + }); }); }); }); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index fa20fc6..604fe5e 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -308,7 +308,8 @@ export function defineFeature( checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( parsedFeatureWithTagFiltersApplied, - featureFromDefinedSteps + featureFromDefinedSteps, + featureFromFile.options ); }); } diff --git a/src/validation/scenario-validation.ts b/src/validation/scenario-validation.ts index abd01f6..adff65f 100644 --- a/src/validation/scenario-validation.ts +++ b/src/validation/scenario-validation.ts @@ -65,7 +65,7 @@ const findScenarioFromStepDefinitions = ( export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( parsedFeature: ScenarioGroup, featureFromStepDefinitions: FeatureFromStepDefinitions, - options?: Options + options: Options ) => { const errors: string[] = []; @@ -92,7 +92,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, parsedScenarios, scenarioFromStepDefinitions.title, - options!.errors as ErrorOptions, + options.errors as ErrorOptions, ); }); } @@ -105,7 +105,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, featureFromStepDefinitions && featureFromStepDefinitions.scenarios, parsedScenario, - options!.errors as ErrorOptions, + options.errors as ErrorOptions, ); }); } From b1ad3e22a9fdda1d5fb9b26f8db915e006e06eef Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 13:37:47 +0100 Subject: [PATCH 07/29] introduce 'collapseRules' option --- .../specs/step-definitions/language.steps.ts | 138 +++++++++--------- .../using-latest-gherkin-keywords.steps.ts | 2 +- src/configuration.ts | 1 + src/models.ts | 1 + src/parsed-feature-loading.ts | 19 +++ 5 files changed, 90 insertions(+), 71 deletions(-) diff --git a/examples/typescript/specs/step-definitions/language.steps.ts b/examples/typescript/specs/step-definitions/language.steps.ts index faf3af6..60cfff3 100644 --- a/examples/typescript/specs/step-definitions/language.steps.ts +++ b/examples/typescript/specs/step-definitions/language.steps.ts @@ -1,4 +1,4 @@ -import { loadFeature, defineRuleBasedFeature } from '../../../../src/'; +import { loadFeature, defineFeature } from '../../../../src/'; import { PasswordValidator } from '../../src/password-validator'; import { OnlineSales } from '../../src/online-sales'; import { BankAccount } from '../../src/bank-account'; @@ -6,103 +6,101 @@ import { TodoList } from '../../src/todo-list'; const feature = loadFeature('./examples/typescript/specs/features/language.feature'); -defineRuleBasedFeature(feature, (rule) => { - rule('Regel is niet vertaalt', (test) => { - describe('basic-scenarios', () => { - let passwordValidator = new PasswordValidator(); - let accessGranted = false; +defineFeature(feature, (test) => { + describe('basic-scenarios', () => { + let passwordValidator = new PasswordValidator(); + let accessGranted = false; - beforeEach(() => { - passwordValidator = new PasswordValidator(); - }); + beforeEach(() => { + passwordValidator = new PasswordValidator(); + }); - test('Invullen van een correct wachtwoord', ({ given, when, then }) => { - given('ik heb voorheen een wachtwoord aangemaakt', () => { - passwordValidator.setPassword('1234'); - }); + test('Invullen van een correct wachtwoord', ({ given, when, then }) => { + given('ik heb voorheen een wachtwoord aangemaakt', () => { + passwordValidator.setPassword('1234'); + }); - when('ik het correcte wachtwoord invoer', () => { - accessGranted = passwordValidator.validatePassword('1234'); - }); + when('ik het correcte wachtwoord invoer', () => { + accessGranted = passwordValidator.validatePassword('1234'); + }); - then('krijg ik toegang', () => { - expect(accessGranted).toBe(true); - }); + then('krijg ik toegang', () => { + expect(accessGranted).toBe(true); }); }); + }); - describe('scenario-outlines', () => { - test('Verkoop voor €', ({ given, when, then }) => { - const onlineSales = new OnlineSales(); - let salesPrice: number | null; + describe('scenario-outlines', () => { + test('Verkoop voor €', ({ given, when, then }) => { + const onlineSales = new OnlineSales(); + let salesPrice: number | null; - given(/^ik heb een (.*)$/, (item) => { - onlineSales.listItem(item); - }); + given(/^ik heb een (.*)$/, (item) => { + onlineSales.listItem(item); + }); - when(/^ik (.*) verkoop$/, (item) => { - salesPrice = onlineSales.sellItem(item); - }); + when(/^ik (.*) verkoop$/, (item) => { + salesPrice = onlineSales.sellItem(item); + }); - then(/^zou ik €(\d+) ontvangen$/, (expectedSalesPrice) => { - expect(salesPrice).toBe(parseInt(expectedSalesPrice, 10)); - }); + then(/^zou ik €(\d+) ontvangen$/, (expectedSalesPrice) => { + expect(salesPrice).toBe(parseInt(expectedSalesPrice, 10)); }); }); + }); - describe('using-dynamic-values', () => { - let myAccount: BankAccount; + describe('using-dynamic-values', () => { + let myAccount: BankAccount; - beforeEach(() => { - myAccount = new BankAccount(); - }); + beforeEach(() => { + myAccount = new BankAccount(); + }); - test('Mijn salaris storten', ({ given, when, then, pending }) => { - given(/^mijn account balans is \€(\d+)$/, (balance) => { - myAccount.deposit(parseInt(balance, 10)); - }); + test('Mijn salaris storten', ({ given, when, then, pending }) => { + given(/^mijn account balans is \€(\d+)$/, (balance) => { + myAccount.deposit(parseInt(balance, 10)); + }); - when(/^ik \€(\d+) krijg betaald voor het schrijven van geweldige code$/, (paycheck) => { - myAccount.deposit(parseInt(paycheck, 10)); - }); + when(/^ik \€(\d+) krijg betaald voor het schrijven van geweldige code$/, (paycheck) => { + myAccount.deposit(parseInt(paycheck, 10)); + }); - then(/^zou mijn account balans \€(\d+) zijn$/, (expectedBalance) => { - expect(myAccount.balance).toBe(parseInt(expectedBalance, 10)); - }); + then(/^zou mijn account balans \€(\d+) zijn$/, (expectedBalance) => { + expect(myAccount.balance).toBe(parseInt(expectedBalance, 10)); }); }); + }); - describe('using-gherkin-tables', () => { - let todoList: TodoList; + describe('using-gherkin-tables', () => { + let todoList: TodoList; - beforeEach(() => { - todoList = new TodoList(); - }); + beforeEach(() => { + todoList = new TodoList(); + }); - test('een artikel toevoegen aan mijn takenlijst', ({ given, when, then }) => { - given('mijn ziet mijn takenlijst er zo uit:', (table: any[]) => { - table.forEach((row: any) => { - todoList.add({ - name: row.TaakNaam, - priority: row.Prioriteit - }); + test('een artikel toevoegen aan mijn takenlijst', ({ given, when, then }) => { + given('mijn ziet mijn takenlijst er zo uit:', (table: any[]) => { + table.forEach((row: any) => { + todoList.add({ + name: row.TaakNaam, + priority: row.Prioriteit, }); }); + }); - when('ik de volgende taken toevoeg:', (table: any) => { - todoList.add({ - name: table[0].TaakNaam, - priority: table[0].Prioriteit - }); + when('ik de volgende taken toevoeg:', (table: any) => { + todoList.add({ + name: table[0].TaakNaam, + priority: table[0].Prioriteit, }); + }); - then('zou ik de volgende takenlijst zien:', (table: any[]) => { - expect(todoList.items.length).toBe(table.length); + then('zou ik de volgende takenlijst zien:', (table: any[]) => { + expect(todoList.items.length).toBe(table.length); - table.forEach((row: any, index) => { - expect(todoList.items[index].name).toBe(table[index].TaakNaam); - expect(todoList.items[index].priority).toBe(table[index].Prioriteit); - }); + table.forEach((row: any, index) => { + expect(todoList.items[index].name).toBe(table[index].TaakNaam); + expect(todoList.items[index].priority).toBe(table[index].Prioriteit); }); }); }); diff --git a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts index a6427e8..ddb7b70 100644 --- a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts +++ b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts @@ -2,7 +2,7 @@ import { loadFeature, defineRuleBasedFeature, DefineStepFunction } from '../../. import { Calculator, CalculatorOperator } from '../../src/calculator'; -const feature = loadFeature('./examples/typescript/specs/features/using-latest-gherkin-keywords.feature'); +const feature = loadFeature('./examples/typescript/specs/features/using-latest-gherkin-keywords.feature', {collapseRules: false}); defineRuleBasedFeature(feature, (rule) => { diff --git a/src/configuration.ts b/src/configuration.ts index bc02499..2b58fa6 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -10,6 +10,7 @@ const defaultErrorSettings = { const defaultConfiguration: Options = { tagFilter: undefined, scenarioNameTemplate: undefined, + collapseRules: true, errors: defaultErrorSettings, }; diff --git a/src/models.ts b/src/models.ts index 6e1b4a5..9931a4b 100644 --- a/src/models.ts +++ b/src/models.ts @@ -66,6 +66,7 @@ export type ErrorOptions = { export type Options = { loadRelativePath?: boolean; tagFilter?: string; + collapseRules?: boolean; errors?: ErrorOptions | boolean; scenarioNameTemplate?: (vars: ScenarioNameTemplateVars) => string; }; diff --git a/src/parsed-feature-loading.ts b/src/parsed-feature-loading.ts index fb8056c..730e47e 100644 --- a/src/parsed-feature-loading.ts +++ b/src/parsed-feature-loading.ts @@ -269,6 +269,21 @@ const collapseBackgroundsOfFeature = (astFeature: any) => { }; }; +const collapseRules = (astFeature: any) => { + const children = astFeature.children.reduce((newChildren: [], nextChild:any) => { + if(nextChild.rule) { + return [...newChildren, ...nextChild.rule.children]; + } + else { + return [...newChildren, ...nextChild]; + } + }, []) + return { + ...astFeature, + children, + } +} + const translateKeywords = (astFeature: any) => { const languageDialect = Gherkins.dialects()[astFeature.language]; const translationMap = createTranslationMap(languageDialect); @@ -344,6 +359,10 @@ export const parseFeature = (featureText: string, options?: Options): ParsedFeat let astFeature = collapseBackgroundsOfFeature(ast.feature); + if(options?.collapseRules) { + astFeature = collapseRules(astFeature); + } + if (astFeature.language !== 'en') { astFeature = translateKeywords(astFeature); } From 23bb00bb0fd95c1ee1878d25b3e10a24bc68903f Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 13:47:43 +0100 Subject: [PATCH 08/29] first working version --- .vscode/launch.json | 3 ++- .../specs/step-definitions/auto-step-binding.steps.ts | 2 +- .../typescript/specs/step-definitions/backgrounds.steps.ts | 2 +- src/parsed-feature-loading.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a7585b0..9812793 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,8 @@ "run-script", "test", "--", //"examples/typescript/specs/step-definitions/auto-step-binding.steps.ts" - "examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts" +// "examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts" + "examples/typescript/specs/step-definitions/language.steps.ts" ], }, { diff --git a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts index 09cd13b..75afcf3 100644 --- a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts +++ b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts @@ -23,6 +23,6 @@ export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) }); }; -const features = loadFeatures('./examples/typescript/specs/features/auto-binding/**/*.feature'); +const features = loadFeatures('./examples/typescript/specs/features/auto-binding/**/*.feature', {collapseRules: false}); autoBindStepsWithRules(features, [ vendingMachineSteps ]); diff --git a/examples/typescript/specs/step-definitions/backgrounds.steps.ts b/examples/typescript/specs/step-definitions/backgrounds.steps.ts index 963e130..bbbc9c1 100644 --- a/examples/typescript/specs/step-definitions/backgrounds.steps.ts +++ b/examples/typescript/specs/step-definitions/backgrounds.steps.ts @@ -1,7 +1,7 @@ import { loadFeature, DefineStepFunction, defineRuleBasedFeature } from '../../../../src/'; import { ArcadeMachine, COIN_TYPES, CoinStatus } from '../../src/arcade-machine'; -const feature = loadFeature('./examples/typescript/specs/features/backgrounds.feature'); +const feature = loadFeature('./examples/typescript/specs/features/backgrounds.feature', {collapseRules: false}); defineRuleBasedFeature(feature, (rule) => { let arcadeMachine: ArcadeMachine; diff --git a/src/parsed-feature-loading.ts b/src/parsed-feature-loading.ts index 730e47e..6d2ce94 100644 --- a/src/parsed-feature-loading.ts +++ b/src/parsed-feature-loading.ts @@ -275,7 +275,7 @@ const collapseRules = (astFeature: any) => { return [...newChildren, ...nextChild.rule.children]; } else { - return [...newChildren, ...nextChild]; + return [...newChildren, nextChild]; } }, []) return { From ba402104880f8453023e962b9d745f0791bcde8c Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 13:53:00 +0100 Subject: [PATCH 09/29] refactor --- src/feature-definition-creation.ts | 59 ++++++++++++++++++------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index 604fe5e..070c6db 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -240,7 +240,11 @@ const createDefineScenarioFunctionWithAliases = ( parsedFeature: ScenarioGroup, options: Options ) => { - const defineScenarioFunctionWithAliases = createDefineScenarioFunction(featureFromStepDefinitions, parsedFeature, options); + const defineScenarioFunctionWithAliases = createDefineScenarioFunction( + featureFromStepDefinitions, + parsedFeature, + options + ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).only = createDefineScenarioFunction( featureFromStepDefinitions, @@ -296,14 +300,18 @@ export function defineFeature( if ( parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && - parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 + parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 ) { return; } describe(featureFromFile.title, () => { scenariosDefinitionCallback( - createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied, featureFromFile.options) + createDefineScenarioFunctionWithAliases( + featureFromDefinedSteps, + parsedFeatureWithTagFiltersApplied, + featureFromFile.options + ) ); checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( @@ -318,33 +326,38 @@ export function defineRuleBasedFeature( featureFromFile: ParsedFeature, rulesDefinitionCallback: RulesDefinitionCallbackFunction ) { - describe(featureFromFile.title, () => { - rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { - - const matchingRules = featureFromFile.rules.filter((rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase()) - if(matchingRules.length != 1) { - throw new Error(`no matching rule found for '${ruleText}'"`) - } + rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { + const matchingRules = featureFromFile.rules.filter( + (rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase() + ); + if (matchingRules.length != 1) { + throw new Error(`no matching rule found for '${ruleText}'"`); + } - const rule = matchingRules[0]; + const rule = matchingRules[0]; - const scenarioGroupFromDefinedSteps: FeatureFromStepDefinitions = { - title: rule.title, - scenarios: [] - }; + const scenarioGroupFromDefinedSteps: FeatureFromStepDefinitions = { + title: rule.title, + scenarios: [] + }; - const parsedFeatureWithTagFiltersApplied = applyTagFilters(rule, featureFromFile.options.tagFilter); + const parsedFeatureWithTagFiltersApplied = applyTagFilters(rule, featureFromFile.options.tagFilter); - if ( - parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && - parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 - ) { - return; - } + if ( + parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && + parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 + ) { + return; + } + describe(featureFromFile.title, () => { describe(ruleText, () => { callback( - createDefineScenarioFunctionWithAliases(scenarioGroupFromDefinedSteps, parsedFeatureWithTagFiltersApplied, featureFromFile.options) + createDefineScenarioFunctionWithAliases( + scenarioGroupFromDefinedSteps, + parsedFeatureWithTagFiltersApplied, + featureFromFile.options + ) ); }); From 3d1f1892a5410b708ca74989ddc6f0436f2c862c Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 14:29:23 +0100 Subject: [PATCH 10/29] refactoring --- src/feature-definition-creation.ts | 90 +++++++++++------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index 070c6db..bd6b114 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -287,16 +287,17 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD }; }; -export function defineFeature( - featureFromFile: ParsedFeature, - scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction -) { +const defineScenarioGroup = ( + group: ScenarioGroup, + scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction, + options: Options +) => { const featureFromDefinedSteps: FeatureFromStepDefinitions = { - title: featureFromFile.title, + title: group.title, scenarios: [] }; - const parsedFeatureWithTagFiltersApplied = applyTagFilters(featureFromFile, featureFromFile.options.tagFilter); + const parsedFeatureWithTagFiltersApplied = applyTagFilters(group, options.tagFilter); if ( parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && @@ -305,20 +306,23 @@ export function defineFeature( return; } - describe(featureFromFile.title, () => { - scenariosDefinitionCallback( - createDefineScenarioFunctionWithAliases( - featureFromDefinedSteps, - parsedFeatureWithTagFiltersApplied, - featureFromFile.options - ) - ); + scenariosDefinitionCallback( + createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied, options) + ); - checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( - parsedFeatureWithTagFiltersApplied, - featureFromDefinedSteps, - featureFromFile.options - ); + checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( + parsedFeatureWithTagFiltersApplied, + featureFromDefinedSteps, + options + ); +}; + +export function defineFeature( + featureFromFile: ParsedFeature, + scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction +) { + describe(featureFromFile.title, () => { + defineScenarioGroup(featureFromFile, scenariosDefinitionCallback, featureFromFile.options); }); } @@ -326,46 +330,16 @@ export function defineRuleBasedFeature( featureFromFile: ParsedFeature, rulesDefinitionCallback: RulesDefinitionCallbackFunction ) { - rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { - const matchingRules = featureFromFile.rules.filter( - (rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase() - ); - if (matchingRules.length != 1) { - throw new Error(`no matching rule found for '${ruleText}'"`); - } - - const rule = matchingRules[0]; - - const scenarioGroupFromDefinedSteps: FeatureFromStepDefinitions = { - title: rule.title, - scenarios: [] - }; - - const parsedFeatureWithTagFiltersApplied = applyTagFilters(rule, featureFromFile.options.tagFilter); - - if ( - parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && - parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 - ) { - return; - } - - describe(featureFromFile.title, () => { - describe(ruleText, () => { - callback( - createDefineScenarioFunctionWithAliases( - scenarioGroupFromDefinedSteps, - parsedFeatureWithTagFiltersApplied, - featureFromFile.options - ) - ); - }); - - checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( - parsedFeatureWithTagFiltersApplied, - scenarioGroupFromDefinedSteps, - featureFromFile.options + describe(featureFromFile.title, () => { + rulesDefinitionCallback((ruleText: string, callback: ScenariosDefinitionCallbackFunction) => { + const matchingRules = featureFromFile.rules.filter( + (rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase() ); + if (matchingRules.length != 1) { + throw new Error(`no matching rule found for '${ruleText}'"`); + } + + defineScenarioGroup(matchingRules[0], callback, featureFromFile.options); }); }); } From 295418b47d4eb07262316bc986ae3e586afea476 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 14:40:54 +0100 Subject: [PATCH 11/29] refactor --- src/automatic-step-binding.ts | 161 ++++++++++++++-------------------- 1 file changed, 65 insertions(+), 96 deletions(-) diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index f6cda85..0c040a2 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -1,6 +1,11 @@ -import { ParsedFeature } from './models'; +import { ParsedFeature, ScenarioGroup } from './models'; import { matchSteps } from './validation/step-definition-validation'; -import { StepsDefinitionCallbackFunction, defineFeature, defineRuleBasedFeature } from './feature-definition-creation'; +import { + StepsDefinitionCallbackFunction, + defineFeature, + defineRuleBasedFeature, + DefineScenarioFunctionWithAliases +} from './feature-definition-creation'; import { generateStepCode } from './code-generation/step-generation'; const globalSteps: Array<{ stepMatcher: string | RegExp; stepFunction: () => any }> = []; @@ -10,18 +15,53 @@ const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) => }; const registerSteps = (stepDefinitionCallback: StepsDefinitionCallbackFunction) => { - stepDefinitionCallback({ - defineStep: registerStep, - given: registerStep, - when: registerStep, - then: registerStep, - and: registerStep, - but: registerStep, - pending: () => { - // Nothing to do - } + stepDefinitionCallback({ + defineStep: registerStep, + given: registerStep, + when: registerStep, + then: registerStep, + and: registerStep, + but: registerStep, + pending: () => { + // Nothing to do + } + }); +}; + +const matchAndDefineSteps = (feature: ScenarioGroup, test: DefineScenarioFunctionWithAliases, errors: string[]) => { + const scenarioOutlineScenarios = feature.scenarioOutlines.map((scenarioOutline) => scenarioOutline.scenarios[0]); + + const scenarios = [ ...feature.scenarios, ...scenarioOutlineScenarios ]; + + scenarios.forEach((scenario) => { + test(scenario.title, (options) => { + scenario.steps.forEach((step, stepIndex) => { + const matches = globalSteps.filter((globalStep) => matchSteps(step.stepText, globalStep.stepMatcher)); + + if (matches.length === 1) { + const match = matches[0]; + + options.defineStep(match.stepMatcher, match.stepFunction); + } else if (matches.length === 0) { + const stepCode = generateStepCode(scenario.steps, stepIndex, false); + // tslint:disable-next-line:max-line-length + errors.push( + `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` + ); + } else { + const matchingCode = matches.map( + (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` + ); + errors.push( + `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( + '\n\n' + )}` + ); + } + }); }); - } + }); +}; export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { stepDefinitions.forEach(registerSteps); @@ -30,44 +70,7 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD features.forEach((feature) => { defineFeature(feature, (test) => { - - const scenarioOutlineScenarios = feature.scenarioOutlines.map( - (scenarioOutline) => scenarioOutline.scenarios[0] - ); - - const scenarios = [ ...feature.scenarios, ...scenarioOutlineScenarios ]; - - scenarios.forEach((scenario) => { - test(scenario.title, (options) => { - scenario.steps.forEach((step, stepIndex) => { - const matches = globalSteps.filter((globalStep) => - matchSteps(step.stepText, globalStep.stepMatcher) - ); - - if (matches.length === 1) { - const match = matches[0]; - - options.defineStep(match.stepMatcher, match.stepFunction); - } else if (matches.length === 0) { - const stepCode = generateStepCode(scenario.steps, stepIndex, false); - // tslint:disable-next-line:max-line-length - errors.push( - `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` - ); - } else { - const matchingCode = matches.map( - (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` - ); - errors.push( - `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( - '\n\n' - )}` - ); - } - }); - }); - }); - + matchAndDefineSteps(feature, test, errors); }); }); @@ -76,59 +79,25 @@ export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsD } }; -export const autoBindStepsWithRules = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => { +export const autoBindStepsWithRules = ( + features: ParsedFeature[], + stepDefinitions: StepsDefinitionCallbackFunction[] +) => { stepDefinitions.forEach(registerSteps); const errors: string[] = []; features.forEach((feature) => { - defineRuleBasedFeature(feature, (rule) => { - - feature.rules.forEach((r) => { - rule(r.title, (test) => { - - const scenarioOutlineScenarios = r.scenarioOutlines.map( - (scenarioOutline) => scenarioOutline.scenarios[0] - ); - - const scenarios = [ ...r.scenarios, ...scenarioOutlineScenarios ]; - - scenarios.forEach((scenario) => { - test(scenario.title, (options) => { - scenario.steps.forEach((step, stepIndex) => { - const matches = globalSteps.filter((globalStep) => - matchSteps(step.stepText, globalStep.stepMatcher) - ); - - if (matches.length === 1) { - const match = matches[0]; - - options.defineStep(match.stepMatcher, match.stepFunction); - } else if (matches.length === 0) { - const stepCode = generateStepCode(scenario.steps, stepIndex, false); - // tslint:disable-next-line:max-line-length - errors.push( - `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` - ); - } else { - const matchingCode = matches.map( - (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` - ); - errors.push( - `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( - '\n\n' - )}` - ); - } - }); - }); - }); - }) - }) + defineRuleBasedFeature(feature, (ruleDefinition) => { + feature.rules.forEach((rule) => { + ruleDefinition(rule.title, (test) => { + matchAndDefineSteps(rule, test, errors); + }); + }); }); }); if (errors.length) { throw new Error(errors.join('\n\n')); } -}; \ No newline at end of file +}; From 801def87108380196fb8f0ed0682a12199029cfc Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 14:49:40 +0100 Subject: [PATCH 12/29] restore launch json --- .vscode/launch.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9812793..4adfb11 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,25 +4,11 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "Launch via NPM", - "type": "pwa-node", - "request": "launch", - "cwd": "${workspaceRoot}", - "runtimeExecutable": "npm", - "runtimeArgs": [ - "run-script", "test", - "--", - //"examples/typescript/specs/step-definitions/auto-step-binding.steps.ts" -// "examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts" - "examples/typescript/specs/step-definitions/language.steps.ts" - ], -}, { "type": "node", "request": "launch", "name": "Launch Program", - "preLaunchTask": "build", + "preLaunchTask": build, "program": "${workspaceFolder}/dist/code-generation-test.js" }, { @@ -33,7 +19,7 @@ "args": [ "-i" ], - "preLaunchTask": "build", + "preLaunchTask": build, "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceRoot}/examples/typescript/dist/**/*" From 3d7e5862e474be49ed3a2ac66daec4409f2e7fce Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:15:40 +0100 Subject: [PATCH 13/29] add example for extended rules support --- .../features/extended-rules-support.feature | 34 ++++++++++++++++ .../extended-rules-support.steps.ts | 40 +++++++++++++++++++ examples/typescript/src/vending-machine.ts | 9 ++++- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 examples/typescript/specs/features/extended-rules-support.feature create mode 100644 examples/typescript/specs/step-definitions/extended-rules-support.steps.ts diff --git a/examples/typescript/specs/features/extended-rules-support.feature b/examples/typescript/specs/features/extended-rules-support.feature new file mode 100644 index 0000000..306298c --- /dev/null +++ b/examples/typescript/specs/features/extended-rules-support.feature @@ -0,0 +1,34 @@ +Feature: Vending machine + + Rule: Dispenses items if correct amount of money is inserted + + Scenario: Selecting a snack + Given the vending machine has "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my "Maltesers" should be dispensed + + Scenario Outline: Selecting a beverage + Given the vending machine has "" in stock + And I have inserted the correct amount of money + When I select "" + Then my "" should be dispensed + + Examples: + | beverage | + | Cola | + | Ginger ale | + + Rule: Returns my money if item is out of stock + + Scenario: Selecting a snack + Given the vending machine has no "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my money should be returned + + Scenario: Selecting a beverage + Given the vending machine has no "Cola" in stock + And I have inserted the correct amount of money + When I select "Cola" + Then my money should be returned diff --git a/examples/typescript/specs/step-definitions/extended-rules-support.steps.ts b/examples/typescript/specs/step-definitions/extended-rules-support.steps.ts new file mode 100644 index 0000000..0517851 --- /dev/null +++ b/examples/typescript/specs/step-definitions/extended-rules-support.steps.ts @@ -0,0 +1,40 @@ +import { StepDefinitions, loadFeature, autoBindStepsWithRules } from '../../../../src'; +import { VendingMachine } from '../../src/vending-machine'; + +export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) => { + let vendingMachine: VendingMachine; + + const myMoney = 0.50; + + given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 1); + }); + + given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 0); + }); + + and('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + + then(/^my money should be returned$/, () => { + const returnedMoney = vendingMachine.moneyReturnSlot; + expect(returnedMoney).toBe(myMoney); + }); + + then(/^my "(.*)" should be dispensed$/, (itemName: string) => { + const inventoryAmount = vendingMachine.items[itemName]; + expect(inventoryAmount).toBe(0); + }); +}; + +const feature = loadFeature('./examples/typescript/specs/features/extended-rules-support.feature', {collapseRules: false}); + +autoBindStepsWithRules([feature], [ vendingMachineSteps ]); diff --git a/examples/typescript/src/vending-machine.ts b/examples/typescript/src/vending-machine.ts index 3cf5f10..ccc6862 100644 --- a/examples/typescript/src/vending-machine.ts +++ b/examples/typescript/src/vending-machine.ts @@ -3,6 +3,7 @@ const ITEM_COST = 0.50; export class VendingMachine { public balance: number = 0; public items: { [itemName: string]: number } = {}; + public moneyReturnSlot: number = 0; public stockItem(itemName: string, count: number) { this.items[itemName] = this.items[itemName] || 0; @@ -14,10 +15,14 @@ export class VendingMachine { } public dispenseItem(itemName: string) { + if(this.items[itemName] === 0) { + this.moneyReturnSlot = this.balance; + this.balance = 0; + } + if (this.balance >= ITEM_COST && this.items[itemName] > 0) { this.balance -= ITEM_COST; + this.items[itemName]--; } - - this.items[itemName]--; } } From ed9fe2a0f1091e241b2d3d39a9672775bf4bca67 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:16:25 +0100 Subject: [PATCH 14/29] restore beverage vending machine test --- .../features/auto-binding/beverage-vending-machine.feature | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature b/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature index 8923815..14e3a41 100644 --- a/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature +++ b/examples/typescript/specs/features/auto-binding/beverage-vending-machine.feature @@ -1,7 +1,5 @@ Feature: Beverage vending machine - Rule: Dispense purchased beverage - Scenario Outline: Purchasing a beverage Given the vending machine has "" in stock And I have inserted the correct amount of money From 36697963ea27a63cd47291ac21de61f09cec5362 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:16:58 +0100 Subject: [PATCH 15/29] restore snack vending machine test --- .../specs/features/auto-binding/snack-vending-machine.feature | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature b/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature index 0e2ef92..e0f676b 100644 --- a/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature +++ b/examples/typescript/specs/features/auto-binding/snack-vending-machine.feature @@ -1,7 +1,5 @@ Feature: Snack vending machine - Rule: Dispenses purchased snack - Scenario: Purchasing a snack Given the vending machine has "Maltesers" in stock And I have inserted the correct amount of money From 78016be4b3086cdd5f7de68578368c026bc8caac Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:18:10 +0100 Subject: [PATCH 16/29] restore --- .../auto-step-binding.steps.ts | 6 +- .../step-definitions/backgrounds.steps.ts | 71 +++++++++---------- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts index 75afcf3..06a7d1c 100644 --- a/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts +++ b/examples/typescript/specs/step-definitions/auto-step-binding.steps.ts @@ -1,4 +1,4 @@ -import { StepDefinitions, loadFeatures, autoBindStepsWithRules } from '../../../../src'; +import { StepDefinitions, loadFeatures, autoBindSteps } from '../../../../src'; import { VendingMachine } from '../../src/vending-machine'; export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) => { @@ -23,6 +23,6 @@ export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) }); }; -const features = loadFeatures('./examples/typescript/specs/features/auto-binding/**/*.feature', {collapseRules: false}); +const features = loadFeatures('./examples/typescript/specs/features/auto-binding/**/*.feature'); -autoBindStepsWithRules(features, [ vendingMachineSteps ]); +autoBindSteps(features, [ vendingMachineSteps ]); diff --git a/examples/typescript/specs/step-definitions/backgrounds.steps.ts b/examples/typescript/specs/step-definitions/backgrounds.steps.ts index bbbc9c1..eb1840e 100644 --- a/examples/typescript/specs/step-definitions/backgrounds.steps.ts +++ b/examples/typescript/specs/step-definitions/backgrounds.steps.ts @@ -1,9 +1,9 @@ -import { loadFeature, DefineStepFunction, defineRuleBasedFeature } from '../../../../src/'; +import { loadFeature, defineFeature, DefineStepFunction } from '../../../../src/'; import { ArcadeMachine, COIN_TYPES, CoinStatus } from '../../src/arcade-machine'; -const feature = loadFeature('./examples/typescript/specs/features/backgrounds.feature', {collapseRules: false}); +const feature = loadFeature('./examples/typescript/specs/features/backgrounds.feature'); -defineRuleBasedFeature(feature, (rule) => { +defineFeature(feature, (test) => { let arcadeMachine: ArcadeMachine; beforeEach(() => { @@ -22,56 +22,51 @@ defineRuleBasedFeature(feature, (rule) => { }); }; - rule('When a coin is inserted, the balance should increase by the amount of the coin', (test) => { - test('Successfully inserting coins', ({ given, when, then }) => { - givenMyMachineIsConfiguredToRequireCoins(given); + test('Successfully inserting coins', ({ given, when, then }) => { + givenMyMachineIsConfiguredToRequireCoins(given); - given('I have not inserted any coins', () => { - arcadeMachine.balance = 0; - }); + given('I have not inserted any coins', () => { + arcadeMachine.balance = 0; + }); - when('I insert one US quarter', () => { - arcadeMachine.insertCoin(COIN_TYPES.USQuarter); - }); + when('I insert one US quarter', () => { + arcadeMachine.insertCoin(COIN_TYPES.USQuarter); + }); - then(/^I should have a balance of (\d+) cents$/, (balance) => { - arcadeMachine.balance = balance / 100; - }); + then(/^I should have a balance of (\d+) cents$/, (balance) => { + arcadeMachine.balance = balance / 100; }); }); - rule('When a coin is not recognized as valid, it should be returned', (test) => { - test('Inserting a Canadian coin', ({ given, when, then }) => { - let coinStatus: CoinStatus; + test('Inserting a Canadian coin', ({ given, when, then }) => { + let coinStatus: CoinStatus; - givenMyMachineIsConfiguredToRequireCoins(given); + givenMyMachineIsConfiguredToRequireCoins(given); - givenMyMachineIsConfiguredToAcceptUsQuarters(given); + givenMyMachineIsConfiguredToAcceptUsQuarters(given); - when('I insert a Canadian Quarter', () => { - coinStatus = arcadeMachine.insertCoin(COIN_TYPES.CanadianQuarter); - }); - - then('my coin should be returned', () => { - expect(coinStatus).toBe('CoinReturned'); - }); + when('I insert a Canadian Quarter', () => { + coinStatus = arcadeMachine.insertCoin(COIN_TYPES.CanadianQuarter); }); - test('Inserting a badly damaged coin', ({ given, when, then }) => { - let coinStatus: CoinStatus; + then('my coin should be returned', () => { + expect(coinStatus).toBe('CoinReturned'); + }); + }); - givenMyMachineIsConfiguredToRequireCoins(given); + test('Inserting a badly damaged coin', ({ given, when, then }) => { + let coinStatus: CoinStatus; - givenMyMachineIsConfiguredToAcceptUsQuarters(given); + givenMyMachineIsConfiguredToRequireCoins(given); - when('I insert a US Quarter that is badly damaged', () => { - coinStatus = arcadeMachine.insertCoin(COIN_TYPES.Unknown); - }); + givenMyMachineIsConfiguredToAcceptUsQuarters(given); - then('my coin should be returned', () => { - expect(coinStatus).toBe('CoinReturned'); - }); + when('I insert a US Quarter that is badly damaged', () => { + coinStatus = arcadeMachine.insertCoin(COIN_TYPES.Unknown); }); - }) + then('my coin should be returned', () => { + expect(coinStatus).toBe('CoinReturned'); + }); + }); }); From 60904aa1fdfc7dc917bc78ac3e084b49f08b685f Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:18:51 +0100 Subject: [PATCH 17/29] restore --- .../using-latest-gherkin-keywords.steps.ts | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts index ddb7b70..cb76718 100644 --- a/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts +++ b/examples/typescript/specs/step-definitions/using-latest-gherkin-keywords.steps.ts @@ -1,11 +1,10 @@ -import { loadFeature, defineRuleBasedFeature, DefineStepFunction } from '../../../../src/'; +import { loadFeature, defineFeature, DefineStepFunction } from '../../../../src/'; import { Calculator, CalculatorOperator } from '../../src/calculator'; -const feature = loadFeature('./examples/typescript/specs/features/using-latest-gherkin-keywords.feature', {collapseRules: false}); - -defineRuleBasedFeature(feature, (rule) => { +const feature = loadFeature('./examples/typescript/specs/features/using-latest-gherkin-keywords.feature'); +defineFeature(feature, (test) => { let calculator: Calculator; let output: number | undefined; @@ -47,40 +46,34 @@ defineRuleBasedFeature(feature, (rule) => { }); }; - rule('When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed', (test) => { - - test('Subtracting two numbers', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); - andIHaveEnteredXAsTheSecondOperand(and); - whenIPressTheEnterKey(when); - thenTheOutputOfXShouldBeDisplayed(then); - }); + test('Subtracting two numbers', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); + andIHaveEnteredXAsTheSecondOperand(and); + whenIPressTheEnterKey(when); + thenTheOutputOfXShouldBeDisplayed(then); + }); - test('Attempting to subtract without entering a second number', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); + test('Attempting to subtract without entering a second number', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); - and('I have not entered a second operand', () => { - // Nothing to do here - }); + and('I have not entered a second operand', () => { + // Nothing to do here + }); - whenIPressTheEnterKey(when); + whenIPressTheEnterKey(when); - then('no output should be displayed', () => { - expect(output).toBeFalsy(); - }); + then('no output should be displayed', () => { + expect(output).toBeFalsy(); }); - }); - rule("When a number, a division sign, a number, and equals is entered into the calculator, the quotient should be calculated and displayed", (test) => { - test('Division operations', ({ given, and, when, then }) => { - givenIHaveEnteredXAsTheFirstOperand(given); - andIHaveEnteredXAsTheOperator(and); - andIHaveEnteredXAsTheSecondOperand(and); - whenIPressTheEnterKey(when); - thenTheOutputOfXShouldBeDisplayed(then); - }); + test('Division operations', ({ given, and, when, then }) => { + givenIHaveEnteredXAsTheFirstOperand(given); + andIHaveEnteredXAsTheOperator(and); + andIHaveEnteredXAsTheSecondOperand(and); + whenIPressTheEnterKey(when); + thenTheOutputOfXShouldBeDisplayed(then); }); }); From ae11eb3241dcdd4a0919b1fb7a051ca53cbbf5d2 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:19:35 +0100 Subject: [PATCH 18/29] restore --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d32afd..9faecd6 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "jest": "jest --verbose", - "test": "npm run build & jest --color", + "test": "npm run build & npm run lint & jest --color", "lint": "tslint --project ./" }, "repository": { From 1f6a6ce382bca317414bbde589497c9be6e24996 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:27:57 +0100 Subject: [PATCH 19/29] formatting --- src/automatic-step-binding.ts | 10 +++--- src/feature-definition-creation.ts | 58 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 0c040a2..332f7a8 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -28,10 +28,10 @@ const registerSteps = (stepDefinitionCallback: StepsDefinitionCallbackFunction) }); }; -const matchAndDefineSteps = (feature: ScenarioGroup, test: DefineScenarioFunctionWithAliases, errors: string[]) => { - const scenarioOutlineScenarios = feature.scenarioOutlines.map((scenarioOutline) => scenarioOutline.scenarios[0]); +const matchAndDefineSteps = (group: ScenarioGroup, test: DefineScenarioFunctionWithAliases, errors: string[]) => { + const scenarioOutlineScenarios = group.scenarioOutlines.map((scenarioOutline) => scenarioOutline.scenarios[0]); - const scenarios = [ ...feature.scenarios, ...scenarioOutlineScenarios ]; + const scenarios = [ ...group.scenarios, ...scenarioOutlineScenarios ]; scenarios.forEach((scenario) => { test(scenario.title, (options) => { @@ -46,14 +46,14 @@ const matchAndDefineSteps = (feature: ScenarioGroup, test: DefineScenarioFunctio const stepCode = generateStepCode(scenario.steps, stepIndex, false); // tslint:disable-next-line:max-line-length errors.push( - `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}` + `No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${group.title}". Please add the following step code: \n\n${stepCode}` ); } else { const matchingCode = matches.map( (match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}` ); errors.push( - `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( + `${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${group.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join( '\n\n' )}` ); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index bd6b114..641a023 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -11,7 +11,7 @@ import { } from './models'; import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, - matchSteps + matchSteps, } from './validation/step-definition-validation'; import { applyTagFilters } from './tag-filtering'; @@ -37,7 +37,7 @@ export type DefineRuleFunction = ( export type DefineScenarioFunction = ( scenarioTitle: string, stepsDefinitionCallback: StepsDefinitionCallbackFunction, - timeout?: number + timeout?: number, ) => void; export type DefineScenarioFunctionWithAliases = DefineScenarioFunction & { @@ -51,7 +51,7 @@ export type DefineStepFunction = (stepMatcher: string | RegExp, stepDefinitionCa const processScenarioTitleTemplate = ( scenarioTitle: string, - parsedFeature: ScenarioGroup, + group: ScenarioGroup, options: Options, parsedScenario: ParsedScenario, parsedScenarioOutline: ParsedScenarioOutline @@ -61,9 +61,9 @@ const processScenarioTitleTemplate = ( return ( options && options.scenarioNameTemplate({ - featureTitle: parsedFeature.title, + featureTitle: group.title, scenarioTitle: scenarioTitle.toString(), - featureTags: parsedFeature.tags, + featureTags: group.tags, scenarioTags: (parsedScenario || parsedScenarioOutline).tags }) ); @@ -125,7 +125,7 @@ const defineScenario = ( only: boolean = false, skip: boolean = false, concurrent: boolean = false, - timeout: number | undefined = undefined + timeout: number | undefined = undefined, ) => { const testFunction = getTestFunction(parsedScenario.skippedViaTagFilter, only, skip, concurrent); @@ -198,35 +198,39 @@ const createDefineScenarioFunction = ( parsedFeature, options, parsedScenario, - parsedScenarioOutline + parsedScenarioOutline, ); ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps( options, parsedScenario || parsedScenarioOutline, - scenarioFromStepDefinitions + scenarioFromStepDefinitions, ); if (checkForPendingSteps(scenarioFromStepDefinitions)) { - xtest( - scenarioTitle, - () => { + xtest(scenarioTitle, () => { // Nothing to do - }, - undefined - ); + }, undefined); } else if (parsedScenario) { - defineScenario(scenarioTitle, scenarioFromStepDefinitions, parsedScenario, only, skip, concurrent, timeout); + defineScenario( + scenarioTitle, + scenarioFromStepDefinitions, + parsedScenario, + only, + skip, + concurrent, + timeout, + ); } else if (parsedScenarioOutline) { parsedScenarioOutline.scenarios.forEach((scenario) => { defineScenario( - scenario.title || scenarioTitle, + (scenario.title || scenarioTitle), scenarioFromStepDefinitions, scenario, only, skip, concurrent, - timeout + timeout, ); }); } @@ -237,40 +241,40 @@ const createDefineScenarioFunction = ( const createDefineScenarioFunctionWithAliases = ( featureFromStepDefinitions: FeatureFromStepDefinitions, - parsedFeature: ScenarioGroup, + group: ScenarioGroup, options: Options ) => { const defineScenarioFunctionWithAliases = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, options ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).only = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, options, true, false, - false + false, ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).skip = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, options, false, true, - false + false, ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).concurrent = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, options, false, false, - true + true, ); return defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases; @@ -280,7 +284,7 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD return (stepMatcher: string | RegExp, stepFunction: () => any) => { const stepDefinition: StepFromStepDefinitions = { stepMatcher, - stepFunction + stepFunction, }; scenarioFromStepDefinitions.steps.push(stepDefinition); @@ -336,7 +340,7 @@ export function defineRuleBasedFeature( (rule) => rule.title.toLocaleLowerCase() === ruleText.toLocaleLowerCase() ); if (matchingRules.length != 1) { - throw new Error(`no matching rule found for '${ruleText}'"`); + throw new Error(`No matching rule found for '${ruleText}'"`); } defineScenarioGroup(matchingRules[0], callback, featureFromFile.options); From 4eb782aec5d4699febcb17dbbee77e62f10844b5 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:28:39 +0100 Subject: [PATCH 20/29] formatting --- .../features/extended-rules-support.feature | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/typescript/specs/features/extended-rules-support.feature b/examples/typescript/specs/features/extended-rules-support.feature index 306298c..58c06d3 100644 --- a/examples/typescript/specs/features/extended-rules-support.feature +++ b/examples/typescript/specs/features/extended-rules-support.feature @@ -2,33 +2,33 @@ Feature: Vending machine Rule: Dispenses items if correct amount of money is inserted - Scenario: Selecting a snack - Given the vending machine has "Maltesers" in stock - And I have inserted the correct amount of money - When I select "Maltesers" - Then my "Maltesers" should be dispensed + Scenario: Selecting a snack + Given the vending machine has "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my "Maltesers" should be dispensed - Scenario Outline: Selecting a beverage - Given the vending machine has "" in stock - And I have inserted the correct amount of money - When I select "" - Then my "" should be dispensed + Scenario Outline: Selecting a beverage + Given the vending machine has "" in stock + And I have inserted the correct amount of money + When I select "" + Then my "" should be dispensed - Examples: - | beverage | - | Cola | - | Ginger ale | + Examples: + | beverage | + | Cola | + | Ginger ale | Rule: Returns my money if item is out of stock - Scenario: Selecting a snack - Given the vending machine has no "Maltesers" in stock - And I have inserted the correct amount of money - When I select "Maltesers" - Then my money should be returned + Scenario: Selecting a snack + Given the vending machine has no "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my money should be returned - Scenario: Selecting a beverage - Given the vending machine has no "Cola" in stock - And I have inserted the correct amount of money - When I select "Cola" - Then my money should be returned + Scenario: Selecting a beverage + Given the vending machine has no "Cola" in stock + And I have inserted the correct amount of money + When I select "Cola" + Then my money should be returned From fc5fa286190215a405af1d8b834afa628170f878 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:29:17 +0100 Subject: [PATCH 21/29] formatting --- .../specs/features/using-latest-gherkin-keywords.feature | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/typescript/specs/features/using-latest-gherkin-keywords.feature b/examples/typescript/specs/features/using-latest-gherkin-keywords.feature index c31ca0a..48407fa 100644 --- a/examples/typescript/specs/features/using-latest-gherkin-keywords.feature +++ b/examples/typescript/specs/features/using-latest-gherkin-keywords.feature @@ -1,6 +1,7 @@ Feature: Using latest Gherkin keywords - Rule: When a number, a minus sign, a number, and equals is entered into the calculator, the sum should be calculated and displayed + Rule: When a number, a minus sign, a number, and equals is entered into the calculator, + the sum should be calculated and displayed Example: Subtracting two numbers Given I have entered "4" as the first operand @@ -16,7 +17,8 @@ Feature: Using latest Gherkin keywords When I press the equals key Then no output should be displayed - Rule: When a number, a division sign, a number, and equals is entered into the calculator, the quotient should be calculated and displayed + Rule: When a number, a division sign, a number, and equals is entered into the calculator, + the quotient should be calculated and displayed Scenario Template: Division operations Given I have entered "" as the first operand From 1c9da16d3a1ed72e6f52eb7d8622ee5532135342 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:29:47 +0100 Subject: [PATCH 22/29] formatting --- examples/typescript/src/vending-machine.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/typescript/src/vending-machine.ts b/examples/typescript/src/vending-machine.ts index ccc6862..a9f8506 100644 --- a/examples/typescript/src/vending-machine.ts +++ b/examples/typescript/src/vending-machine.ts @@ -16,8 +16,8 @@ export class VendingMachine { public dispenseItem(itemName: string) { if(this.items[itemName] === 0) { - this.moneyReturnSlot = this.balance; - this.balance = 0; + this.moneyReturnSlot = this.balance; + this.balance = 0; } if (this.balance >= ITEM_COST && this.items[itemName] > 0) { From 5ff73f614d657efc3d38a15fc24bc1430fbf0a33 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:37:50 +0100 Subject: [PATCH 23/29] formatting --- src/automatic-step-binding.ts | 2 +- src/feature-definition-creation.ts | 101 +++++++++++++---------------- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 332f7a8..83d956e 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -8,7 +8,7 @@ import { } from './feature-definition-creation'; import { generateStepCode } from './code-generation/step-generation'; -const globalSteps: Array<{ stepMatcher: string | RegExp; stepFunction: () => any }> = []; +const globalSteps: Array<{ stepMatcher: string | RegExp, stepFunction: () => any }> = []; const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) => { globalSteps.push({ stepMatcher, stepFunction }); diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index 641a023..d8f4a4a 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -3,10 +3,8 @@ import { ScenarioFromStepDefinitions, FeatureFromStepDefinitions, StepFromStepDefinitions, - ParsedFeature, - ParsedScenario, - Options, - ParsedScenarioOutline, + ParsedFeature, ParsedScenario, + Options, ParsedScenarioOutline, ScenarioGroup } from './models'; import { @@ -54,23 +52,20 @@ const processScenarioTitleTemplate = ( group: ScenarioGroup, options: Options, parsedScenario: ParsedScenario, - parsedScenarioOutline: ParsedScenarioOutline + parsedScenarioOutline: ParsedScenarioOutline, ) => { if (options && options.scenarioNameTemplate) { try { - return ( - options && - options.scenarioNameTemplate({ - featureTitle: group.title, - scenarioTitle: scenarioTitle.toString(), - featureTags: group.tags, - scenarioTags: (parsedScenario || parsedScenarioOutline).tags - }) - ); + return options && options.scenarioNameTemplate({ + featureTitle: group.title, + scenarioTitle: scenarioTitle.toString(), + featureTags: group.tags, + scenarioTags: (parsedScenario || parsedScenarioOutline).tags + }); } catch (err) { throw new Error( // tslint:disable-next-line:max-line-length - `An error occurred while executing a scenario name template. \nTemplate:\n${options.scenarioNameTemplate}\nError:${err.message}` + `An error occurred while executing a scenario name template. \nTemplate:\n${options.scenarioNameTemplate}\nError:${err.message}`, ); } } @@ -129,28 +124,24 @@ const defineScenario = ( ) => { const testFunction = getTestFunction(parsedScenario.skippedViaTagFilter, only, skip, concurrent); - testFunction( - scenarioTitle, - () => { - return scenarioFromStepDefinitions.steps.reduce((promiseChain, nextStep, index) => { - const stepArgument = parsedScenario.steps[index].stepArgument; - const matches = matchSteps( - parsedScenario.steps[index].stepText, - scenarioFromStepDefinitions.steps[index].stepMatcher - ); - let matchArgs: string[] = []; + testFunction(scenarioTitle, () => { + return scenarioFromStepDefinitions.steps.reduce((promiseChain, nextStep, index) => { + const stepArgument = parsedScenario.steps[index].stepArgument; + const matches = matchSteps( + parsedScenario.steps[index].stepText, + scenarioFromStepDefinitions.steps[index].stepMatcher + ); + let matchArgs: string[] = []; - if (matches && (matches as RegExpMatchArray).length) { - matchArgs = (matches as RegExpMatchArray).slice(1); - } + if (matches && (matches as RegExpMatchArray).length) { + matchArgs = (matches as RegExpMatchArray).slice(1); + } - const args = [ ...matchArgs, stepArgument ]; + const args = [ ...matchArgs, stepArgument ]; - return promiseChain.then(() => nextStep.stepFunction(...args)); - }, Promise.resolve()); - }, - timeout - ); + return promiseChain.then(() => nextStep.stepFunction(...args)); + }, Promise.resolve()); + }, timeout); }; const createDefineScenarioFunction = ( @@ -159,16 +150,16 @@ const createDefineScenarioFunction = ( options: Options, only: boolean = false, skip: boolean = false, - concurrent: boolean = false + concurrent: boolean = false, ) => { const defineScenarioFunction: DefineScenarioFunction = ( scenarioTitle: string, stepsDefinitionFunctionCallback: StepsDefinitionCallbackFunction, - timeout?: number + timeout?: number, ) => { const scenarioFromStepDefinitions: ScenarioFromStepDefinitions = { title: scenarioTitle, - steps: [] + steps: [], }; featureFromStepDefinitions.scenarios.push(scenarioFromStepDefinitions); @@ -185,13 +176,11 @@ const createDefineScenarioFunction = ( } }); - const parsedScenario = parsedFeature.scenarios.filter( - (s) => s.title.toLowerCase() === scenarioTitle.toLowerCase() - )[0]; + const parsedScenario = parsedFeature.scenarios + .filter((s) => s.title.toLowerCase() === scenarioTitle.toLowerCase())[0]; - const parsedScenarioOutline = parsedFeature.scenarioOutlines.filter( - (s) => s.title.toLowerCase() === scenarioTitle.toLowerCase() - )[0]; + const parsedScenarioOutline = parsedFeature.scenarioOutlines + .filter((s) => s.title.toLowerCase() === scenarioTitle.toLowerCase())[0]; scenarioTitle = processScenarioTitleTemplate( scenarioTitle, @@ -212,15 +201,15 @@ const createDefineScenarioFunction = ( // Nothing to do }, undefined); } else if (parsedScenario) { - defineScenario( - scenarioTitle, - scenarioFromStepDefinitions, - parsedScenario, - only, - skip, - concurrent, - timeout, - ); + defineScenario( + scenarioTitle, + scenarioFromStepDefinitions, + parsedScenario, + only, + skip, + concurrent, + timeout, + ); } else if (parsedScenarioOutline) { parsedScenarioOutline.scenarios.forEach((scenario) => { defineScenario( @@ -294,18 +283,18 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD const defineScenarioGroup = ( group: ScenarioGroup, scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction, - options: Options + options: Options, ) => { const featureFromDefinedSteps: FeatureFromStepDefinitions = { title: group.title, - scenarios: [] + scenarios: [], }; const parsedFeatureWithTagFiltersApplied = applyTagFilters(group, options.tagFilter); if ( - parsedFeatureWithTagFiltersApplied.scenarios.length === 0 && - parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 + parsedFeatureWithTagFiltersApplied.scenarios.length === 0 + && parsedFeatureWithTagFiltersApplied.scenarioOutlines.length === 0 ) { return; } From 794d11208fc20b159c95c895fa56c1447f8f2982 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:39:34 +0100 Subject: [PATCH 24/29] formatting --- src/parsed-feature-loading.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parsed-feature-loading.ts b/src/parsed-feature-loading.ts index 6d2ce94..88f56b2 100644 --- a/src/parsed-feature-loading.ts +++ b/src/parsed-feature-loading.ts @@ -248,7 +248,7 @@ const parseBackgrounds = (ast: any) => { .map((child: any) => child.background); }; -const collapseBackgroundsOfFeature = (astFeature: any) => { +const parseAndCollapseBackgrounds = (astFeature: any) => { const featureBackgrounds = parseBackgrounds(astFeature); const children = collapseBackgrounds(astFeature.children, featureBackgrounds) @@ -357,7 +357,7 @@ export const parseFeature = (featureText: string, options?: Options): ParsedFeat throw new Error(`Error parsing feature Gherkin: ${err.message}`); } - let astFeature = collapseBackgroundsOfFeature(ast.feature); + let astFeature = parseAndCollapseBackgrounds(ast.feature); if(options?.collapseRules) { astFeature = collapseRules(astFeature); From 96a727dc9a057728f05d3b6eca926a961c86d084 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:41:23 +0100 Subject: [PATCH 25/29] formatting --- src/tag-filtering.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tag-filtering.ts b/src/tag-filtering.ts index d66330f..af3658b 100644 --- a/src/tag-filtering.ts +++ b/src/tag-filtering.ts @@ -74,24 +74,24 @@ const setScenarioSkipped = (parsedFeature: ScenarioGroup, scenario: ParsedScenar }; export const applyTagFilters = ( - parsedFeature: ScenarioGroup, + group: ScenarioGroup, tagFilter: string | undefined ) => { if (tagFilter === undefined) { - return parsedFeature; + return group; } - const scenarios = parsedFeature.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario, tagFilter)); - const scenarioOutlines = parsedFeature.scenarioOutlines + const scenarios = group.scenarios.map((scenario) => setScenarioSkipped(group, scenario, tagFilter)); + const scenarioOutlines = group.scenarioOutlines .map((scenarioOutline) => { return { - ...setScenarioSkipped(parsedFeature, scenarioOutline, tagFilter), - scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario, tagFilter)), + ...setScenarioSkipped(group, scenarioOutline, tagFilter), + scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(group, scenario, tagFilter)), }; }); return { - ...parsedFeature, + ...group, scenarios, scenarioOutlines, } as ParsedFeature; From 9029154a8c364b24219e257cf0e521f35fe12b96 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:43:54 +0100 Subject: [PATCH 26/29] rename example --- ...support.feature => extended-rules-auto-step-binding.feature} | 0 ...pport.steps.ts => extended-rules-auto-step-binding.steps.ts} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/typescript/specs/features/{extended-rules-support.feature => extended-rules-auto-step-binding.feature} (100%) rename examples/typescript/specs/step-definitions/{extended-rules-support.steps.ts => extended-rules-auto-step-binding.steps.ts} (92%) diff --git a/examples/typescript/specs/features/extended-rules-support.feature b/examples/typescript/specs/features/extended-rules-auto-step-binding.feature similarity index 100% rename from examples/typescript/specs/features/extended-rules-support.feature rename to examples/typescript/specs/features/extended-rules-auto-step-binding.feature diff --git a/examples/typescript/specs/step-definitions/extended-rules-support.steps.ts b/examples/typescript/specs/step-definitions/extended-rules-auto-step-binding.steps.ts similarity index 92% rename from examples/typescript/specs/step-definitions/extended-rules-support.steps.ts rename to examples/typescript/specs/step-definitions/extended-rules-auto-step-binding.steps.ts index 0517851..f7a40b1 100644 --- a/examples/typescript/specs/step-definitions/extended-rules-support.steps.ts +++ b/examples/typescript/specs/step-definitions/extended-rules-auto-step-binding.steps.ts @@ -35,6 +35,6 @@ export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) }); }; -const feature = loadFeature('./examples/typescript/specs/features/extended-rules-support.feature', {collapseRules: false}); +const feature = loadFeature('./examples/typescript/specs/features/extended-rules-auto-step-binding.feature', {collapseRules: false}); autoBindStepsWithRules([feature], [ vendingMachineSteps ]); From 9a34002aeed931621ea6682dad618e7bdd1a3018 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 15:57:55 +0100 Subject: [PATCH 27/29] add example for verbose scenario definition --- .../extended-rules-definition.feature | 34 +++++++ .../extended-rules-definition.steps.ts | 96 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 examples/typescript/specs/features/extended-rules-definition.feature create mode 100644 examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts diff --git a/examples/typescript/specs/features/extended-rules-definition.feature b/examples/typescript/specs/features/extended-rules-definition.feature new file mode 100644 index 0000000..58c06d3 --- /dev/null +++ b/examples/typescript/specs/features/extended-rules-definition.feature @@ -0,0 +1,34 @@ +Feature: Vending machine + + Rule: Dispenses items if correct amount of money is inserted + + Scenario: Selecting a snack + Given the vending machine has "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my "Maltesers" should be dispensed + + Scenario Outline: Selecting a beverage + Given the vending machine has "" in stock + And I have inserted the correct amount of money + When I select "" + Then my "" should be dispensed + + Examples: + | beverage | + | Cola | + | Ginger ale | + + Rule: Returns my money if item is out of stock + + Scenario: Selecting a snack + Given the vending machine has no "Maltesers" in stock + And I have inserted the correct amount of money + When I select "Maltesers" + Then my money should be returned + + Scenario: Selecting a beverage + Given the vending machine has no "Cola" in stock + And I have inserted the correct amount of money + When I select "Cola" + Then my money should be returned diff --git a/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts b/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts new file mode 100644 index 0000000..a01cdd0 --- /dev/null +++ b/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts @@ -0,0 +1,96 @@ +import { StepDefinitions, loadFeature, defineRuleBasedFeature } from '../../../../src'; +import { VendingMachine } from '../../src/vending-machine'; + +const feature = loadFeature('./examples/typescript/specs/features/extended-rules-definition.feature', {collapseRules: false}); + +defineRuleBasedFeature(feature, (rule) => { + let vendingMachine: VendingMachine; + + const myMoney = 0.50; + + rule("Dispenses items if correct amount of money is inserted", (test) => { + + test('Selecting a snack', ({ given, and, when, then }) => { + given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 1); + }); + + and('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + + then(/^my "(.*)" should be dispensed$/, (itemName: string) => { + const inventoryAmount = vendingMachine.items[itemName]; + expect(inventoryAmount).toBe(0); + }); + }); + + test('Selecting a beverage', ({ given, and, when, then }) => { + given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 1); + }); + + and('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + + then(/^my "(.*)" should be dispensed$/, (itemName: string) => { + const inventoryAmount = vendingMachine.items[itemName]; + expect(inventoryAmount).toBe(0); + }); + }); + }); + + rule("Returns my money if item is out of stock", (test) => { + + test('Selecting a snack', ({ given, and, when, then }) => { + given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 0); + }); + + and('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + + then(/^my money should be returned$/, () => { + const returnedMoney = vendingMachine.moneyReturnSlot; + expect(returnedMoney).toBe(myMoney); + }); + }); + + test('Selecting a beverage', ({ given, and, when, then }) => { + given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 0); + }); + + and('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + + then(/^my money should be returned$/, () => { + const returnedMoney = vendingMachine.moneyReturnSlot; + expect(returnedMoney).toBe(myMoney); + }); + }); + }); +}); From bc24a388b111688d2924c23fa677914aff5fa9d7 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 16:07:34 +0100 Subject: [PATCH 28/29] refactor --- .../extended-rules-definition.steps.ts | 121 ++++++++---------- 1 file changed, 55 insertions(+), 66 deletions(-) diff --git a/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts b/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts index a01cdd0..de49158 100644 --- a/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts +++ b/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts @@ -1,4 +1,5 @@ -import { StepDefinitions, loadFeature, defineRuleBasedFeature } from '../../../../src'; +import { loadFeature, defineRuleBasedFeature } from '../../../../src'; +import { DefineStepFunction } from '../../../../src/feature-definition-creation'; import { VendingMachine } from '../../src/vending-machine'; const feature = loadFeature('./examples/typescript/specs/features/extended-rules-definition.feature', {collapseRules: false}); @@ -8,89 +9,77 @@ defineRuleBasedFeature(feature, (rule) => { const myMoney = 0.50; - rule("Dispenses items if correct amount of money is inserted", (test) => { + const givenTheVendingMachineHasXInStock = (given: DefineStepFunction) => { + given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 1); + }); + }; - test('Selecting a snack', ({ given, and, when, then }) => { - given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { - vendingMachine = new VendingMachine(); - vendingMachine.stockItem(itemName, 1); - }); + const givenTheVendingMachineHasNoXInStock = (given: DefineStepFunction) => { + given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { + vendingMachine = new VendingMachine(); + vendingMachine.stockItem(itemName, 0); + }); + } - and('I have inserted the correct amount of money', () => { - vendingMachine.insertMoney(myMoney); - }); + const givenIHaveInsertedTheCorrectAmountOfMoney = (given: DefineStepFunction) => { + given('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + }; - when(/^I select "(.*)"$/, (itemName: string) => { - vendingMachine.dispenseItem(itemName); - }); + const whenISelectX = (when: DefineStepFunction) => { + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + }; - then(/^my "(.*)" should be dispensed$/, (itemName: string) => { - const inventoryAmount = vendingMachine.items[itemName]; - expect(inventoryAmount).toBe(0); - }); + const thenXShouldBeDespensed = (then: DefineStepFunction) => { + then(/^my "(.*)" should be dispensed$/, (itemName: string) => { + const inventoryAmount = vendingMachine.items[itemName]; + expect(inventoryAmount).toBe(0); }); + } - test('Selecting a beverage', ({ given, and, when, then }) => { - given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => { - vendingMachine = new VendingMachine(); - vendingMachine.stockItem(itemName, 1); - }); + const thenMyMoneyShouldBeReturned = (then: DefineStepFunction) => { + then(/^my money should be returned$/, () => { + const returnedMoney = vendingMachine.moneyReturnSlot; + expect(returnedMoney).toBe(myMoney); + }); + } - and('I have inserted the correct amount of money', () => { - vendingMachine.insertMoney(myMoney); - }); + rule("Dispenses items if correct amount of money is inserted", (test) => { - when(/^I select "(.*)"$/, (itemName: string) => { - vendingMachine.dispenseItem(itemName); - }); + test('Selecting a snack', ({ given, and, when, then }) => { + givenTheVendingMachineHasXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenXShouldBeDespensed(then); + }); - then(/^my "(.*)" should be dispensed$/, (itemName: string) => { - const inventoryAmount = vendingMachine.items[itemName]; - expect(inventoryAmount).toBe(0); - }); + test('Selecting a beverage', ({ given, and, when, then }) => { + givenTheVendingMachineHasXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenXShouldBeDespensed(then); }); }); rule("Returns my money if item is out of stock", (test) => { test('Selecting a snack', ({ given, and, when, then }) => { - given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { - vendingMachine = new VendingMachine(); - vendingMachine.stockItem(itemName, 0); - }); - - and('I have inserted the correct amount of money', () => { - vendingMachine.insertMoney(myMoney); - }); - - when(/^I select "(.*)"$/, (itemName: string) => { - vendingMachine.dispenseItem(itemName); - }); - - then(/^my money should be returned$/, () => { - const returnedMoney = vendingMachine.moneyReturnSlot; - expect(returnedMoney).toBe(myMoney); - }); + givenTheVendingMachineHasNoXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenMyMoneyShouldBeReturned(then); }); test('Selecting a beverage', ({ given, and, when, then }) => { - given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => { - vendingMachine = new VendingMachine(); - vendingMachine.stockItem(itemName, 0); - }); - - and('I have inserted the correct amount of money', () => { - vendingMachine.insertMoney(myMoney); - }); - - when(/^I select "(.*)"$/, (itemName: string) => { - vendingMachine.dispenseItem(itemName); - }); - - then(/^my money should be returned$/, () => { - const returnedMoney = vendingMachine.moneyReturnSlot; - expect(returnedMoney).toBe(myMoney); - }); + givenTheVendingMachineHasNoXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenMyMoneyShouldBeReturned(then); }); }); }); From 04f7c7133670d513fd94a32db9906f6a843e5f75 Mon Sep 17 00:00:00 2001 From: Markus Mueller Date: Thu, 18 Mar 2021 17:32:04 +0100 Subject: [PATCH 29/29] fix rule describe --- src/feature-definition-creation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/feature-definition-creation.ts b/src/feature-definition-creation.ts index d8f4a4a..457892c 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -332,7 +332,9 @@ export function defineRuleBasedFeature( throw new Error(`No matching rule found for '${ruleText}'"`); } - defineScenarioGroup(matchingRules[0], callback, featureFromFile.options); + describe(ruleText, () => { + defineScenarioGroup(matchingRules[0], callback, featureFromFile.options); + }) }); }); }