@g_package/jest-cucumber-fusion 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +9 -10
  2. package/codecov +0 -0
  3. package/codecov.SHA256SUM +1 -1
  4. package/codecov.SHA256SUM.sig +13 -13
  5. package/docs/product/expectations/fix-l2-outline-regex/outline-regex-binds.md +148 -0
  6. package/docs/product/expectations/fix-l4-escaped-parens-outline/escaped-parens-bind-in-outlines.md +106 -0
  7. package/docs/product/expectations/fix-m3-singleton-reset/clean-slate-per-feature.md +133 -0
  8. package/package.json +11 -5
  9. package/src/index.d.ts +15 -5
  10. package/src/index.js +196 -102
  11. package/test/specs/features/l3-step-argument-delivery.feature +44 -0
  12. package/test/specs/features/m6-hooks-once-per-test.feature +21 -0
  13. package/test/specs/features/m6-hooks-outline-only.feature +12 -0
  14. package/test/specs/features/scenario-outline2.feature +6 -4
  15. package/test/specs/features/step-definitions/ambiguous-step-shadowing.steps.js +92 -0
  16. package/test/specs/features/step-definitions/basic-scenarios.steps.js +19 -2
  17. package/test/specs/features/step-definitions/fuzz-properties.steps.js +170 -0
  18. package/test/specs/features/step-definitions/hook-error.steps.js +59 -0
  19. package/test/specs/features/step-definitions/l2-outline-edge-cases.steps.js +327 -0
  20. package/test/specs/features/step-definitions/l3-step-argument-delivery.steps.js +191 -0
  21. package/test/specs/features/step-definitions/l4-escaped-parens-outline.steps.js +256 -0
  22. package/test/specs/features/step-definitions/m1-before-hooks-clobber.steps.js +64 -0
  23. package/test/specs/features/step-definitions/m2-duplicate-matcher.steps.js +60 -0
  24. package/test/specs/features/step-definitions/m3-singleton-reset.steps.js +275 -0
  25. package/test/specs/features/step-definitions/m4-callsite-resolution.steps.js +76 -0
  26. package/test/specs/features/step-definitions/m5-errors-false-silent-skip.steps.js +80 -0
  27. package/test/specs/features/step-definitions/m6-hooks-once-per-test.steps.js +90 -0
  28. package/test/specs/features/step-definitions/missing-feature-file.steps.js +23 -0
  29. package/test/specs/features/step-definitions/reuse-code.js +9 -4
  30. package/test/specs/features/step-definitions/reuse-definition.steps.js +9 -1
  31. package/test/specs/features/step-definitions/scenario-outline2.steps.js +28 -17
  32. package/test/specs/features/step-definitions/undefined-step.steps.js +39 -0
  33. package/test/specs/features/undefined-step.feature +4 -0
package/src/index.js CHANGED
@@ -1,12 +1,17 @@
1
- const stepsDefinition = {
1
+ const emptyStepsDefinition = () => ({
2
2
  given: {},
3
3
  when: {},
4
4
  then: {},
5
5
  and: {},
6
6
  but: {},
7
- before: null,
8
- after: null,
9
- };
7
+ before: [],
8
+ after: [],
9
+ });
10
+
11
+ // Module-level registry. Rebuilt from the empty shape above once a feature has been
12
+ // loaded, so a second Fusion() in the same module starts from a clean slate rather
13
+ // than inheriting the previous feature's step definitions and hooks.
14
+ let stepsDefinition = emptyStepsDefinition();
10
15
 
