@g_package/jest-cucumber-fusion 2.0.0 → 3.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 +444 -38
- package/dist/THIRD_PARTY_LICENSES.txt +91 -0
- package/dist/index.cjs +9917 -0
- package/dist/index.d.cts +145 -0
- package/package.json +60 -10
- package/scripts/prepare-hooks.js +23 -0
- package/src/code-suggestion.js +240 -0
- package/src/configuration.js +147 -0
- package/src/feature-source.js +322 -0
- package/src/index.d.ts +123 -13
- package/src/index.js +153 -420
- package/src/keywords.js +56 -0
- package/src/scenario-name.js +128 -0
- package/src/shared-state.js +36 -0
- package/src/step-argument.js +56 -0
- package/src/step-matching.js +89 -0
- package/src/tag-filter.js +90 -0
- package/src/test-registration.js +178 -0
- package/src/value-description.js +26 -0
- package/.prettierignore +0 -16
- package/.prettierrc.json +0 -0
- package/codecov +0 -0
- package/codecov.SHA256SUM +0 -1
- package/codecov.SHA256SUM.sig +0 -16
- package/docs/AdditionalConfiguration.md +0 -155
- package/docs/GherkinTables.md +0 -61
- package/docs/Language.md +0 -76
- package/docs/ReusingStepDefinitions.md +0 -110
- package/docs/RunningTheExamples.md +0 -16
- package/docs/ScenarioOutlines.md +0 -43
- package/docs/StepDefinitionArguments.md +0 -37
- package/docs/product/expectations/fix-l2-outline-regex/outline-regex-binds.md +0 -148
- package/docs/product/expectations/fix-l4-escaped-parens-outline/escaped-parens-bind-in-outlines.md +0 -106
- package/docs/product/expectations/fix-m3-singleton-reset/clean-slate-per-feature.md +0 -133
- package/test/specs/features/basic-scenarios.feature +0 -28
- package/test/specs/features/l3-step-argument-delivery.feature +0 -44
- package/test/specs/features/language.feature +0 -41
- package/test/specs/features/m6-hooks-once-per-test.feature +0 -21
- package/test/specs/features/m6-hooks-outline-only.feature +0 -12
- package/test/specs/features/reuse-definition.feature +0 -13
- package/test/specs/features/scenario-outline2.feature +0 -73
- package/test/specs/features/scenario-outlines.feature +0 -88
- package/test/specs/features/step-definitions/ambiguous-step-shadowing.steps.js +0 -92
- package/test/specs/features/step-definitions/basic-scenarios.steps.js +0 -62
- package/test/specs/features/step-definitions/fuzz-properties.steps.js +0 -170
- package/test/specs/features/step-definitions/hook-error.steps.js +0 -59
- package/test/specs/features/step-definitions/l2-outline-edge-cases.steps.js +0 -327
- package/test/specs/features/step-definitions/l3-step-argument-delivery.steps.js +0 -191
- package/test/specs/features/step-definitions/l4-escaped-parens-outline.steps.js +0 -256
- package/test/specs/features/step-definitions/language.steps.js +0 -86
- package/test/specs/features/step-definitions/m1-before-hooks-clobber.steps.js +0 -64
- package/test/specs/features/step-definitions/m2-duplicate-matcher.steps.js +0 -60
- package/test/specs/features/step-definitions/m3-singleton-reset.steps.js +0 -275
- package/test/specs/features/step-definitions/m4-callsite-resolution.steps.js +0 -76
- package/test/specs/features/step-definitions/m5-errors-false-silent-skip.steps.js +0 -80
- package/test/specs/features/step-definitions/m6-hooks-once-per-test.steps.js +0 -90
- package/test/specs/features/step-definitions/missing-feature-file.steps.js +0 -23
- package/test/specs/features/step-definitions/reuse-code.js +0 -22
- package/test/specs/features/step-definitions/reuse-definition.steps.js +0 -30
- package/test/specs/features/step-definitions/scenario-outline2.steps.js +0 -57
- package/test/specs/features/step-definitions/scenario-outlines.steps.js +0 -110
- package/test/specs/features/step-definitions/undefined-step.steps.js +0 -39
- package/test/specs/features/step-definitions/using-dynamic-values.steps.js +0 -70
- package/test/specs/features/step-definitions/using-gherkin-tables.steps.js +0 -42
- package/test/specs/features/undefined-step.feature +0 -4
- package/test/specs/features/using-dynamic-values.feature +0 -34
- package/test/specs/features/using-gherkin-tables.feature +0 -16
- package/test/src/bank-account.js +0 -17
- package/test/src/online-sales.js +0 -33
- package/test/src/rocket.js +0 -13
- package/test/src/todo-list.js +0 -20
package/src/index.js
CHANGED
|
@@ -1,34 +1,114 @@
|
|
|
1
|
+
// The public surface: the step verbs, the hooks, the registry they write into, and Fusion.
|
|
2
|
+
//
|
|
3
|
+
// Everything Fusion does beyond holding the registry is delegated: the option merge to
|
|
4
|
+
// src/configuration.js, the outside world (the feature file, the parser, the caller stack) to
|
|
5
|
+
// src/feature-source.js, and the Jest runner to src/test-registration.js. Both ports are
|
|
6
|
+
// required by module path and are never exported, so they stay internal.
|
|
7
|
+
import { inspect } from "util";
|
|
8
|
+
import { mergeFusionOptions, setFusionConfiguration } from "./configuration.js";
|
|
9
|
+
import * as featureSource from "./feature-source.js";
|
|
10
|
+
import * as testRegistration from "./test-registration.js";
|
|
11
|
+
import { shared, replaceShared } from "./shared-state.js";
|
|
12
|
+
|
|
13
|
+
// Each bucket is keyed by step text or regex source, so it has no prototype: a step whose text
|
|
14
|
+
// is `constructor` or `toString` would otherwise find the inherited property and be refused as
|
|
15
|
+
// a duplicate on its first registration (fresh fuzz of PR #16, 2026-10-10, B2).
|
|
16
|
+
const emptyBucket = () => Object.create(null);
|
|
1
17
|
const emptyStepsDefinition = () => ({
|
|
2
|
-
given:
|
|
3
|
-
when:
|
|
4
|
-
then:
|
|
5
|
-
and:
|
|
6
|
-
but:
|
|
18
|
+
given: emptyBucket(),
|
|
19
|
+
when: emptyBucket(),
|
|
20
|
+
then: emptyBucket(),
|
|
21
|
+
and: emptyBucket(),
|
|
22
|
+
but: emptyBucket(),
|
|
7
23
|
before: [],
|
|
8
24
|
after: [],
|
|
9
25
|
});
|
|
10
26
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
27
|
+
// The registry the verbs and hooks write into. It lives in src/shared-state.js, not in a module
|
|
28
|
+
// variable, so that a step file using one copy of the dual package (import) and a shared step
|
|
29
|
+
// library using the other (require) register into the same one. Rebuilt from the empty shape
|
|
30
|
+
// above once a feature has been loaded, so a second Fusion() starts from a clean slate rather
|
|
13
31
|
// than inheriting the previous feature's step definitions and hooks.
|
|
14
|
-
|
|
32
|
+
const stepsDefinition = () => shared("steps", emptyStepsDefinition);
|
|
33
|
+
|
|
34
|
+
const isRegExp = (candidate) =>
|
|
35
|
+
Object.prototype.toString.call(candidate) === "[object RegExp]";
|
|
36
|
+
|
|
37
|
+
const isStepMatcher = (candidate) =>
|
|
38
|
+
typeof candidate === "string" || isRegExp(candidate);
|
|
39
|
+
|
|
40
|
+
const verbNamed = (definitionType) =>
|
|
41
|
+
definitionType.charAt(0).toUpperCase() + definitionType.slice(1);
|
|
42
|
+
|
|
43
|
+
const whatTheMatcherWas = (candidate) =>
|
|
44
|
+
candidate === undefined || candidate === null
|
|
45
|
+
? String(candidate)
|
|
46
|
+
: `${typeof candidate} ${inspect(candidate)}`;
|
|
47
|
+
|
|
48
|
+
// Refused at the call, like every other misuse. Until 3.0.0 a matcher of any other type was
|
|
49
|
+
// silently ignored, so the scenario failed later as an unbound step, far from the mistake.
|
|
50
|
+
const refuseUnsupportedMatcher = (definitionType, candidate) =>
|
|
51
|
+
new Error(
|
|
52
|
+
`Unsupported step matcher: ${verbNamed(
|
|
53
|
+
definitionType,
|
|
54
|
+
)} was given ${whatTheMatcherWas(candidate)}.\n\n` +
|
|
55
|
+
`WHY: Fusion binds a step definition to the feature's steps by its matcher, and only\n` +
|
|
56
|
+
` a string or a regular expression can match a step's text. Any other matcher\n` +
|
|
57
|
+
` binds nothing, and the step would then fail as unbound, far from this call.\n` +
|
|
58
|
+
`HOW: pass the step's text, ${verbNamed(
|
|
59
|
+
definitionType,
|
|
60
|
+
)}("the shop is open", fn), or a regular\n` +
|
|
61
|
+
` expression, ${verbNamed(
|
|
62
|
+
definitionType,
|
|
63
|
+
)}(/^(\\d+) items? in the basket$/, fn).`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Refused at the call too. Accepted, a definition with no function failed only when its step
|
|
67
|
+
// ran, as "step.bound.stepFn is not a function" (fresh fuzz of PR #16, 2026-10-10).
|
|
68
|
+
const refuseMissingStepFunction = (
|
|
69
|
+
definitionType,
|
|
70
|
+
matcher,
|
|
71
|
+
fnForDefinition,
|
|
72
|
+
) => {
|
|
73
|
+
const call = `${verbNamed(definitionType)}(${
|
|
74
|
+
typeof matcher === "string" ? JSON.stringify(matcher) : String(matcher)
|
|
75
|
+
})`;
|
|
76
|
+
return new Error(
|
|
77
|
+
`Missing step function: ${call} was given ${whatTheMatcherWas(
|
|
78
|
+
fnForDefinition,
|
|
79
|
+
)}.\n\n` +
|
|
80
|
+
`WHY: the function is what runs when a step matches, so a definition without one\n` +
|
|
81
|
+
` would fail only when its step ran, far from this call.\n` +
|
|
82
|
+
`HOW: pass the step's code as the second argument, for example\n` +
|
|
83
|
+
` ${call.slice(0, -1)}, () => { ... }).`,
|
|
84
|
+
);
|
|
85
|
+
};
|
|
15
86
|
|
|
16
87
|
const addDefinitionFunction = (
|
|
17
88
|
definitionType,
|
|
18
89
|
regexpSentence,
|
|
19
|
-
fnForDefinition
|
|
90
|
+
fnForDefinition,
|
|
20
91
|
) => {
|
|
21
|
-
if (
|
|
22
|
-
|
|
92
|
+
if (!isStepMatcher(regexpSentence))
|
|
93
|
+
throw refuseUnsupportedMatcher(definitionType, regexpSentence);
|
|
94
|
+
if (typeof fnForDefinition !== "function")
|
|
95
|
+
throw refuseMissingStepFunction(
|
|
96
|
+
definitionType,
|
|
97
|
+
regexpSentence,
|
|
98
|
+
fnForDefinition,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (stepsDefinition()[definitionType]) {
|
|
102
|
+
if (isRegExp(regexpSentence)) {
|
|
23
103
|
throwIfDuplicateMatcher(definitionType, regexpSentence.source);
|
|
24
|
-
stepsDefinition[definitionType][regexpSentence.source] = {
|
|
104
|
+
stepsDefinition()[definitionType][regexpSentence.source] = {
|
|
25
105
|
stepRegExp: regexpSentence,
|
|
26
106
|
stepExpression: null,
|
|
27
107
|
stepFn: fnForDefinition,
|
|
28
108
|
};
|
|
29
|
-
} else
|
|
109
|
+
} else {
|
|
30
110
|
throwIfDuplicateMatcher(definitionType, regexpSentence);
|
|
31
|
-
stepsDefinition[definitionType][regexpSentence] = {
|
|
111
|
+
stepsDefinition()[definitionType][regexpSentence] = {
|
|
32
112
|
stepRegExp: null,
|
|
33
113
|
stepExpression: regexpSentence,
|
|
34
114
|
stepFn: fnForDefinition,
|
|
@@ -38,9 +118,9 @@ const addDefinitionFunction = (
|
|
|
38
118
|
};
|
|
39
119
|
|
|
40
120
|
const throwIfDuplicateMatcher = (definitionType, matcherKey) => {
|
|
41
|
-
if (stepsDefinition[definitionType][matcherKey])
|
|
121
|
+
if (stepsDefinition()[definitionType][matcherKey])
|
|
42
122
|
throw new Error(
|
|
43
|
-
`Duplicate step definition: "${matcherKey}" is already registered for "${definitionType}"
|
|
123
|
+
`Duplicate step definition: "${matcherKey}" is already registered for "${definitionType}"`,
|
|
44
124
|
);
|
|
45
125
|
};
|
|
46
126
|
|
|
@@ -48,7 +128,7 @@ const Given = (regexpSentenceOrChainedObject, fnForDefinition) => {
|
|
|
48
128
|
return defineAndChain(
|
|
49
129
|
"given",
|
|
50
130
|
regexpSentenceOrChainedObject,
|
|
51
|
-
fnForDefinition
|
|
131
|
+
fnForDefinition,
|
|
52
132
|
);
|
|
53
133
|
};
|
|
54
134
|
const When = (regexpSentenceOrChainedObject, fnForDefinition) => {
|
|
@@ -65,18 +145,21 @@ const But = (regexpSentenceOrChainedObject, fnForDefinition) => {
|
|
|
65
145
|
return defineAndChain("but", regexpSentenceOrChainedObject, fnForDefinition);
|
|
66
146
|
};
|
|
67
147
|
|
|
148
|
+
// The chained form: a StepChain handed back by an earlier verb, re-registered under a second
|
|
149
|
+
// keyword. A single argument carrying a stepSentence is one of those; a matcher is not, and
|
|
150
|
+
// the RegExp clause keeps a regex out even though a regex carries no stepSentence either.
|
|
151
|
+
const isChainedStepObject = (candidate, fnForStep) =>
|
|
152
|
+
!fnForStep &&
|
|
153
|
+
candidate instanceof Object &&
|
|
154
|
+
Object.prototype.toString.call(candidate) !== "[object RegExp]" &&
|
|
155
|
+
candidate.stepSentence;
|
|
156
|
+
|
|
68
157
|
const defineAndChain = (stepType, stepObjectOrSentence, fnForStep) => {
|
|
69
|
-
if (
|
|
70
|
-
!fnForStep &&
|
|
71
|
-
stepObjectOrSentence instanceof Object &&
|
|
72
|
-
Object.prototype.toString.call(stepObjectOrSentence) !==
|
|
73
|
-
"[object RegExp]" &&
|
|
74
|
-
stepObjectOrSentence.stepSentence
|
|
75
|
-
) {
|
|
158
|
+
if (isChainedStepObject(stepObjectOrSentence, fnForStep)) {
|
|
76
159
|
addDefinitionFunction(
|
|
77
160
|
stepType,
|
|
78
161
|
stepObjectOrSentence.stepSentence,
|
|
79
|
-
stepObjectOrSentence.stepFnDefinition
|
|
162
|
+
stepObjectOrSentence.stepFnDefinition,
|
|
80
163
|
);
|
|
81
164
|
|
|
82
165
|
return stepObjectOrSentence;
|
|
@@ -88,411 +171,61 @@ const defineAndChain = (stepType, stepObjectOrSentence, fnForStep) => {
|
|
|
88
171
|
};
|
|
89
172
|
|
|
90
173
|
const Before = (fnDefinition) => {
|
|
91
|
-
stepsDefinition.before.push(fnDefinition);
|
|
174
|
+
stepsDefinition().before.push(fnDefinition);
|
|
92
175
|
};
|
|
93
176
|
const After = (fnDefinition) => {
|
|
94
|
-
stepsDefinition.after.push(fnDefinition);
|
|
95
|
-
};
|
|
177
|
+
stepsDefinition().after.push(fnDefinition);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Fusion() with no path, or a non-string one, reached Node's path module and failed with its
|
|
181
|
+
// raw `The "paths[1]" argument must be of type string` (fresh fuzz of PR #16, 2026-10-10).
|
|
182
|
+
const refuseFeaturePathNotAString = (featureFileToLoad) =>
|
|
183
|
+
new Error(
|
|
184
|
+
`Fusion needs the feature file's path as a string, but was given ${whatTheMatcherWas(
|
|
185
|
+
featureFileToLoad,
|
|
186
|
+
)}.\n\n` +
|
|
187
|
+
`HOW: pass the feature file's path, relative to this steps file, for example\n` +
|
|
188
|
+
` Fusion("rocket-launching.feature"). A file: URL needs url.fileURLToPath first.`,
|
|
189
|
+
);
|
|
96
190
|
|
|
97
|
-
const Fusion = (featureFileToLoad,
|
|
191
|
+
const Fusion = (featureFileToLoad, optionsForThisFeature) => {
|
|
98
192
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
);
|
|
193
|
+
if (typeof featureFileToLoad !== "string")
|
|
194
|
+
throw refuseFeaturePathNotAString(featureFileToLoad);
|
|
195
|
+
const absoluteFeatureFilePath =
|
|
196
|
+
featureSource.resolveFeaturePath(featureFileToLoad);
|
|
197
|
+
const effectiveOptions = mergeFusionOptions(optionsForThisFeature);
|
|
113
198
|
|
|
114
|
-
const
|
|
115
|
-
const feature = jestCucumber.loadFeature(
|
|
199
|
+
const loadedFeature = featureSource.loadFeature(
|
|
116
200
|
absoluteFeatureFilePath,
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
|
|
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));
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const matchJestTestSuiteWithCucumberFeature = (
|
|
171
|
-
featureRegistry,
|
|
172
|
-
featureScenariosOrOutline,
|
|
173
|
-
testFn,
|
|
174
|
-
isOutline,
|
|
175
|
-
failOnUnmatchedStep
|
|
176
|
-
) => {
|
|
177
|
-
featureScenariosOrOutline.forEach((currentScenarioOrOutline) => {
|
|
178
|
-
matchJestTestWithCucumberScenario(
|
|
179
|
-
featureRegistry,
|
|
180
|
-
currentScenarioOrOutline.title,
|
|
181
|
-
currentScenarioOrOutline.steps,
|
|
182
|
-
testFn,
|
|
183
|
-
isOutline,
|
|
184
|
-
failOnUnmatchedStep
|
|
185
|
-
);
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const matchJestTestWithCucumberScenario = (
|
|
190
|
-
featureRegistry,
|
|
191
|
-
currentScenarioTitle,
|
|
192
|
-
currentScenarioSteps,
|
|
193
|
-
testFn,
|
|
194
|
-
isOutline,
|
|
195
|
-
failOnUnmatchedStep
|
|
196
|
-
) => {
|
|
197
|
-
testFn(currentScenarioTitle, ({ given, when, then, and, but }) => {
|
|
198
|
-
currentScenarioSteps.forEach((currentStep) => {
|
|
199
|
-
matchJestDefinitionWithCucumberStep(
|
|
200
|
-
featureRegistry,
|
|
201
|
-
{ given, when, then, and, but },
|
|
202
|
-
currentStep,
|
|
203
|
-
isOutline,
|
|
204
|
-
failOnUnmatchedStep
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const matchJestDefinitionWithCucumberStep = (
|
|
211
|
-
featureRegistry,
|
|
212
|
-
verbFunction,
|
|
213
|
-
currentStep,
|
|
214
|
-
isOutline,
|
|
215
|
-
failOnUnmatchedStep
|
|
216
|
-
) => {
|
|
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
|
-
}
|
|
227
|
-
|
|
228
|
-
// this will be the "given", "when", "then"...functions
|
|
229
|
-
verbFunction[currentStep.keyword](
|
|
230
|
-
foundMatchingStep.stepExpression,
|
|
231
|
-
foundMatchingStep.stepFn
|
|
232
|
-
);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const findMatchingStep = (featureRegistry, currentStep, isOutline) => {
|
|
236
|
-
const scenarioType = currentStep.keyword;
|
|
237
|
-
const scenarioSentence = currentStep.stepText;
|
|
238
|
-
const matchingSteps = Object.keys(featureRegistry[scenarioType]).filter(
|
|
239
|
-
(currentStepDefinitionFunction) => {
|
|
240
|
-
return isFunctionForScenario(
|
|
241
|
-
scenarioSentence,
|
|
242
|
-
featureRegistry[scenarioType][currentStepDefinitionFunction],
|
|
243
|
-
isOutline
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
);
|
|
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}`
|
|
201
|
+
effectiveOptions,
|
|
255
202
|
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return injectVariable(
|
|
259
|
-
featureRegistry,
|
|
260
|
-
scenarioType,
|
|
261
|
-
scenarioSentence,
|
|
262
|
-
matchingSteps[0],
|
|
263
|
-
currentStep.stepArgument
|
|
264
|
-
);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const isFunctionForScenario = (
|
|
268
|
-
scenarioSentence,
|
|
269
|
-
stepDefinitionFunction,
|
|
270
|
-
isOutline
|
|
271
|
-
) => {
|
|
272
|
-
if (stepDefinitionFunction.stepRegExp) {
|
|
273
|
-
if (isOutline && /<[\w]*>/.test(scenarioSentence)) {
|
|
274
|
-
return isPotentialStepFunctionForScenario(
|
|
275
|
-
scenarioSentence,
|
|
276
|
-
stepDefinitionFunction.stepRegExp
|
|
277
|
-
);
|
|
278
|
-
} else return scenarioSentence.match(stepDefinitionFunction.stepRegExp);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return scenarioSentence === stepDefinitionFunction.stepExpression;
|
|
282
|
-
};
|
|
283
203
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
// its result still addresses the RAW string.
|
|
288
|
-
const maskEscapedParens = (stepFunctionDef) =>
|
|
289
|
-
stepFunctionDef.replace(/\\[()]/g, "\\-");
|
|
204
|
+
// This feature binds the definitions and hooks registered for IT, captured before the
|
|
205
|
+
// registry is reset below, so the binding never depends on registration that came after.
|
|
206
|
+
const registryForThisFeature = stepsDefinition();
|
|
290
207
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.replace(/\\\(/g, "(")
|
|
296
|
-
.replace(/\\\)/g, ")")
|
|
297
|
-
.replace(/\\\^/g, "^")
|
|
298
|
-
.replace(/\\\$/g, "$");
|
|
299
|
-
|
|
300
|
-
const isPotentialStepFunctionForScenario = (
|
|
301
|
-
scenarioDefinition,
|
|
302
|
-
regStepFunc
|
|
303
|
-
) => {
|
|
304
|
-
//so this one is tricky, to ensure we only find the
|
|
305
|
-
// step definition corresponding to actual steps function in the case of outlined gherkin
|
|
306
|
-
// we have to "disable" the outlining (since it can replace regular expression
|
|
307
|
-
// and then ensure that all "non-outlined" part do respect the regular expression of
|
|
308
|
-
// of the step function
|
|
309
|
-
// FIRST, we clean the string version of the step definition that has outline variable
|
|
310
|
-
const cleanedStepFunc = regStepFunc.source
|
|
311
|
-
.replace(/^\^/, "")
|
|
312
|
-
// .replace( /\\\(/g, '(' )
|
|
313
|
-
// .replace( /\\\)/g, ')')
|
|
314
|
-
// .replace( /\\\^/g, '^')
|
|
315
|
-
// .replace( /\\\$/g, '$')
|
|
316
|
-
.replace(/\$$/, "");
|
|
317
|
-
// .replace( /\([.\\]+[sSdDwWbB*][*?+]?\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g, '' )
|
|
318
|
-
|
|
319
|
-
let currentScenarioPart;
|
|
320
|
-
let currentStepFuncLeft = cleanedStepFunc;
|
|
321
|
-
let currentScenarioDefLeft = scenarioDefinition;
|
|
322
|
-
|
|
323
|
-
//we step through each of the scenario outline variables
|
|
324
|
-
// from there, we will try to detect any regexp present in the
|
|
325
|
-
// step definition, so that we can ensure to find the right match
|
|
326
|
-
while (
|
|
327
|
-
(currentScenarioPart = /<[\w]*>/gi.exec(currentScenarioDefLeft)) != null
|
|
328
|
-
) {
|
|
329
|
-
let fixedPart = currentScenarioPart.input.substring(
|
|
330
|
-
0,
|
|
331
|
-
currentScenarioPart.index
|
|
332
|
-
);
|
|
333
|
-
let idxCutScenarioPart =
|
|
334
|
-
currentScenarioPart.index + currentScenarioPart[0].length;
|
|
335
|
-
|
|
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)
|
|
208
|
+
testRegistration.registerFeature(
|
|
209
|
+
loadedFeature,
|
|
210
|
+
registryForThisFeature,
|
|
211
|
+
effectiveOptions,
|
|
349
212
|
);
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
groupInStepFunc &&
|
|
358
|
-
stepFuncPrefixAsScenarioText.length == currentScenarioPart.index
|
|
359
|
-
) {
|
|
360
|
-
//if we have a regex inside our step function definition
|
|
361
|
-
// and that regex is at the same position than our Outlined variable
|
|
362
|
-
// we just need to check that the sentence match,
|
|
363
|
-
// so we can "evaluate" the step function and remove the regex in it
|
|
364
|
-
currentStepFuncLeft =
|
|
365
|
-
stepFuncPrefixAsScenarioText +
|
|
366
|
-
currentStepFuncLeft.substring(
|
|
367
|
-
groupInStepFunc.index + groupInStepFunc[0].length
|
|
368
|
-
);
|
|
369
|
-
} else if (
|
|
370
|
-
groupInStepFunc &&
|
|
371
|
-
groupInStepFunc.index < currentScenarioPart.index
|
|
372
|
-
) {
|
|
373
|
-
//if we have a regex inside our step function definition
|
|
374
|
-
// but that regex is not at the same position than our outlined variable
|
|
375
|
-
// we need to evaluate the regex against the scenario part
|
|
376
|
-
const strRegexToEvaluate = currentStepFuncLeft.substring(
|
|
377
|
-
0,
|
|
378
|
-
groupInStepFunc.index + groupInStepFunc[0].length
|
|
379
|
-
);
|
|
380
|
-
const regexToEvaluate = new RegExp(strRegexToEvaluate);
|
|
381
|
-
const regIntermediatePart = regexToEvaluate.exec(
|
|
382
|
-
currentScenarioPart.input
|
|
383
|
-
);
|
|
384
|
-
if (regIntermediatePart) {
|
|
385
|
-
fixedPart = strRegexToEvaluate;
|
|
386
|
-
idxCutScenarioPart = regIntermediatePart[0].length;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const partIndex = currentStepFuncLeft.indexOf(fixedPart);
|
|
391
|
-
if (partIndex !== -1) {
|
|
392
|
-
currentStepFuncLeft = currentStepFuncLeft.substring(
|
|
393
|
-
partIndex + fixedPart.length
|
|
394
|
-
);
|
|
395
|
-
currentScenarioDefLeft =
|
|
396
|
-
currentScenarioDefLeft.substring(idxCutScenarioPart);
|
|
397
|
-
} else {
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return (
|
|
403
|
-
(currentScenarioDefLeft === "" && currentStepFuncLeft === "") ||
|
|
404
|
-
evaluateStepFuncEndVsScenarioEnd(
|
|
405
|
-
currentStepFuncLeft,
|
|
406
|
-
currentScenarioDefLeft
|
|
407
|
-
)
|
|
408
|
-
);
|
|
409
|
-
};
|
|
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
|
-
|
|
417
|
-
const evaluateStepFuncEndVsScenarioEnd = (
|
|
418
|
-
stepFunctionDef,
|
|
419
|
-
scenarioDefinition
|
|
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.
|
|
427
|
-
if (
|
|
428
|
-
/\(.*(\?\:)?[.\\]*[sSdDwWbB*][*?+]?.*\)|\(\[.*\](?:[+?*]{1}|\{\d\})\)/g.test(
|
|
429
|
-
stepFunctionDef
|
|
430
|
-
) ||
|
|
431
|
-
holdsCapturingGroup(stepFunctionDef)
|
|
432
|
-
) {
|
|
433
|
-
return new RegExp(stepFunctionDef).test(scenarioDefinition);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return stepFunctionDef.endsWith(scenarioDefinition);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const injectVariable = (
|
|
440
|
-
featureRegistry,
|
|
441
|
-
scenarioType,
|
|
442
|
-
scenarioSentence,
|
|
443
|
-
stepFunctionDefinition,
|
|
444
|
-
stepArgs
|
|
445
|
-
) => {
|
|
446
|
-
const stepObject = featureRegistry[scenarioType][stepFunctionDefinition];
|
|
447
|
-
|
|
448
|
-
if (!stepObject.stepRegExp)
|
|
449
|
-
return {
|
|
450
|
-
stepExpression: scenarioSentence,
|
|
451
|
-
// Forward through a rest-param wrapper so the reported arity is 0.
|
|
452
|
-
// jest-cucumber v4.4.0+ treats an arity mismatch (stepFn.length >
|
|
453
|
-
// matched args) as a request for a done() callback and then blocks
|
|
454
|
-
// on a Promise that never resolves. A rest param has length 0 while
|
|
455
|
-
// still passing along whatever args jest-cucumber provides.
|
|
456
|
-
stepFn: (...args) => stepObject.stepFn(...args),
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
const exprMatches = stepObject.stepRegExp.exec(scenarioSentence);
|
|
460
|
-
|
|
461
|
-
if (!exprMatches || /<.*>/.test(scenarioSentence))
|
|
462
|
-
return {
|
|
463
|
-
stepExpression: stepObject.stepRegExp,
|
|
464
|
-
// See note above: keep arity 0 to avoid jest-cucumber's done-callback
|
|
465
|
-
// heuristic hanging the step (matters for plain scenarios whose text
|
|
466
|
-
// contains "<...>" and for outline template steps).
|
|
467
|
-
stepFn: (...args) => stepObject.stepFn(...args),
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const dynamicMatchThatAreVariables = [];
|
|
471
|
-
|
|
472
|
-
exprMatches.forEach((match, groupIndex) => {
|
|
473
|
-
if (groupIndex > 0) dynamicMatchThatAreVariables.push(match);
|
|
474
|
-
});
|
|
475
|
-
|
|
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) {
|
|
482
|
-
dynamicMatchThatAreVariables.push(stepArgs);
|
|
213
|
+
} finally {
|
|
214
|
+
// Unconditional: Fusion() always leaves a clean slate (normal return OR throw). Replacing
|
|
215
|
+
// the shared registry (never mutating it in place) keeps the object captured above intact
|
|
216
|
+
// for whoever still holds it, while the next Fusion() starts empty and must re-register.
|
|
217
|
+
replaceShared("steps", emptyStepsDefinition());
|
|
483
218
|
}
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
stepExpression: stepObject.stepRegExp,
|
|
487
|
-
stepFn: () => stepObject.stepFn(...dynamicMatchThatAreVariables),
|
|
488
|
-
};
|
|
489
219
|
};
|
|
490
220
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
221
|
+
export { Before };
|
|
222
|
+
export { After };
|
|
223
|
+
export { Given };
|
|
224
|
+
export { When };
|
|
225
|
+
export { Then };
|
|
226
|
+
export { And };
|
|
227
|
+
export { But };
|
|
228
|
+
export { Fusion };
|
|
229
|
+
// Re-exported, not re-implemented: the module that owns the merge owns the layer the setter
|
|
230
|
+
// writes, so the entry point keeps no state of its own.
|
|
231
|
+
export { setFusionConfiguration };
|