Skip to content

Commit

Permalink
fix(scenario): when a scenario has a mix of outline variable and the …
Browse files Browse the repository at this point in the history
…definition have regular expression, is can all be mixed up and crash, this covers most cases that should be allowed
  • Loading branch information
bruno-morel committed Jun 23, 2020
1 parent 9c30df2 commit 5d54bc2
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 61 deletions.
167 changes: 108 additions & 59 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,80 +91,129 @@ function matchJestTestSuiteWithCucumberFeature( featureScenariosOrOutline, befor
function matchJestTestWithCucumberScenario( currentScenarioTitle, currentScenarioSteps, testFn, isOutline ){
testFn( currentScenarioTitle, ( { given, when, then, and, but } ) => {
currentScenarioSteps.forEach( ( currentStep ) => {
// if( !stepsDefinition[ currentStep.keyword ] )
// return

matchJestDefinitionWithCucumberStep( { given, when, then, and, but }, currentStep.keyword, currentStep.stepText, isOutline )

} )
} )
}

function matchJestDefinitionWithCucumberStep( { given, when, then, and, but }, currentStepKeyWork, currentStepText, isOutline ){
const foundMatchingStep = findStep( currentStepKeyWork, currentStepText, isOutline )
if( !foundMatchingStep )
return

switch ( currentStepKeyWork ) {
case "given":
given( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
break

case "when":
when( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
break
function matchJestDefinitionWithCucumberStep( verbFunction, currentStepKeyword, currentStepText, isOutline ){

const foundMatchingStep = findMatchingStep( currentStepKeyword, currentStepText, isOutline )
if( !foundMatchingStep ) return

// this will be the "given", "when", "then"...functions
verbFunction[ currentStepKeyword ] ( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
}

case "then":
then( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
break

case "but":
but( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
break
function findMatchingStep( scenarioType, scenarioSentence, isOutline ) {
const foundStep = Object.keys( stepsDefinition[ scenarioType ] )
.find( ( currentStepDefinitionFunction ) => {
return isFunctionForScenario( scenarioSentence,
stepsDefinition[ scenarioType ][ currentStepDefinitionFunction ],
isOutline )
} )
if( !foundStep ) return null

return injectVariable( scenarioType, scenarioSentence, foundStep )
}

case "and":
default:
and( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
break
function isFunctionForScenario( scenarioSentence, stepDefinitionFunction, isOutline ){
if( stepDefinitionFunction.stepRegExp ){
if( isOutline && /<[\w]*>/.test( scenarioSentence ) ){
return isPotentialStepFunctionForScenario( scenarioSentence, stepDefinitionFunction.stepRegExp )
}

else return scenarioSentence.match( stepDefinitionFunction.stepRegExp )
}

return scenarioSentence === stepDefinitionFunction.stepExpression
}

function findStep( scenarioType, scenarioSentence, isOutline ) {
// if( !stepsDefinition[ scenarioType ] )
// return null

const foundStep = Object.keys( stepsDefinition[ scenarioType ] ).find( ( currentSentence ) => {
if( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp ){
if( isOutline && /<[\w]*>/.test( scenarioSentence ) ){
const cleanedSentence = scenarioSentence.replace( /<[\w]*>/gi, '' )
const cleanedRegexp = stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp.source
.replace( /^\^/, '' )
.replace( /\\\(/g, '(' )
.replace( /\\\)/g, ')')
.replace( /\\\^/g, '^')
.replace( /\\\$/g, '$')
.replace( /\$$/, '' )
.replace( /\([.\\]+[sSdDwWbB*][*?+]?\)/g, '')
.replace( /\(\[.*\](?:[+?*]{1}|\{\d\})\)/g, '' )

// const groupInStepDef = new RegExp( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp.source + '|' ).exec('')
// const numGroupInStepDef = groupInStepDef.length - 1
// const groupInSentence = /(<[\w]*>)|/gm.exec( scenarioSentence )
// const numGroupInSentence = /(<[\w]*>)|/gm.exec( scenarioSentence ).length - 1
//check that we have the same number of capture group than enclosed variables in the expression
return cleanedRegexp === cleanedSentence

function isPotentialStepFunctionForScenario( scenarioDefinition, regStepFunc ){
//so this one is tricky, to ensure we only find the
// step definition corresponding to actual steps function in the case of outlined gherkin
// we have to "disable" the outlining (since it can replace regular expression
// and then ensure that all "non-outlined" part do respect the regular expression of
// of the step function
// FIRST, we clean the string version of the step definition that has outline variable
const cleanedStepFunc = regStepFunc.source
.replace( /^\^/, '' )
// .replace( /\\\(/g, '(' )
// .replace( /\\\)/g, ')')
// .replace( /\\\^/g, '^')
// .replace( /\\\$/g, '$')
.replace( /\$$/, '' )
// .replace( /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g, '' )

let currentScenarioPart
let currentStepFuncLeft = cleanedStepFunc
let currentScenarioDefLeft = scenarioDefinition

//we step through each of the scenario outline variables
// from there, we will try to detect any regexp present in the
// step definition, so that we can ensure to find the right match
while( ( currentScenarioPart = /<[\w]*>/gi.exec( currentScenarioDefLeft ) ) != null ){

let fixedPart = currentScenarioPart.input.substring( 0, currentScenarioPart.index )
let idxCutScenarioPart = currentScenarioPart.index + currentScenarioPart[ 0 ].length

const regEscapedStepFunc = /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.exec( currentStepFuncLeft.replace( /\\\(/g, '(' )
.replace( /\\\)/g, ')')
.replace( /\\\^/g, '^')
.replace( /\\\$/g, '$') )
const regStepFuncLeft = /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.exec( currentStepFuncLeft )

if( regStepFuncLeft && regEscapedStepFunc.index == currentScenarioPart.index ){
//if we have a regex inside our step function definition
// and that regex is at the same position than our Outlined variable
// we just need to check that the sentence match,
// so we can "evaluate" the step function and remove the regex in it
currentStepFuncLeft = regEscapedStepFunc.input.substring( 0, regEscapedStepFunc.index )
+ currentStepFuncLeft.substring( regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )

}
else if( regStepFuncLeft && regStepFuncLeft.index < currentScenarioPart.index ){
//if we have a regex inside our step function definition
// but that regex is not at the same position than our outlined variable
// we need to evaluate the regex against the scenario part
const strRegexToEvaluate = regStepFuncLeft.input.substring( 0, regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )
const regexToEvaluate = new RegExp( strRegexToEvaluate )
const regIntermediatePart = regexToEvaluate.exec( currentScenarioPart.input )
if( regIntermediatePart ){
fixedPart = regStepFuncLeft.input.substring( 0, regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )
idxCutScenarioPart = regIntermediatePart[ 0 ].length
}
}

const partIndex = currentStepFuncLeft.indexOf( fixedPart )
if( partIndex !== -1 ){
currentStepFuncLeft = currentStepFuncLeft.substring( partIndex + fixedPart.length )
currentScenarioDefLeft = currentScenarioDefLeft.substring( idxCutScenarioPart )
}
else {
return false
}
}

return ( currentScenarioDefLeft === '' && currentStepFuncLeft === '' )
|| evaluateStepFuncEndVsScenarioEnd( currentStepFuncLeft, currentScenarioDefLeft )
}

else
return scenarioSentence.match( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp )
function evaluateStepFuncEndVsScenarioEnd( stepFunctionDef, scenarioDefinition ) {
if( /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.test( stepFunctionDef ) ){
return new RegExp( stepFunctionDef ).test( scenarioDefinition )
}

return stepFunctionDef.endsWith( scenarioDefinition )
}

}

return scenarioSentence === stepsDefinition[ scenarioType ][ currentSentence ].stepExpression
} )
if( !foundStep )
return null

const stepObject = stepsDefinition[ scenarioType ][ foundStep ]
function injectVariable( scenarioType, scenarioSentence, stepFunctionDefinition ){
const stepObject = stepsDefinition[ scenarioType ][ stepFunctionDefinition ]

if( !stepObject.stepRegExp )
return {
Expand All @@ -186,7 +235,7 @@ function findStep( scenarioType, scenarioSentence, isOutline ) {
const dynamicMatchThatAreVariables = exprMatches //exprMatches.filter( ( currentMatch ) => {
// return foundStep.indexOf( currentMatch ) === -1
// } )

return {
stepExpression: stepObject.stepRegExp,
stepFn: () => ( stepObject.stepFn( ...dynamicMatchThatAreVariables ) )
Expand Down
48 changes: 47 additions & 1 deletion test/specs/features/scenario-outlines.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Feature: Online sales

Scenario Outline: Selling all of one
Given I want to sell all my <Item>
When I sell all my <Item> at the price of $<Amount>
When I sell all my <Item> at the price of $<Amount> CAD
Then I should still get $<Amount>

Examples:
Expand All @@ -30,3 +30,49 @@ Feature: Online sales
Given I have an Item named '<ThatCouldLookLikeAnOutlineVariable>'
When I sell <ThatCouldLookLikeAnOutlineVariable With Spaces in it>
Then I get $<Amount>


Scenario Outline: Additional regexp in outline line
Given I want to sell all my <Item>
When I sell all my <Item> at the price of $100 CAD
Then I should still get $<Amount>

Examples:
| Item | Amount |
| Autographed Neil deGrasse Tyson book | 100 |
| Rick Astley t-shirt | 100 |


Scenario Outline: Additional regexp in outline line with non-string variables for <Item>
Given I want to sell all my <Item>
When I sell all my <Item> with a starting price of $<StartingPrice> at the rebate price of $100
Then I should still get $<Amount>

Examples:
| Item | StartingPrice | Amount |
| Autographed Neil deGrasse Tyson book | 100 | 100 |
| Rick Astley t-shirt | 22 | 100 |
#
#
Scenario Outline: Additional regexp in outline line with non-string variables and static one in the middle for <Item>
Given I want to sell all my <Item>
When I sell all my <Item> with a starting price of $100 at the rebate price of $<RebatePrice>
Then I should still get $<Amount>

Examples:
| Item | RebatePrice | Amount |
| Autographed Neil deGrasse Tyson book | 20 | 20 |
| Rick Astley t-shirt | 50 | 50 |

# Ability: this ones are not currently possible
#
# Scenario Outline: Additional mix regexp and non-regexp in outline line with non-string variables and static one in the middle for <Item>
# Given I want to sell all my <Item>
# When I sell all my <Item> with a starting price <Description> price of <RebatePrice>$ <Currency> which is nice
# Then I should still get $<Amount>
#
# Examples:
# | Item | Description | RebatePrice | Amount | Currency |
# | Autographed Neil deGrasse Tyson book | of $100 at the fantastic | 20 | 20 | USD |
# | Rick Astley t-shirt | of $100 at the fantastic | 50 | 50 | CAD |
# | Rick Astley t-shirt | of $100 at the rebate | 50 | 50 | CAD |
44 changes: 43 additions & 1 deletion test/specs/features/step-definitions/scenario-outlines.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,50 @@ Given( /^I want to sell all my (.*)$/, item => {
onlineSales.listItem(item)
} )

When( /^I sell all my (.*) at the price of \$(\d+)$/, ( item, expectedSalesPrice ) => {
When( /^I sell all my (.*) at the price of \$(\d+) CAD$/, ( item, expectedSalesPrice ) => {
salesPrice = onlineSales.sellItem(item)
if( salesPrice )
salesPrice = parseInt( expectedSalesPrice )
} )

When( /^I sell all my (.*) with a starting price of \$(\d+) at the rebate price of \$(\d+)$/, ( item, startingPrice, expectedSalesPrice ) => {
salesPrice = onlineSales.sellItem(item)
if( salesPrice ){
salesPrice = parseInt( expectedSalesPrice )
}
else {
salesPrice = null
}
} )

When( /^I sell all my (.*) with a starting price of \$(\d+) at the fantastic price of (\d+)\$ USD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
salesPrice = onlineSales.sellItem(item)
if( salesPrice ){
salesPrice = parseInt( expectedSalesPrice )
}
else {
salesPrice = null
}
} )

When( /^I sell all my (.*) with a starting price of \$(\d+) at the fantastic price of (\d+)\$ CAD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
salesPrice = onlineSales.sellItem(item)
if( salesPrice ){
salesPrice = parseInt( expectedSalesPrice )
}
else {
salesPrice = null
}
} )

When( /^I sell all my (.*) with a starting price of \$(\d+) at the rebate price of (\d+)\$ CAD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
salesPrice = onlineSales.sellItem(item)
if( salesPrice ){
salesPrice = parseInt( expectedSalesPrice )
}
else {
salesPrice = null
}
} )

Then( /^I should still get \$(\d+)$/, expectedSalesPrice => {
Expand Down

0 comments on commit 5d54bc2

Please sign in to comment.