11
16
  const addDefinitionFunction = (
12
17
  definitionType,
@@ -14,21 +19,31 @@ const addDefinitionFunction = (
14
19
  fnForDefinition
15
20
  ) => {
16
21
  if (stepsDefinition[definitionType]) {
17
- if (regexpSentence.constructor === RegExp)
22
+ if (regexpSentence.constructor === RegExp) {
23
+ throwIfDuplicateMatcher(definitionType, regexpSentence.source);
18
24
  stepsDefinition[definitionType][regexpSentence.source] = {
19
25
  stepRegExp: regexpSentence,
20
26
  stepExpression: null,
21
27
  stepFn: fnForDefinition,
22
28
  };
23
- else if (typeof regexpSentence === "string")
29
+ } else if (typeof regexpSentence === "string") {
30
+ throwIfDuplicateMatcher(definitionType, regexpSentence);
24
31
  stepsDefinition[definitionType][regexpSentence] = {
25
32
  stepRegExp: null,
26
33
  stepExpression: regexpSentence,
27
34
  stepFn: fnForDefinition,
28
35
  };
36
+ }
29
37
  }
30
38
  };
31
39
 
40
+ const throwIfDuplicateMatcher = (definitionType, matcherKey) => {
41
+ if (stepsDefinition[definitionType][matcherKey])
42
+ throw new Error(
43
+ `Duplicate step definition: "${matcherKey}" is already registered for "${definitionType}"`
44
+ );
45
+ };
46
+
32
47
  const Given = (regexpSentenceOrChainedObject, fnForDefinition) => {
33
48
  return defineAndChain(
34
49
  "given",
@@ -73,90 +88,142 @@ const defineAndChain = (stepType, stepObjectOrSentence, fnForStep) => {
73
88
  };
74
89
 
75
90
  const Before = (fnDefinition) => {
76
- stepsDefinition.before = fnDefinition;
91
+ stepsDefinition.before.push(fnDefinition);
77
92
  };
78
93
  const After = (fnDefinition) => {
79
- stepsDefinition.after = fnDefinition;
94
+ stepsDefinition.after.push(fnDefinition);
80
95
  };
81
96
 
82
97
  const Fusion = (featureFileToLoad, optionsToPassToJestCucumber) => {
83
- const path = require("path");
84
- const callerSites = require("callsites");
85
- const callerSiteCaller = callerSites.default()[1].getFileName();
86
- const dirOfCaller = path.dirname(callerSiteCaller || "");
87
- const absoluteFeatureFilePath = path.resolve(dirOfCaller, featureFileToLoad);
88
-
89
- const jestCucumber = require("jest-cucumber");
90
- const feature = jestCucumber.loadFeature(
91
- absoluteFeatureFilePath,
92
- optionsToPassToJestCucumber
93
- );
98
+ try {
99
+ const path = require("path");
100
+ const callerSites = require("callsites");
101
+ // Resolve the feature path from the FIRST stack frame outside this package, so
102
+ // an in-package re-export/wrapper frame does not retarget it; guard a shallow
103
+ // stack (no external frame) so we never call getFileName() on undefined.
104
+ const externalFrame = callerSites
105
+ .default()
106
+ .find((currentFrame) => currentFrame.getFileName() !== __filename);
107
+ const callerSiteCaller = externalFrame ? externalFrame.getFileName() : "";
108
+ const dirOfCaller = path.dirname(callerSiteCaller || "");
109
+ const absoluteFeatureFilePath = path.resolve(
110
+ dirOfCaller,
111
+ featureFileToLoad
112
+ );
94
113
 
95
- jestCucumber.defineFeature(feature, (testFn) => {
96
- if (feature.scenarios.length > 0)
97
- matchJestTestSuiteWithCucumberFeature(
98
- feature.scenarios,
99
- beforeEach,
100
- afterEach,
101
- testFn
102
- );
114
+ const jestCucumber = require("jest-cucumber");
115
+ const feature = jestCucumber.loadFeature(
116
+ absoluteFeatureFilePath,
117
+ optionsToPassToJestCucumber
118
+ );
103
119
 
104
- if (feature.scenarioOutlines.length > 0)
105
- matchJestTestSuiteWithCucumberFeature(
106
- feature.scenarioOutlines,
107
- beforeEach,
108
- afterEach,
109
- testFn,
110
- true
111
- );
112
- });
120
+ // When jest-cucumber's own step-count validation is disabled ({ errors: false }),
121
+ // the wrapper must fail loudly on an unmatched step itself; otherwise stay silent
122
+ // so jest-cucumber's native validation remains the (transparent) source of truth.
123
+ const failOnUnmatchedStep = !!(
124
+ optionsToPassToJestCucumber &&
125
+ optionsToPassToJestCucumber.errors === false
126
+ );
127
+
128
+ // This feature binds the definitions and hooks registered for IT — captured before the
129
+ // registry is reset below, so the binding does not depend on when jest-cucumber invokes
130
+ // the callback.
131
+ const registryForThisFeature = stepsDefinition;
132
+
133
+ jestCucumber.defineFeature(feature, (testFn) => {
134
+ // jest-cucumber wraps the whole feature in ONE describe, so a hook registered here
135
+ // already runs around every test in it. Register each hook once per feature, never
136
+ // per scenario: N registrations would run every hook N times per test.
137
+ registerHooks(registryForThisFeature, beforeEach, afterEach);
138
+
139
+ if (feature.scenarios.length > 0)
140
+ matchJestTestSuiteWithCucumberFeature(
141
+ registryForThisFeature,
142
+ feature.scenarios,
143
+ testFn,
144
+ false,
145
+ failOnUnmatchedStep
146
+ );
147
+
148
+ if (feature.scenarioOutlines.length > 0)
149
+ matchJestTestSuiteWithCucumberFeature(
150
+ registryForThisFeature,
151
+ feature.scenarioOutlines,
152
+ testFn,
153
+ true,
154
+ failOnUnmatchedStep
155
+ );
156
+ });
157
+ } finally {
158
+ // Unconditional: Fusion() always leaves a clean slate — normal return OR throw. Rebinding
159
+ // the module-level registry (never mutating it in place) keeps the object captured above
160
+ // intact for the callback, while the next Fusion() starts empty and must re-register.
161
+ stepsDefinition = emptyStepsDefinition();
162
+ }
163
+ };
164
+
165
+ const registerHooks = (featureRegistry, beforeEachFn, afterEachFn) => {
166
+ featureRegistry.before.forEach((beforeHook) => beforeEachFn(beforeHook));
167
+ featureRegistry.after.forEach((afterHook) => afterEachFn(afterHook));
113
168
  };
114
169
 
115
170
  const matchJestTestSuiteWithCucumberFeature = (
171
+ featureRegistry,
116
172
  featureScenariosOrOutline,
117
- beforeEachFn,
118
- afterEachFn,
119
173
  testFn,
120
- isOutline
174
+ isOutline,
175
+ failOnUnmatchedStep
121
176
  ) => {
122
177
  featureScenariosOrOutline.forEach((currentScenarioOrOutline) => {
123
- if (stepsDefinition.before) beforeEachFn(stepsDefinition.before);
124
-
125
178
  matchJestTestWithCucumberScenario(
179
+ featureRegistry,
126
180
  currentScenarioOrOutline.title,
127
181
  currentScenarioOrOutline.steps,
128
182
  testFn,
129
- isOutline
183
+ isOutline,
184
+ failOnUnmatchedStep
130
185
  );
131
-
132
- if (stepsDefinition.after) afterEachFn(stepsDefinition.after);
133
186
  });
134
187
  };
135
188
 
136
189
  const matchJestTestWithCucumberScenario = (
190
+ featureRegistry,
137
191
  currentScenarioTitle,
138
192
  currentScenarioSteps,
139
193
  testFn,
140
- isOutline
194
+ isOutline,
195
+ failOnUnmatchedStep
141
196
  ) => {
142
197
  testFn(currentScenarioTitle, ({ given, when, then, and, but }) => {
143
198
  currentScenarioSteps.forEach((currentStep) => {
144
199
  matchJestDefinitionWithCucumberStep(
200
+ featureRegistry,
145
201
  { given, when, then, and, but },
146
202
  currentStep,
147
- isOutline
203
+ isOutline,
204
+ failOnUnmatchedStep
148
205
  );
149
206
  });
150
207
  });
151
208
  };
152
209
 
153
210
  const matchJestDefinitionWithCucumberStep = (
211
+ featureRegistry,
154
212
  verbFunction,
155
213
  currentStep,
156
- isOutline
214
+ isOutline,
215
+ failOnUnmatchedStep
157
216
  ) => {
158
- const foundMatchingStep = findMatchingStep(currentStep, isOutline);
159
- if (!foundMatchingStep) return;
217
+ const foundMatchingStep = findMatchingStep(
218
+ featureRegistry,
219
+ currentStep,
220
+ isOutline
221
+ );
222
+ if (!foundMatchingStep) {
223
+ if (failOnUnmatchedStep)
224
+ throw new Error(`No step definition matches: "${currentStep.stepText}"`);
225
+ return;
226
+ }
160
227
 
161
228
  // this will be the "given", "when", "then"...functions
162
229
  verbFunction[currentStep.keyword](
@@ -165,24 +232,34 @@ const matchJestDefinitionWithCucumberStep = (
165
232
  );
166
233
  };
167
234
 
168
- const findMatchingStep = (currentStep, isOutline) => {
235
+ const findMatchingStep = (featureRegistry, currentStep, isOutline) => {
169
236
  const scenarioType = currentStep.keyword;
170
237
  const scenarioSentence = currentStep.stepText;
171
- const foundStep = Object.keys(stepsDefinition[scenarioType]).find(
238
+ const matchingSteps = Object.keys(featureRegistry[scenarioType]).filter(
172
239
  (currentStepDefinitionFunction) => {
173
240
  return isFunctionForScenario(
174
241
  scenarioSentence,
175
- stepsDefinition[scenarioType][currentStepDefinitionFunction],
242
+ featureRegistry[scenarioType][currentStepDefinitionFunction],
176
243
  isOutline
177
244
  );
178
245
  }
179
246
  );
180
- if (!foundStep) return null;
247
+ if (matchingSteps.length === 0) return null;
248
+
249
+ if (matchingSteps.length > 1) {
250
+ const competingMatchers = matchingSteps
251
+ .map((matcherSource) => `"${matcherSource}"`)
252
+ .join(", ");
253
+ throw new Error(
254
+ `Ambiguous step definition: "${scenarioSentence}" matches ${matchingSteps.length} step definitions: ${competingMatchers}`
255
+ );
256
+ }
181
257
 
182
258
  return injectVariable(
259
+ featureRegistry,
183
260
  scenarioType,
184
261
  scenarioSentence,
185
- foundStep,
262
+ matchingSteps[0],
186
263
  currentStep.stepArgument
187
264
  );
188
265
  };
@@ -204,6 +281,22 @@ const isFunctionForScenario = (
204
281
  return scenarioSentence === stepDefinitionFunction.stepExpression;
205
282
  };
206
283
 
284
+ // An ESCAPED paren is literal text, never a group delimiter — the same knowledge holdsCapturingGroup
285
+ // (below) already encodes. Masking each `\(` / `\)` with an ordinary two-character body sequence keeps
286
+ // every index and length identical to the raw source, so the group locator can run over the mask and
287
+ // its result still addresses the RAW string.
288
+ const maskEscapedParens = (stepFunctionDef) =>
289
+ stepFunctionDef.replace(/\\[()]/g, "\\-");
290
+
291
+ // The scenario sentence carries a matcher's parens/anchors as LITERAL characters, so a position in the
292
+ // sentence is only comparable against the step function once its escapes are collapsed the same way.
293
+ const asScenarioText = (stepFunctionDef) =>
294
+ stepFunctionDef
295
+ .replace(/\\\(/g, "(")
296
+ .replace(/\\\)/g, ")")
297
+ .replace(/\\\^/g, "^")
298
+ .replace(/\\\$/g, "$");
299
+
207
300
  const isPotentialStepFunctionForScenario = (
208
301
  scenarioDefinition,
209
302
  regStepFunc
@@ -240,50 +333,56 @@ const isPotentialStepFunctionForScenario = (
240
333
  let idxCutScenarioPart =
241
334
  currentScenarioPart.index + currentScenarioPart[0].length;
242
335
 
243
- const regEscapedStepFunc = /\([a-zA-Z0!|,:?*+.^=${}><\\\-]+\)/g.exec(
244
- currentStepFuncLeft
245
- .replace(/\\\(/g, "(")
246
- .replace(/\\\)/g, ")")
247
- .replace(/\\\^/g, "^")
248
- .replace(/\\\$/g, "$")
249
- );
250
- const regStepFuncLeft = /\([a-zA-Z0!|,:?*+.^=${}><\\\-]+\)/g.exec(
251
- currentStepFuncLeft
336
+ // The character class spans the source of a capturing group. It must include the digits 0-9:
337
+ // spelled `0` alone it cannot span a group whose source holds a digit 1-9, which makes EVERY
338
+ // bounded quantifier — "(\d{4})" — invisible to this detector and therefore unbindable in an
339
+ // outline, while "(\d+)" (no digit in its source) binds. Braces are not the cause; the digits are.
340
+ //
341
+ // Locate the group over the MASKED source, so an escaped paren can never be mistaken for a group
342
+ // boundary: previously the locator ran twice — once on the raw source, once on a source whose
343
+ // `\(`/`\)` had been unescaped into BARE parens — and the two disagreed about where (and whether)
344
+ // a group was. For "(\w+\(\))" the raw pass matched "(\)" while the unescaped pass ("(\w+())")
345
+ // matched nothing at all, and a guard that tested the first while dereferencing the second threw
346
+ // on the null. One escape-aware pass gives one answer: the real group, in raw coordinates.
347
+ const groupInStepFunc = /\([a-zA-Z0-9!|,:?*+.^=${}><\\\-]+\)/g.exec(
348
+ maskEscapedParens(currentStepFuncLeft)
252
349
  );
350
+ // The scenario sentence spells the step function's prefix as literal text, so the two are only
351
+ // comparable in scenario-text coordinates — escapes collapsed, one character each.
352
+ const stepFuncPrefixAsScenarioText = groupInStepFunc
353
+ ? asScenarioText(currentStepFuncLeft.substring(0, groupInStepFunc.index))
354
+ : "";
253
355
 
254
356
  if (
255
- regStepFuncLeft &&
256
- regEscapedStepFunc.index == currentScenarioPart.index
357
+ groupInStepFunc &&
358
+ stepFuncPrefixAsScenarioText.length == currentScenarioPart.index
257
359
  ) {
258
360
  //if we have a regex inside our step function definition
259
361
  // and that regex is at the same position than our Outlined variable
260
362
  // we just need to check that the sentence match,
261
363
  // so we can "evaluate" the step function and remove the regex in it
262
364
  currentStepFuncLeft =
263
- regEscapedStepFunc.input.substring(0, regEscapedStepFunc.index) +
365
+ stepFuncPrefixAsScenarioText +
264
366
  currentStepFuncLeft.substring(
265
- regStepFuncLeft.index + regStepFuncLeft[0].length
367
+ groupInStepFunc.index + groupInStepFunc[0].length
266
368
  );
267
369
  } else if (
268
- regStepFuncLeft &&
269
- regStepFuncLeft.index < currentScenarioPart.index
370
+ groupInStepFunc &&
371
+ groupInStepFunc.index < currentScenarioPart.index
270
372
  ) {
271
373
  //if we have a regex inside our step function definition
272
374
  // but that regex is not at the same position than our outlined variable
273
375
  // we need to evaluate the regex against the scenario part
274
- const strRegexToEvaluate = regStepFuncLeft.input.substring(
376
+ const strRegexToEvaluate = currentStepFuncLeft.substring(
275
377
  0,
276
- regStepFuncLeft.index + regStepFuncLeft[0].length
378
+ groupInStepFunc.index + groupInStepFunc[0].length
277
379
  );
278
380
  const regexToEvaluate = new RegExp(strRegexToEvaluate);
279
381
  const regIntermediatePart = regexToEvaluate.exec(
280
382
  currentScenarioPart.input
281
383
  );
282
384
  if (regIntermediatePart) {
283
- fixedPart = regStepFuncLeft.input.substring(
284
- 0,
285
- regStepFuncLeft.index + regStepFuncLeft[0].length
286
- );
385
+ fixedPart = strRegexToEvaluate;
287
386
  idxCutScenarioPart = regIntermediatePart[0].length;
288
387
  }
289
388
  }
@@ -309,14 +408,27 @@ const isPotentialStepFunctionForScenario = (
309
408
  );
310
409
  };
311
410
 
411
+ // A leftover fragment holds a CAPTURING group — so it has to be evaluated as a regex, not compared
412
+ // as a literal. Escaped parens are stripped first (`\(n\)` is literal text, not a group) and `(?...)`
413
+ // is skipped (non-capturing), so this recognises exactly the real groups.
414
+ const holdsCapturingGroup = (stepFunctionDef) =>
415
+ /\((?!\?)[^()]*\)/.test(stepFunctionDef.replace(/\\[()]/g, ""));
416
+
312
417
  const evaluateStepFuncEndVsScenarioEnd = (
313
418
  stepFunctionDef,
314
419
  scenarioDefinition
315
420
  ) => {
421
+ // The leftover is regex SOURCE, so a capturing group in it must be evaluated as a regex. The old
422
+ // test only recognised a group holding one of [sSdDwWbB*], which made the group's SPELLING the
423
+ // discriminator rather than its presence: " lamp is (on|off)" fell through to a literal endsWith
424
+ // (always false — the scenario text reads " lamp is on") and never bound, while the identically
425
+ // shaped " lamp is (on|down)" took the regex branch and bound. Widening is strict: every fragment
426
+ // that already took the regex branch still does.
316
427
  if (
317
428
  /\(.*(\?\:)?[.\\]*[sSdDwWbB*][*?+]?.*\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.test(
318
429
  stepFunctionDef
319
- )
430
+ ) ||
431
+ holdsCapturingGroup(stepFunctionDef)
320
432
  ) {
321
433
  return new RegExp(stepFunctionDef).test(scenarioDefinition);
322
434
  }
@@ -325,12 +437,13 @@ const evaluateStepFuncEndVsScenarioEnd = (
325
437
  };
326
438
 
327
439
  const injectVariable = (
440
+ featureRegistry,
328
441
  scenarioType,
329
442
  scenarioSentence,
330
443
  stepFunctionDefinition,
331
444
  stepArgs
332
445
  ) => {
333
- const stepObject = stepsDefinition[scenarioType][stepFunctionDefinition];
446
+ const stepObject = featureRegistry[scenarioType][stepFunctionDefinition];
334
447
 
335
448
  if (!stepObject.stepRegExp)
336
449
  return {
@@ -360,7 +473,12 @@ const injectVariable = (
360
473
  if (groupIndex > 0) dynamicMatchThatAreVariables.push(match);
361
474
  });
362
475
 
363
- if (Array.isArray(stepArgs) && stepArgs.length > 0) {
476
+ // Forward the step's Gherkin argument on PRESENCE, never on its type — the same test jest-cucumber
477
+ // itself applies before handing an argument to a step (feature-definition-creation.js:129-130). A
478
+ // type test drops half the shapes: Gherkin parses a data table to an array but a docstring to a
479
+ // string, and an empty docstring to "" — which a `.length` test would drop too. Absent, the argument
480
+ // is null, so this is the whole distinction that matters.
481
+ if (stepArgs != null) {
364
482
  dynamicMatchThatAreVariables.push(stepArgs);
365
483
  }
366
484
 
@@ -370,29 +488,6 @@ const injectVariable = (
370
488
  };
371
489
  };
372
490
 
373
- // const path = require( 'path' )
374
- //
375
- // function walkthroughDirectory( directoryName, bRecursive, fnDoForEachFile ) {
376
- // const filesystem = require( 'fs' )
377
- // let filenamesOrDirectorynames = filesystem.readdirSync( directoryName )
378
- //
379
- // filenamesOrDirectorynames.forEach( ( currentFilenameOrDirectoryname ) => {
380
- // let currentFilepath = path.join( directoryName, currentFilenameOrDirectoryname )
381
- // let currentFileStats = filesystem.statSync( currentFilepath )
382
- // if( bRecursive && currentFileStats.isDirectory() )
383
- // walkthroughDirectory( currentFilepath, activeLoadings, bRecursive, fnOnLoading )
384
- //
385
- // else
386
- // fnDoForEachFile( currentFilepath )
387
- // } )
388
- // }
389
-
390
- // function FusionAll( dirFeatureFiles, bRecursive ) {
391
- // const dirFinalDirectory = ( !dirFeatureFiles ? '<rootDir>/test/feature' : dirFeatureFiles )
392
- //
393
- // walkthroughDirectory( dirFinalDirectory, bRecursive, Fusion )
394
- // }
395
-
396
491
  module.exports.Before = Before;
397
492
  module.exports.After = After;
398
493
  module.exports.Given = Given;
@@ -401,4 +496,3 @@ module.exports.Then = Then;
401
496
  module.exports.And = And;
402
497
  module.exports.But = But;
403
498
  module.exports.Fusion = Fusion;
404
- // module.exports.FusionAll = FusionAll
@@ -0,0 +1,44 @@
1
+ Feature: Step arguments reaching a step definition
2
+
3
+ Scenario: A step matched on its captures receives the incident notes attached to it
4
+ When I file an incident for rocket "Falcon" with the following notes:
5
+ """
6
+ Engine 3 shut down at T+42 seconds
7
+ """
8
+ Then the incident step should have received the rocket "Falcon" and then the notes "Engine 3 shut down at T+42 seconds"
9
+
10
+ Scenario: A step matched on its exact wording receives the incident notes attached to it
11
+ When I file an incident for the flagship rocket with the following notes:
12
+ """
13
+ Engine 3 shut down at T+42 seconds
14
+ """
15
+ Then the flagship incident step should have received only the notes "Engine 3 shut down at T+42 seconds"
16
+
17
+ Scenario: A step matched on its captures receives the crew table attached to it
18
+ When I assign 2 crew to rocket "Falcon":
19
+ | Name | Role |
20
+ | Ada | pilot |
21
+ | Grace | engineer |
22
+ Then the crew step should have received the count "2" and the rocket "Falcon" and then the crew table
23
+
24
+ Scenario: A step matched on its captures with nothing attached does not receive a phantom extra argument
25
+ When I ground rocket "Falcon"
26
+ Then the grounding step should have received only the rocket "Falcon"
27
+
28
+ Scenario: A step matched on its captures receives empty incident notes attached to it
29
+ When I file an incident for rocket "Falcon" with the following notes:
30
+ """
31
+ """
32
+ Then the incident step should have received the rocket "Falcon" and then empty notes
33
+
34
+ Scenario Outline: A step of an outline receives the incident notes attached to it, written for rocket <rocket>
35
+ When I file an incident for rocket "<rocket>" with the following notes:
36
+ """
37
+ <rocket> shut down engine 3 at T+42 seconds
38
+ """
39
+ Then the outline incident step should have received the rocket "<rocket>" and then its own notes
40
+
41
+ Examples:
42
+ | rocket |
43
+ | Falcon |
44
+ | Vega |
@@ -0,0 +1,21 @@
1
+ Feature: Hooks run once per test
2
+
3
+ Every Before and After hook runs exactly once around each test, however many
4
+ scenarios and outlines the feature holds.
5
+
6
+ Scenario: first scenario
7
+ Given the hooks have run once for this test
8
+
9
+ Scenario: second scenario
10
+ Given the hooks have run once for this test
11
+
12
+ Scenario: third scenario
13
+ Given the hooks have run once for this test
14
+
15
+ Scenario Outline: outline row <row>
16
+ Given the hooks have run once for this test
17
+
18
+ Examples:
19
+ | row |
20
+ | one |
21
+ | two |
@@ -0,0 +1,12 @@
1
+ Feature: Hooks run once per test in an outline-only feature
2
+
3
+ A feature with no plain scenario must still wire its hooks, once per example row.
4
+
5
+ Scenario Outline: outline-only row <row>
6
+ Given the outline-only hooks have run once for this row
7
+
8
+ Examples:
9
+ | row |
10
+ | one |
11
+ | two |
12
+ | three |
@@ -1,10 +1,10 @@
1
1
  Feature: Scenario Outline verification
2
2
 
3
3
  Scenario Outline: Simple Scenario Outline. Buy item: <Item>
4
- This scenario use Before fusion hook (see scenario-outline2.steps.js)
5
- This hook is remove all items from list of onlineSales if number of items >=2.
6
- This scenario also demonstrate case dependancy in scenario outline:
7
- if you are using some global variable it is not clear between scenarious.
4
+ This scenario uses the Before fusion hook (see scenario-outline2.steps.js).
5
+ The hook empties the shop before every scenario and every example row, so each
6
+ row stands on its own: the Given establishes the items already for sale, and
7
+ nothing carries over from the row before it.
8
8
 
9
9
  Given I have <nItems> items for sale
10
10
  When I bought "<Item>"
@@ -46,6 +46,8 @@ Feature: Scenario Outline verification
46
46
  | Sabaton. Great War |
47
47
  | Cucumber for dummies |
48
48
 
49
+ Then I have 2 items for sale
50
+
49
51
  Scenario Outline: Using examples in sentance and in table
50
52
 
51
53
  Given I have 0 items for sale
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Regression test — H1: regex first-match shadowing.
3
+ * RCA: docs/feature/review-hardening/discuss/rca.md (H1, HIGH — only silent-wrong-result bug).
4
+ *
5
+ * findMatchingStep (src/index.js:168-188) uses Object.keys(...).find(...) → the FIRST
6
+ * registered definition that matches a step wins. Register a broad regex before a specific
7
+ * one for the same keyword and the broad (wrong) handler is bound silently with the wrong
8
+ * captured args; jest-cucumber's re-validation is still satisfied, so the scenario passes.
9
+ * jest-cucumber's own automatic binding raises an ambiguity error here — this wrapper
10
+ * suppresses it.
11
+ *
12
+ * LOCKED FIX CONTRACT (DELIVER implements; this test asserts it): when MORE THAN ONE step
13
+ * definition matches a step, Fusion must THROW an ambiguity error (naming the step text /
14
+ * competing matchers) rather than silently taking the first.
15
+ *
16
+ * MECHANISM / caveat: the wrapper's step matching (findMatchingStep) runs inside the scenario
17
+ * callback that Fusion hands to jest-cucumber's defineFeature — it is NOT reachable
18
+ * synchronously through Fusion's return value, and an ambiguous feature can never resolve to
19
+ * a green jest-cucumber scenario (post-fix it throws by design). So we drive the REAL wrapper
20
+ * (Fusion + real src/index.js) and substitute ONLY the external collaborator jest-cucumber
21
+ * (the non-deterministic test-runner port) with a double that captures the scenario callback,
22
+ * then invoke that callback synchronously and assert it throws. This is the "drive the SUT
23
+ * through its public surface, fake only external ports" mandate applied to this JS wrapper.
24
+ *
25
+ * CURRENT STATUS: RED — findMatchingStep does not throw on ambiguity, so the captured callback
26
+ * completes without error and toThrow fails ("received function did not throw").
27
+ */
28
+
29
+ const mockState = { capturedCallback: null, feature: null };
30
+
31
+ jest.mock("jest-cucumber", () => ({
32
+ loadFeature: jest.fn(() => mockState.feature),
33
+ defineFeature: jest.fn((feature, callback) => {
34
+ mockState.capturedCallback = callback;
35
+ }),
36
+ }));
37
+
38
+ const { Given, Fusion } = require("../../../../src");
39
+
40
+ // Broad definition registered FIRST — this is the one that shadows.
41
+ Given(/^I have (.*)$/, () => {});
42
+ // Specific definition registered SECOND — the handler the author actually intends.
43
+ Given(/^I have (\d+) apples$/, () => {});
44
+
45
+ describe("H1 — ambiguous step definitions", () => {
46
+ test("a step matching two definitions is rejected as ambiguous, not silently shadowed", () => {
47
+ // The feature file is never read (loadFeature is faked); this is the parsed shape the
48
+ // wrapper consumes. The step "I have 3 apples" matches BOTH definitions above.
49
+ mockState.feature = {
50
+ title: "Ambiguous step matching",
51
+ scenarios: [
52
+ {
53
+ title: "counting apples",
54
+ steps: [
55
+ {
56
+ keyword: "given",
57
+ stepText: "I have 3 apples",
58
+ stepArgument: undefined,
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ scenarioOutlines: [],
64
+ };
65
+
66
+ // Drive the real wrapper; the faked jest-cucumber captures the scenario callback.
67
+ Fusion("../ambiguous-step-shadowing.feature");
68
+
69
+ const runScenarioMatching = () => {
70
+ // capturedCallback = (testFn) =>
71
+ // matchJestTestSuiteWithCucumberFeature(registry, scenarios, testFn, ...)
72
+ // (hooks are wired once per feature by registerHooks, outside this call: M6)
73
+ mockState.capturedCallback((_title, scenarioBody) => {
74
+ // scenarioBody performs the wrapper's step matching (findMatchingStep) synchronously.
75
+ scenarioBody({
76
+ given: () => {},
77
+ when: () => {},
78
+ then: () => {},
79
+ and: () => {},
80
+ but: () => {},
81
+ });
82
+ });
83
+ };
84
+
85
+ // Locked contract: ambiguity must throw. Pre-fix this does NOT throw (silent first-match).
86
+ // Match the full contract signature (not a loose /ambig/i) so an incidental "ambiguous"
87
+ // in some unrelated error can't satisfy the assertion.
88
+ expect(runScenarioMatching).toThrow(
89
+ /Ambiguous step definition.*matches \d+ step definitions/i
90
+ );
91
+ });
92
+ });