Skip to content

Improved rules support #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 "<beverage>" in stock
And I have inserted the correct amount of money
When I select "<beverage>"
Then my "<beverage>" 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
Original file line number Diff line number Diff line change
@@ -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 "<beverage>" in stock
And I have inserted the correct amount of money
When I select "<beverage>"
Then my "<beverage>" 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
Original file line number Diff line number Diff line change
@@ -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 ]);
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
9 changes: 7 additions & 2 deletions examples/typescript/src/vending-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]--;
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 78 additions & 38 deletions src/automatic-step-binding.ts
Original file line number Diff line number Diff line change
@@ -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 }> = [];
Expand All @@ -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);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const defaultErrorSettings = {
const defaultConfiguration: Options = {
tagFilter: undefined,
scenarioNameTemplate: undefined,
collapseRules: true,
errors: defaultErrorSettings,
};

Expand Down
Loading