@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.
- package/README.md +9 -10
- package/codecov +0 -0
- package/codecov.SHA256SUM +1 -1
- package/codecov.SHA256SUM.sig +13 -13
- package/docs/product/expectations/fix-l2-outline-regex/outline-regex-binds.md +148 -0
- package/docs/product/expectations/fix-l4-escaped-parens-outline/escaped-parens-bind-in-outlines.md +106 -0
- package/docs/product/expectations/fix-m3-singleton-reset/clean-slate-per-feature.md +133 -0
- package/package.json +11 -5
- package/src/index.d.ts +15 -5
- package/src/index.js +196 -102
- package/test/specs/features/l3-step-argument-delivery.feature +44 -0
- package/test/specs/features/m6-hooks-once-per-test.feature +21 -0
- package/test/specs/features/m6-hooks-outline-only.feature +12 -0
- package/test/specs/features/scenario-outline2.feature +6 -4
- package/test/specs/features/step-definitions/ambiguous-step-shadowing.steps.js +92 -0
- package/test/specs/features/step-definitions/basic-scenarios.steps.js +19 -2
- package/test/specs/features/step-definitions/fuzz-properties.steps.js +170 -0
- package/test/specs/features/step-definitions/hook-error.steps.js +59 -0
- package/test/specs/features/step-definitions/l2-outline-edge-cases.steps.js +327 -0
- package/test/specs/features/step-definitions/l3-step-argument-delivery.steps.js +191 -0
- package/test/specs/features/step-definitions/l4-escaped-parens-outline.steps.js +256 -0
- package/test/specs/features/step-definitions/m1-before-hooks-clobber.steps.js +64 -0
- package/test/specs/features/step-definitions/m2-duplicate-matcher.steps.js +60 -0
- package/test/specs/features/step-definitions/m3-singleton-reset.steps.js +275 -0
- package/test/specs/features/step-definitions/m4-callsite-resolution.steps.js +76 -0
- package/test/specs/features/step-definitions/m5-errors-false-silent-skip.steps.js +80 -0
- package/test/specs/features/step-definitions/m6-hooks-once-per-test.steps.js +90 -0
- package/test/specs/features/step-definitions/missing-feature-file.steps.js +23 -0
- package/test/specs/features/step-definitions/reuse-code.js +9 -4
- package/test/specs/features/step-definitions/reuse-definition.steps.js +9 -1
- package/test/specs/features/step-definitions/scenario-outline2.steps.js +28 -17
- package/test/specs/features/step-definitions/undefined-step.steps.js +39 -0
- package/test/specs/features/undefined-step.feature +4 -0
package/src/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
const
|
|
1
|
+
const emptyStepsDefinition = () => ({
|
|
2
2
|
given: {},
|
|
3
3
|
when: {},
|
|
4
4
|
then: {},
|
|
5
5
|
and: {},
|
|
6
6
|
but: {},
|
|
7
|
-
before:
|
|
8
|
-
after:
|
|
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
|
|
91
|
+
stepsDefinition.before.push(fnDefinition);
|
|
77
92
|
};
|
|
78
93
|
const After = (fnDefinition) => {
|
|
79
|
-
stepsDefinition.after
|
|
94
|
+
stepsDefinition.after.push(fnDefinition);
|
|
80
95
|
};
|
|
81
96
|
|
|
82
97
|
const Fusion = (featureFileToLoad, optionsToPassToJestCucumber) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
afterEach,
|
|
101
|
-
testFn
|
|
102
|
-
);
|
|
114
|
+
const jestCucumber = require("jest-cucumber");
|
|
115
|
+
const feature = jestCucumber.loadFeature(
|
|
116
|
+
absoluteFeatureFilePath,
|
|
117
|
+
optionsToPassToJestCucumber
|
|
118
|
+
);
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
159
|
-
|
|
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
|
|
238
|
+
const matchingSteps = Object.keys(featureRegistry[scenarioType]).filter(
|
|
172
239
|
(currentStepDefinitionFunction) => {
|
|
173
240
|
return isFunctionForScenario(
|
|
174
241
|
scenarioSentence,
|
|
175
|
-
|
|
242
|
+
featureRegistry[scenarioType][currentStepDefinitionFunction],
|
|
176
243
|
isOutline
|
|
177
244
|
);
|
|
178
245
|
}
|
|
179
246
|
);
|
|
180
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
365
|
+
stepFuncPrefixAsScenarioText +
|
|
264
366
|
currentStepFuncLeft.substring(
|
|
265
|
-
|
|
367
|
+
groupInStepFunc.index + groupInStepFunc[0].length
|
|
266
368
|
);
|
|
267
369
|
} else if (
|
|
268
|
-
|
|
269
|
-
|
|
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 =
|
|
376
|
+
const strRegexToEvaluate = currentStepFuncLeft.substring(
|
|
275
377
|
0,
|
|
276
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
});
|