diff --git a/examples/typescript/specs/features/extended-rules-auto-step-binding.feature b/examples/typescript/specs/features/extended-rules-auto-step-binding.feature new file mode 100644 index 0000000..58c06d3 --- /dev/null +++ b/examples/typescript/specs/features/extended-rules-auto-step-binding.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/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-auto-step-binding.steps.ts b/examples/typescript/specs/step-definitions/extended-rules-auto-step-binding.steps.ts new file mode 100644 index 0000000..f7a40b1 --- /dev/null +++ b/examples/typescript/specs/step-definitions/extended-rules-auto-step-binding.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-auto-step-binding.feature', {collapseRules: false}); + +autoBindStepsWithRules([feature], [ vendingMachineSteps ]); 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..de49158 --- /dev/null +++ b/examples/typescript/specs/step-definitions/extended-rules-definition.steps.ts @@ -0,0 +1,85 @@ +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}); + +defineRuleBasedFeature(feature, (rule) => { + let vendingMachine: VendingMachine; + + const myMoney = 0.50; + + const givenTheVendingMachineHasXInStock = (given: DefineStepFunction) => { + 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); + }); + } + + const givenIHaveInsertedTheCorrectAmountOfMoney = (given: DefineStepFunction) => { + given('I have inserted the correct amount of money', () => { + vendingMachine.insertMoney(myMoney); + }); + }; + + const whenISelectX = (when: DefineStepFunction) => { + when(/^I select "(.*)"$/, (itemName: string) => { + vendingMachine.dispenseItem(itemName); + }); + }; + + const thenXShouldBeDespensed = (then: DefineStepFunction) => { + then(/^my "(.*)" should be dispensed$/, (itemName: string) => { + const inventoryAmount = vendingMachine.items[itemName]; + expect(inventoryAmount).toBe(0); + }); + } + + const thenMyMoneyShouldBeReturned = (then: DefineStepFunction) => { + then(/^my money should be returned$/, () => { + const returnedMoney = vendingMachine.moneyReturnSlot; + expect(returnedMoney).toBe(myMoney); + }); + } + + rule("Dispenses items if correct amount of money is inserted", (test) => { + + test('Selecting a snack', ({ given, and, when, then }) => { + givenTheVendingMachineHasXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenXShouldBeDespensed(then); + }); + + 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 }) => { + givenTheVendingMachineHasNoXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenMyMoneyShouldBeReturned(then); + }); + + test('Selecting a beverage', ({ given, and, when, then }) => { + givenTheVendingMachineHasNoXInStock(given); + givenIHaveInsertedTheCorrectAmountOfMoney(given); + whenISelectX(when); + thenMyMoneyShouldBeReturned(then); + }); + }); +}); diff --git a/examples/typescript/src/vending-machine.ts b/examples/typescript/src/vending-machine.ts index 3cf5f10..a9f8506 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]--; } } 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/src/automatic-step-binding.ts b/src/automatic-step-binding.ts index 58085d6..83d956e 100644 --- a/src/automatic-step-binding.ts +++ b/src/automatic-step-binding.ts @@ -1,6 +1,11 @@ -import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models'; +import { ParsedFeature, ScenarioGroup } from './models'; import { matchSteps } from './validation/step-definition-validation'; -import { StepsDefinitionCallbackFunction, defineFeature } 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 }> = []; @@ -9,49 +14,84 @@ const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) => globalSteps.push({ stepMatcher, stepFunction }); }; -export const autoBindSteps = (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 registerSteps = (stepDefinitionCallback: StepsDefinitionCallbackFunction) => { + stepDefinitionCallback({ + defineStep: registerStep, + given: registerStep, + when: registerStep, + then: registerStep, + and: registerStep, + but: registerStep, + pending: () => { + // Nothing to do + } + }); +}; + +const matchAndDefineSteps = (group: ScenarioGroup, test: DefineScenarioFunctionWithAliases, errors: string[]) => { + const scenarioOutlineScenarios = group.scenarioOutlines.map((scenarioOutline) => scenarioOutline.scenarios[0]); + + const scenarios = [ ...group.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 "${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 "${group.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); const errors: string[] = []; 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); + }); + }); + + if (errors.length) { + throw new Error(errors.join('\n\n')); + } +}; + +export const autoBindStepsWithRules = ( + features: ParsedFeature[], + stepDefinitions: StepsDefinitionCallbackFunction[] +) => { + stepDefinitions.forEach(registerSteps); + + const errors: string[] = []; + + features.forEach((feature) => { + defineRuleBasedFeature(feature, (ruleDefinition) => { + feature.rules.forEach((rule) => { + ruleDefinition(rule.title, (test) => { + matchAndDefineSteps(rule, test, errors); }); }); }); 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/feature-definition-creation.ts b/src/feature-definition-creation.ts index f95dd89..457892c 100644 --- a/src/feature-definition-creation.ts +++ b/src/feature-definition-creation.ts @@ -5,6 +5,7 @@ import { StepFromStepDefinitions, ParsedFeature, ParsedScenario, Options, ParsedScenarioOutline, + ScenarioGroup } from './models'; import { ensureFeatureFileAndStepDefinitionScenarioHaveSameSteps, @@ -24,6 +25,13 @@ 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, @@ -41,18 +49,18 @@ export type DefineStepFunction = (stepMatcher: string | RegExp, stepDefinitionCa const processScenarioTitleTemplate = ( scenarioTitle: string, - parsedFeature: ParsedFeature, + group: ScenarioGroup, options: Options, parsedScenario: ParsedScenario, parsedScenarioOutline: ParsedScenarioOutline, ) => { if (options && options.scenarioNameTemplate) { try { - return options && options.scenarioNameTemplate({ - featureTitle: parsedFeature.title, + return options && options.scenarioNameTemplate({ + featureTitle: group.title, scenarioTitle: scenarioTitle.toString(), - featureTags: parsedFeature.tags, - scenarioTags: (parsedScenario || parsedScenarioOutline).tags, + featureTags: group.tags, + scenarioTags: (parsedScenario || parsedScenarioOutline).tags }); } catch (err) { throw new Error( @@ -121,7 +129,7 @@ const defineScenario = ( const stepArgument = parsedScenario.steps[index].stepArgument; const matches = matchSteps( parsedScenario.steps[index].stepText, - scenarioFromStepDefinitions.steps[index].stepMatcher, + scenarioFromStepDefinitions.steps[index].stepMatcher ); let matchArgs: string[] = []; @@ -129,7 +137,7 @@ const defineScenario = ( matchArgs = (matches as RegExpMatchArray).slice(1); } - const args = [...matchArgs, stepArgument]; + const args = [ ...matchArgs, stepArgument ]; return promiseChain.then(() => nextStep.stepFunction(...args)); }, Promise.resolve()); @@ -138,7 +146,8 @@ const defineScenario = ( const createDefineScenarioFunction = ( featureFromStepDefinitions: FeatureFromStepDefinitions, - parsedFeature: ParsedFeature, + parsedFeature: ScenarioGroup, + options: Options, only: boolean = false, skip: boolean = false, concurrent: boolean = false, @@ -164,7 +173,7 @@ const createDefineScenarioFunction = ( but: createDefineStepFunction(scenarioFromStepDefinitions), pending: () => { // Nothing to do - }, + } }); const parsedScenario = parsedFeature.scenarios @@ -173,8 +182,6 @@ const createDefineScenarioFunction = ( const parsedScenarioOutline = parsedFeature.scenarioOutlines .filter((s) => s.title.toLowerCase() === scenarioTitle.toLowerCase())[0]; - const options = parsedFeature.options; - scenarioTitle = processScenarioTitleTemplate( scenarioTitle, parsedFeature, @@ -191,7 +198,7 @@ const createDefineScenarioFunction = ( if (checkForPendingSteps(scenarioFromStepDefinitions)) { xtest(scenarioTitle, () => { - // Nothing to do + // Nothing to do }, undefined); } else if (parsedScenario) { defineScenario( @@ -223,13 +230,19 @@ const createDefineScenarioFunction = ( const createDefineScenarioFunctionWithAliases = ( featureFromStepDefinitions: FeatureFromStepDefinitions, - parsedFeature: ParsedFeature, + group: ScenarioGroup, + options: Options ) => { - const defineScenarioFunctionWithAliases = createDefineScenarioFunction(featureFromStepDefinitions, parsedFeature); + const defineScenarioFunctionWithAliases = createDefineScenarioFunction( + featureFromStepDefinitions, + group, + options + ); (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).only = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, + options, true, false, false, @@ -237,7 +250,8 @@ const createDefineScenarioFunctionWithAliases = ( (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).skip = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, + options, false, true, false, @@ -245,7 +259,8 @@ const createDefineScenarioFunctionWithAliases = ( (defineScenarioFunctionWithAliases as DefineScenarioFunctionWithAliases).concurrent = createDefineScenarioFunction( featureFromStepDefinitions, - parsedFeature, + group, + options, false, false, true, @@ -265,16 +280,17 @@ const createDefineStepFunction = (scenarioFromStepDefinitions: ScenarioFromStepD }; }; -export function defineFeature( - featureFromFile: ParsedFeature, +const defineScenarioGroup = ( + group: ScenarioGroup, scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction, -) { + options: Options, +) => { const featureFromDefinedSteps: FeatureFromStepDefinitions = { - title: featureFromFile.title, + title: group.title, scenarios: [], }; - const parsedFeatureWithTagFiltersApplied = applyTagFilters(featureFromFile); + const parsedFeatureWithTagFiltersApplied = applyTagFilters(group, options.tagFilter); if ( parsedFeatureWithTagFiltersApplied.scenarios.length === 0 @@ -283,14 +299,42 @@ export function defineFeature( return; } + scenariosDefinitionCallback( + createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied, options) + ); + + checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( + parsedFeatureWithTagFiltersApplied, + featureFromDefinedSteps, + options + ); +}; + +export function defineFeature( + featureFromFile: ParsedFeature, + scenariosDefinitionCallback: ScenariosDefinitionCallbackFunction +) { describe(featureFromFile.title, () => { - scenariosDefinitionCallback( - createDefineScenarioFunctionWithAliases(featureFromDefinedSteps, parsedFeatureWithTagFiltersApplied), - ); + defineScenarioGroup(featureFromFile, scenariosDefinitionCallback, featureFromFile.options); + }); +} - checkThatFeatureFileAndStepDefinitionsHaveSameScenarios( - parsedFeatureWithTagFiltersApplied, - 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.toLocaleLowerCase() + ); + if (matchingRules.length != 1) { + throw new Error(`No matching rule found for '${ruleText}'"`); + } + + describe(ruleText, () => { + defineScenarioGroup(matchingRules[0], callback, featureFromFile.options); + }) + }); }); } diff --git a/src/index.ts b/src/index.ts index b10042d..6b44424 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ 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, 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'; diff --git a/src/models.ts b/src/models.ts index d1c4747..9931a4b 100644 --- a/src/models.ts +++ b/src/models.ts @@ -37,12 +37,16 @@ export type ParsedScenarioOutline = { skippedViaTagFilter: boolean; }; -export type ParsedFeature = { +export type ScenarioGroup = { title: string; scenarios: ParsedScenario[]; scenarioOutlines: ParsedScenarioOutline[]; - options: Options; tags: string[]; +} + +export interface ParsedFeature extends ScenarioGroup { + rules: ScenarioGroup[]; + options: Options; }; export type ScenarioNameTemplateVars = { @@ -62,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 6cb8a59..88f56b2 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, ScenarioGroup} 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 ScenarioGroup; +} + 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 parseAndCollapseBackgrounds = (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 { @@ -257,6 +269,21 @@ const collapseRulesAndBackgrounds = (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); @@ -330,7 +357,11 @@ export const parseFeature = (featureText: string, options?: Options): ParsedFeat throw new Error(`Error parsing feature Gherkin: ${err.message}`); } - let astFeature = collapseRulesAndBackgrounds(ast.feature); + let astFeature = parseAndCollapseBackgrounds(ast.feature); + + if(options?.collapseRules) { + astFeature = collapseRules(astFeature); + } if (astFeature.language !== 'en') { astFeature = translateKeywords(astFeature); @@ -340,6 +371,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/tag-filtering.ts b/src/tag-filtering.ts index ca502d1..af3658b 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,23 +74,24 @@ const setScenarioSkipped = (parsedFeature: ParsedFeature, scenario: ParsedScenar }; export const applyTagFilters = ( - parsedFeature: ParsedFeature, + group: ScenarioGroup, + tagFilter: string | undefined ) => { - if (parsedFeature.options.tagFilter === undefined) { - return parsedFeature; + if (tagFilter === undefined) { + return group; } - const scenarios = parsedFeature.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario)); - const scenarioOutlines = parsedFeature.scenarioOutlines + const scenarios = group.scenarios.map((scenario) => setScenarioSkipped(group, scenario, tagFilter)); + const scenarioOutlines = group.scenarioOutlines .map((scenarioOutline) => { return { - ...setScenarioSkipped(parsedFeature, scenarioOutline), - scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(parsedFeature, scenario)), + ...setScenarioSkipped(group, scenarioOutline, tagFilter), + scenarios: scenarioOutline.scenarios.map((scenario) => setScenarioSkipped(group, scenario, tagFilter)), }; }); return { - ...parsedFeature, + ...group, scenarios, scenarioOutlines, } as ParsedFeature; diff --git a/src/validation/scenario-validation.ts b/src/validation/scenario-validation.ts index dbd6481..adff65f 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,7 +79,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( parsedScenarios = parsedScenarios.concat(parsedFeature.scenarioOutlines); } - if (parsedFeature.options && parsedFeature.options.errors === false) { + if (options && options.errors === false) { return; } @@ -89,7 +92,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, parsedScenarios, scenarioFromStepDefinitions.title, - parsedFeature.options.errors as ErrorOptions, + options.errors as ErrorOptions, ); }); } @@ -102,7 +105,7 @@ export const checkThatFeatureFileAndStepDefinitionsHaveSameScenarios = ( errors, featureFromStepDefinitions && featureFromStepDefinitions.scenarios, parsedScenario, - parsedFeature.options.errors as ErrorOptions, + options.errors as ErrorOptions, ); }); }