Skip to content

Commit 5d54bc2

Browse files
author
bruno-morel
committed
fix(scenario): when a scenario has a mix of outline variable and the definition have regular expression, is can all be mixed up and crash, this covers most cases that should be allowed
1 parent 9c30df2 commit 5d54bc2

File tree

3 files changed

+198
-61
lines changed

3 files changed

+198
-61
lines changed

src/index.js

Lines changed: 108 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -91,80 +91,129 @@ function matchJestTestSuiteWithCucumberFeature( featureScenariosOrOutline, befor
9191
function matchJestTestWithCucumberScenario( currentScenarioTitle, currentScenarioSteps, testFn, isOutline ){
9292
testFn( currentScenarioTitle, ( { given, when, then, and, but } ) => {
9393
currentScenarioSteps.forEach( ( currentStep ) => {
94-
// if( !stepsDefinition[ currentStep.keyword ] )
95-
// return
9694

9795
matchJestDefinitionWithCucumberStep( { given, when, then, and, but }, currentStep.keyword, currentStep.stepText, isOutline )
96+
9897
} )
9998
} )
10099
}
101100

102-
function matchJestDefinitionWithCucumberStep( { given, when, then, and, but }, currentStepKeyWork, currentStepText, isOutline ){
103-
const foundMatchingStep = findStep( currentStepKeyWork, currentStepText, isOutline )
104-
if( !foundMatchingStep )
105-
return
106-
107-
switch ( currentStepKeyWork ) {
108-
case "given":
109-
given( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
110-
break
111-
112-
case "when":
113-
when( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
114-
break
101+
function matchJestDefinitionWithCucumberStep( verbFunction, currentStepKeyword, currentStepText, isOutline ){
102+
103+
const foundMatchingStep = findMatchingStep( currentStepKeyword, currentStepText, isOutline )
104+
if( !foundMatchingStep ) return
105+
106+
// this will be the "given", "when", "then"...functions
107+
verbFunction[ currentStepKeyword ] ( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
108+
}
115109

116-
case "then":
117-
then( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
118-
break
119110

120-
case "but":
121-
but( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
122-
break
111+
function findMatchingStep( scenarioType, scenarioSentence, isOutline ) {
112+
const foundStep = Object.keys( stepsDefinition[ scenarioType ] )
113+
.find( ( currentStepDefinitionFunction ) => {
114+
return isFunctionForScenario( scenarioSentence,
115+
stepsDefinition[ scenarioType ][ currentStepDefinitionFunction ],
116+
isOutline )
117+
} )
118+
if( !foundStep ) return null
119+
120+
return injectVariable( scenarioType, scenarioSentence, foundStep )
121+
}
123122

124-
case "and":
125-
default:
126-
and( foundMatchingStep.stepExpression, foundMatchingStep.stepFn )
127-
break
123+
function isFunctionForScenario( scenarioSentence, stepDefinitionFunction, isOutline ){
124+
if( stepDefinitionFunction.stepRegExp ){
125+
if( isOutline && /<[\w]*>/.test( scenarioSentence ) ){
126+
return isPotentialStepFunctionForScenario( scenarioSentence, stepDefinitionFunction.stepRegExp )
127+
}
128+
129+
else return scenarioSentence.match( stepDefinitionFunction.stepRegExp )
128130
}
131+
132+
return scenarioSentence === stepDefinitionFunction.stepExpression
129133
}
130134

131-
function findStep( scenarioType, scenarioSentence, isOutline ) {
132-
// if( !stepsDefinition[ scenarioType ] )
133-
// return null
134-
135-
const foundStep = Object.keys( stepsDefinition[ scenarioType ] ).find( ( currentSentence ) => {
136-
if( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp ){
137-
if( isOutline && /<[\w]*>/.test( scenarioSentence ) ){
138-
const cleanedSentence = scenarioSentence.replace( /<[\w]*>/gi, '' )
139-
const cleanedRegexp = stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp.source
140-
.replace( /^\^/, '' )
141-
.replace( /\\\(/g, '(' )
142-
.replace( /\\\)/g, ')')
143-
.replace( /\\\^/g, '^')
144-
.replace( /\\\$/g, '$')
145-
.replace( /\$$/, '' )
146-
.replace( /\([.\\]+[sSdDwWbB*][*?+]?\)/g, '')
147-
.replace( /\(\[.*\](?:[+?*]{1}|\{\d\})\)/g, '' )
148-
149-
// const groupInStepDef = new RegExp( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp.source + '|' ).exec('')
150-
// const numGroupInStepDef = groupInStepDef.length - 1
151-
// const groupInSentence = /(<[\w]*>)|/gm.exec( scenarioSentence )
152-
// const numGroupInSentence = /(<[\w]*>)|/gm.exec( scenarioSentence ).length - 1
153-
//check that we have the same number of capture group than enclosed variables in the expression
154-
return cleanedRegexp === cleanedSentence
135+
136+
function isPotentialStepFunctionForScenario( scenarioDefinition, regStepFunc ){
137+
//so this one is tricky, to ensure we only find the
138+
// step definition corresponding to actual steps function in the case of outlined gherkin
139+
// we have to "disable" the outlining (since it can replace regular expression
140+
// and then ensure that all "non-outlined" part do respect the regular expression of
141+
// of the step function
142+
// FIRST, we clean the string version of the step definition that has outline variable
143+
const cleanedStepFunc = regStepFunc.source
144+
.replace( /^\^/, '' )
145+
// .replace( /\\\(/g, '(' )
146+
// .replace( /\\\)/g, ')')
147+
// .replace( /\\\^/g, '^')
148+
// .replace( /\\\$/g, '$')
149+
.replace( /\$$/, '' )
150+
// .replace( /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g, '' )
151+
152+
let currentScenarioPart
153+
let currentStepFuncLeft = cleanedStepFunc
154+
let currentScenarioDefLeft = scenarioDefinition
155+
156+
//we step through each of the scenario outline variables
157+
// from there, we will try to detect any regexp present in the
158+
// step definition, so that we can ensure to find the right match
159+
while( ( currentScenarioPart = /<[\w]*>/gi.exec( currentScenarioDefLeft ) ) != null ){
160+
161+
let fixedPart = currentScenarioPart.input.substring( 0, currentScenarioPart.index )
162+
let idxCutScenarioPart = currentScenarioPart.index + currentScenarioPart[ 0 ].length
163+
164+
const regEscapedStepFunc = /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.exec( currentStepFuncLeft.replace( /\\\(/g, '(' )
165+
.replace( /\\\)/g, ')')
166+
.replace( /\\\^/g, '^')
167+
.replace( /\\\$/g, '$') )
168+
const regStepFuncLeft = /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.exec( currentStepFuncLeft )
169+
170+
if( regStepFuncLeft && regEscapedStepFunc.index == currentScenarioPart.index ){
171+
//if we have a regex inside our step function definition
172+
// and that regex is at the same position than our Outlined variable
173+
// we just need to check that the sentence match,
174+
// so we can "evaluate" the step function and remove the regex in it
175+
currentStepFuncLeft = regEscapedStepFunc.input.substring( 0, regEscapedStepFunc.index )
176+
+ currentStepFuncLeft.substring( regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )
177+
178+
}
179+
else if( regStepFuncLeft && regStepFuncLeft.index < currentScenarioPart.index ){
180+
//if we have a regex inside our step function definition
181+
// but that regex is not at the same position than our outlined variable
182+
// we need to evaluate the regex against the scenario part
183+
const strRegexToEvaluate = regStepFuncLeft.input.substring( 0, regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )
184+
const regexToEvaluate = new RegExp( strRegexToEvaluate )
185+
const regIntermediatePart = regexToEvaluate.exec( currentScenarioPart.input )
186+
if( regIntermediatePart ){
187+
fixedPart = regStepFuncLeft.input.substring( 0, regStepFuncLeft.index + regStepFuncLeft[ 0 ].length )
188+
idxCutScenarioPart = regIntermediatePart[ 0 ].length
155189
}
190+
}
191+
192+
const partIndex = currentStepFuncLeft.indexOf( fixedPart )
193+
if( partIndex !== -1 ){
194+
currentStepFuncLeft = currentStepFuncLeft.substring( partIndex + fixedPart.length )
195+
currentScenarioDefLeft = currentScenarioDefLeft.substring( idxCutScenarioPart )
196+
}
197+
else {
198+
return false
199+
}
200+
}
201+
202+
return ( currentScenarioDefLeft === '' && currentStepFuncLeft === '' )
203+
|| evaluateStepFuncEndVsScenarioEnd( currentStepFuncLeft, currentScenarioDefLeft )
204+
}
156205

157-
else
158-
return scenarioSentence.match( stepsDefinition[ scenarioType ][ currentSentence ].stepRegExp )
206+
function evaluateStepFuncEndVsScenarioEnd( stepFunctionDef, scenarioDefinition ) {
207+
if( /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.test( stepFunctionDef ) ){
208+
return new RegExp( stepFunctionDef ).test( scenarioDefinition )
209+
}
210+
211+
return stepFunctionDef.endsWith( scenarioDefinition )
212+
}
159213

160-
}
161-
162-
return scenarioSentence === stepsDefinition[ scenarioType ][ currentSentence ].stepExpression
163-
} )
164-
if( !foundStep )
165-
return null
166214

167-
const stepObject = stepsDefinition[ scenarioType ][ foundStep ]
215+
function injectVariable( scenarioType, scenarioSentence, stepFunctionDefinition ){
216+
const stepObject = stepsDefinition[ scenarioType ][ stepFunctionDefinition ]
168217

169218
if( !stepObject.stepRegExp )
170219
return {
@@ -186,7 +235,7 @@ function findStep( scenarioType, scenarioSentence, isOutline ) {
186235
const dynamicMatchThatAreVariables = exprMatches //exprMatches.filter( ( currentMatch ) => {
187236
// return foundStep.indexOf( currentMatch ) === -1
188237
// } )
189-
238+
190239
return {
191240
stepExpression: stepObject.stepRegExp,
192241
stepFn: () => ( stepObject.stepFn( ...dynamicMatchThatAreVariables ) )

test/specs/features/scenario-outlines.feature

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Feature: Online sales
1515

1616
Scenario Outline: Selling all of one
1717
Given I want to sell all my <Item>
18-
When I sell all my <Item> at the price of $<Amount>
18+
When I sell all my <Item> at the price of $<Amount> CAD
1919
Then I should still get $<Amount>
2020

2121
Examples:
@@ -30,3 +30,49 @@ Feature: Online sales
3030
Given I have an Item named '<ThatCouldLookLikeAnOutlineVariable>'
3131
When I sell <ThatCouldLookLikeAnOutlineVariable With Spaces in it>
3232
Then I get $<Amount>
33+
34+
35+
Scenario Outline: Additional regexp in outline line
36+
Given I want to sell all my <Item>
37+
When I sell all my <Item> at the price of $100 CAD
38+
Then I should still get $<Amount>
39+
40+
Examples:
41+
| Item | Amount |
42+
| Autographed Neil deGrasse Tyson book | 100 |
43+
| Rick Astley t-shirt | 100 |
44+
45+
46+
Scenario Outline: Additional regexp in outline line with non-string variables for <Item>
47+
Given I want to sell all my <Item>
48+
When I sell all my <Item> with a starting price of $<StartingPrice> at the rebate price of $100
49+
Then I should still get $<Amount>
50+
51+
Examples:
52+
| Item | StartingPrice | Amount |
53+
| Autographed Neil deGrasse Tyson book | 100 | 100 |
54+
| Rick Astley t-shirt | 22 | 100 |
55+
#
56+
#
57+
Scenario Outline: Additional regexp in outline line with non-string variables and static one in the middle for <Item>
58+
Given I want to sell all my <Item>
59+
When I sell all my <Item> with a starting price of $100 at the rebate price of $<RebatePrice>
60+
Then I should still get $<Amount>
61+
62+
Examples:
63+
| Item | RebatePrice | Amount |
64+
| Autographed Neil deGrasse Tyson book | 20 | 20 |
65+
| Rick Astley t-shirt | 50 | 50 |
66+
67+
# Ability: this ones are not currently possible
68+
#
69+
# Scenario Outline: Additional mix regexp and non-regexp in outline line with non-string variables and static one in the middle for <Item>
70+
# Given I want to sell all my <Item>
71+
# When I sell all my <Item> with a starting price <Description> price of <RebatePrice>$ <Currency> which is nice
72+
# Then I should still get $<Amount>
73+
#
74+
# Examples:
75+
# | Item | Description | RebatePrice | Amount | Currency |
76+
# | Autographed Neil deGrasse Tyson book | of $100 at the fantastic | 20 | 20 | USD |
77+
# | Rick Astley t-shirt | of $100 at the fantastic | 50 | 50 | CAD |
78+
# | Rick Astley t-shirt | of $100 at the rebate | 50 | 50 | CAD |

test/specs/features/step-definitions/scenario-outlines.steps.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,50 @@ Given( /^I want to sell all my (.*)$/, item => {
3535
onlineSales.listItem(item)
3636
} )
3737

38-
When( /^I sell all my (.*) at the price of \$(\d+)$/, ( item, expectedSalesPrice ) => {
38+
When( /^I sell all my (.*) at the price of \$(\d+) CAD$/, ( item, expectedSalesPrice ) => {
3939
salesPrice = onlineSales.sellItem(item)
40+
if( salesPrice )
41+
salesPrice = parseInt( expectedSalesPrice )
42+
} )
43+
44+
When( /^I sell all my (.*) with a starting price of \$(\d+) at the rebate price of \$(\d+)$/, ( item, startingPrice, expectedSalesPrice ) => {
45+
salesPrice = onlineSales.sellItem(item)
46+
if( salesPrice ){
47+
salesPrice = parseInt( expectedSalesPrice )
48+
}
49+
else {
50+
salesPrice = null
51+
}
52+
} )
53+
54+
When( /^I sell all my (.*) with a starting price of \$(\d+) at the fantastic price of (\d+)\$ USD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
55+
salesPrice = onlineSales.sellItem(item)
56+
if( salesPrice ){
57+
salesPrice = parseInt( expectedSalesPrice )
58+
}
59+
else {
60+
salesPrice = null
61+
}
62+
} )
63+
64+
When( /^I sell all my (.*) with a starting price of \$(\d+) at the fantastic price of (\d+)\$ CAD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
65+
salesPrice = onlineSales.sellItem(item)
66+
if( salesPrice ){
67+
salesPrice = parseInt( expectedSalesPrice )
68+
}
69+
else {
70+
salesPrice = null
71+
}
72+
} )
73+
74+
When( /^I sell all my (.*) with a starting price of \$(\d+) at the rebate price of (\d+)\$ CAD which is nice$/, ( item, startingPrice, expectedSalesPrice ) => {
75+
salesPrice = onlineSales.sellItem(item)
76+
if( salesPrice ){
77+
salesPrice = parseInt( expectedSalesPrice )
78+
}
79+
else {
80+
salesPrice = null
81+
}
4082
} )
4183

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

0 commit comments

Comments
 (0)