@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.
Files changed (71) hide show
  1. package/README.md +444 -38
  2. package/dist/THIRD_PARTY_LICENSES.txt +91 -0
  3. package/dist/index.cjs +9917 -0
  4. package/dist/index.d.cts +145 -0
  5. package/package.json +60 -10
  6. package/scripts/prepare-hooks.js +23 -0
  7. package/src/code-suggestion.js +240 -0
  8. package/src/configuration.js +147 -0
  9. package/src/feature-source.js +322 -0
  10. package/src/index.d.ts +123 -13
  11. package/src/index.js +153 -420
  12. package/src/keywords.js +56 -0
  13. package/src/scenario-name.js +128 -0
  14. package/src/shared-state.js +36 -0
  15. package/src/step-argument.js +56 -0
  16. package/src/step-matching.js +89 -0
  17. package/src/tag-filter.js +90 -0
  18. package/src/test-registration.js +178 -0
  19. package/src/value-description.js +26 -0
  20. package/.prettierignore +0 -16
  21. package/.prettierrc.json +0 -0
  22. package/codecov +0 -0
  23. package/codecov.SHA256SUM +0 -1
  24. package/codecov.SHA256SUM.sig +0 -16
  25. package/docs/AdditionalConfiguration.md +0 -155
  26. package/docs/GherkinTables.md +0 -61
  27. package/docs/Language.md +0 -76
  28. package/docs/ReusingStepDefinitions.md +0 -110
  29. package/docs/RunningTheExamples.md +0 -16
  30. package/docs/ScenarioOutlines.md +0 -43
  31. package/docs/StepDefinitionArguments.md +0 -37
  32. package/docs/product/expectations/fix-l2-outline-regex/outline-regex-binds.md +0 -148
  33. package/docs/product/expectations/fix-l4-escaped-parens-outline/escaped-parens-bind-in-outlines.md +0 -106
  34. package/docs/product/expectations/fix-m3-singleton-reset/clean-slate-per-feature.md +0 -133
  35. package/test/specs/features/basic-scenarios.feature +0 -28
  36. package/test/specs/features/l3-step-argument-delivery.feature +0 -44
  37. package/test/specs/features/language.feature +0 -41
  38. package/test/specs/features/m6-hooks-once-per-test.feature +0 -21
  39. package/test/specs/features/m6-hooks-outline-only.feature +0 -12
  40. package/test/specs/features/reuse-definition.feature +0 -13
  41. package/test/specs/features/scenario-outline2.feature +0 -73
  42. package/test/specs/features/scenario-outlines.feature +0 -88
  43. package/test/specs/features/step-definitions/ambiguous-step-shadowing.steps.js +0 -92
  44. package/test/specs/features/step-definitions/basic-scenarios.steps.js +0 -62
  45. package/test/specs/features/step-definitions/fuzz-properties.steps.js +0 -170
  46. package/test/specs/features/step-definitions/hook-error.steps.js +0 -59
  47. package/test/specs/features/step-definitions/l2-outline-edge-cases.steps.js +0 -327
  48. package/test/specs/features/step-definitions/l3-step-argument-delivery.steps.js +0 -191
  49. package/test/specs/features/step-definitions/l4-escaped-parens-outline.steps.js +0 -256
  50. package/test/specs/features/step-definitions/language.steps.js +0 -86
  51. package/test/specs/features/step-definitions/m1-before-hooks-clobber.steps.js +0 -64
  52. package/test/specs/features/step-definitions/m2-duplicate-matcher.steps.js +0 -60
  53. package/test/specs/features/step-definitions/m3-singleton-reset.steps.js +0 -275
  54. package/test/specs/features/step-definitions/m4-callsite-resolution.steps.js +0 -76
  55. package/test/specs/features/step-definitions/m5-errors-false-silent-skip.steps.js +0 -80
  56. package/test/specs/features/step-definitions/m6-hooks-once-per-test.steps.js +0 -90
  57. package/test/specs/features/step-definitions/missing-feature-file.steps.js +0 -23
  58. package/test/specs/features/step-definitions/reuse-code.js +0 -22
  59. package/test/specs/features/step-definitions/reuse-definition.steps.js +0 -30
  60. package/test/specs/features/step-definitions/scenario-outline2.steps.js +0 -57
  61. package/test/specs/features/step-definitions/scenario-outlines.steps.js +0 -110
  62. package/test/specs/features/step-definitions/undefined-step.steps.js +0 -39
  63. package/test/specs/features/step-definitions/using-dynamic-values.steps.js +0 -70
  64. package/test/specs/features/step-definitions/using-gherkin-tables.steps.js +0 -42
  65. package/test/specs/features/undefined-step.feature +0 -4
  66. package/test/specs/features/using-dynamic-values.feature +0 -34
  67. package/test/specs/features/using-gherkin-tables.feature +0 -16
  68. package/test/src/bank-account.js +0 -17
  69. package/test/src/online-sales.js +0 -33
  70. package/test/src/rocket.js +0 -13
  71. package/test/src/todo-list.js +0 -20
@@ -0,0 +1,322 @@
1
+ // Driven port: everything outside the process boundary that getting a feature file needs.
2
+ //
3
+ // This is the ONLY module under src/ that requires the filesystem, the Gherkin parser or the
4
+ // caller-stack reader: docs/Architecture.md's table says so, and
5
+ // test/specs/arch/dependency-direction.steps.js holds every module to its row. It hands the
6
+ // rest of the package one plain value (a LoadedFeature), so the core works on values and
7
+ // needs no file or parser to be read or tested.
8
+ //
9
+ // LoadedFeature, the whole of what crosses back:
10
+ // { title, featureTags, scenarios: [ { title, excludedByTagFilter, tags,
11
+ // steps: [ { keyword, stepText, stepArgument } ] } ] }
12
+ // `keyword` is already a registry bucket, `stepText` is already substituted for its Examples
13
+ // row, and `stepArgument` is already the shape a step function receives, so nothing
14
+ // downstream has to know a pickle exists. `excludedByTagFilter` is present only when a
15
+ // tagFilter was given, and `featureTags` and `tags` are read only when a scenarioNameTemplate
16
+ // is given; all three are read by presence or truthiness, so a value staged without them
17
+ // behaves exactly as one did before those options did anything.
18
+
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import url from "url";
22
+ import callerSites from "callsites";
23
+ import * as gherkin from "@cucumber/gherkin";
24
+ import * as messages from "@cucumber/messages";
25
+ // The named export, not the default: 11.0.1 publishes both, and `default` is interop
26
+ // scaffolding a later major could drop without that being a documented breaking change.
27
+ import { parse as parseTagExpression } from "@cucumber/tag-expressions";
28
+
29
+ import { bucketForKeyword } from "./keywords.js";
30
+ import { stepArgumentFrom } from "./step-argument.js";
31
+ import { tagFilterFor } from "./tag-filter.js";
32
+
33
+ // Resolve the feature path from the FIRST stack frame outside this package, so an in-package
34
+ // re-export/wrapper frame does not retarget it; guard a shallow stack (no external frame) so
35
+ // we never call getFileName() on undefined.
36
+ //
37
+ // Only a frame whose file is ON DISK can be the caller: an absolute path, or the file: URL an
38
+ // ES module frame reports. Any other frame is skipped: a native function holds one with no file
39
+ // name when Fusion is handed straight to it, `[...].forEach(Fusion)`; Node's own modules report
40
+ // `node:events` and the like; code run through vm reports `evalmachine.<anonymous>`. Taking any
41
+ // of them as the caller resolved the path against the working directory (finding F7 of the
42
+ // PR #16 review, and the review of its fix).
43
+ const fileOnDisk = (fileName) => {
44
+ if (typeof fileName !== "string") return null;
45
+ if (fileName.startsWith("file:")) return url.fileURLToPath(fileName);
46
+ return path.isAbsolute(fileName) ? fileName : null;
47
+ };
48
+
49
+ // The directory this module runs from: src/ for the ES module entry, dist/ for the CommonJS
50
+ // bundle, where scripts/build-cjs.js defines import.meta.url as the bundle's own file.
51
+ const THIS_PACKAGE_DIRECTORY = path.dirname(url.fileURLToPath(import.meta.url));
52
+
53
+ const resolveFeaturePath = (featureFileToLoad) => {
54
+ const insideThisPackage = (fileName) =>
55
+ fileName.startsWith(THIS_PACKAGE_DIRECTORY + path.sep);
56
+
57
+ const callerFile = callerSites()
58
+ .map((currentFrame) => fileOnDisk(currentFrame.getFileName()))
59
+ .find((fileName) => fileName !== null && !insideThisPackage(fileName));
60
+ const dirOfCaller = path.dirname(callerFile || "");
61
+
62
+ return path.resolve(dirOfCaller, featureFileToLoad);
63
+ };
64
+
65
+ const readFeatureText = (absoluteFeatureFilePath) => {
66
+ if (!fs.existsSync(absoluteFeatureFilePath))
67
+ throw new Error(
68
+ `Feature file not found (${absoluteFeatureFilePath})` +
69
+ `. Fusion resolves a feature path relative to the directory of the file that calls` +
70
+ ` Fusion, so check the path from there.`,
71
+ );
72
+
73
+ // A directory exists too, and reading it threw a raw EISDIR (fresh fuzz of PR #16).
74
+ if (fs.statSync(absoluteFeatureFilePath).isDirectory())
75
+ throw new Error(
76
+ `Feature path is a directory, not a feature file (${absoluteFeatureFilePath})` +
77
+ `. Name the .feature file itself, relative to the file that calls Fusion.`,
78
+ );
79
+
80
+ return fs.readFileSync(absoluteFeatureFilePath, "utf8");
81
+ };
82
+
83
+ // Parsing and compiling are two steps rather than one so that a check over the parsed AST
84
+ // (the duplicate declared-title check below) can refuse before any pickle work is done.
85
+ const parseFeature = (featureText) => {
86
+ // One incrementing id source, shared with the compile below: it replaces the uuid v4 the
87
+ // removed intermediary used, which is one of the two routes by which uuid reached a
88
+ // consumer's tree.
89
+ const ids = messages.IdGenerator.incrementing();
90
+ const parser = new gherkin.Parser(
91
+ new gherkin.AstBuilder(ids),
92
+ new gherkin.GherkinClassicTokenMatcher(),
93
+ );
94
+
95
+ try {
96
+ return { document: parser.parse(featureText), ids };
97
+ } catch (parseFailure) {
98
+ throw new Error(`Error parsing feature Gherkin: ${parseFailure.message}`);
99
+ }
100
+ };
101
+
102
+ const compilePickles = (document, absoluteFeatureFilePath, ids) => {
103
+ try {
104
+ return gherkin.compile(document, absoluteFeatureFilePath, ids);
105
+ } catch (compileFailure) {
106
+ throw new Error(`Error parsing feature Gherkin: ${compileFailure.message}`);
107
+ }
108
+ };
109
+
110
+ // Every child of a feature, in document order, with a Rule's own children flattened in where
111
+ // the Rule is declared. One walker for both views below, so a Rule-nesting fix is made once
112
+ // rather than twice in two copies that have to be kept agreeing.
113
+ //
114
+ // A rule child is visited as well as descended into, not instead: that is what the two copies
115
+ // did, and the traversal has to stay equivalent because the registration order read off it is
116
+ // the 2.0.0 test-name contract.
117
+ const eachChild = (children, visit) => {
118
+ (children || []).forEach((child) => {
119
+ if (child.rule) eachChild(child.rule.children, visit);
120
+ visit(child);
121
+ });
122
+ };
123
+
124
+ // Every AST step, by id, so a pickle step can be resolved back to the keyword a consumer
125
+ // registered against. A Rule's Background and scenarios carry steps of their own, which is why
126
+ // the walk descends.
127
+ const astStepsById = (feature) => {
128
+ const steps = new Map();
129
+
130
+ eachChild(feature.children, (child) => {
131
+ const node = child.background || child.scenario;
132
+ if (node) node.steps.forEach((step) => steps.set(step.id, step));
133
+ });
134
+
135
+ return steps;
136
+ };
137
+
138
+ // Every AST scenario, in document order, Rule children flattened in where they are declared.
139
+ // This is the DEFINITION-level view of the feature, and the only one anywhere in the package:
140
+ // one entry per declared Scenario or Scenario Outline, whatever its Examples do. Both the
141
+ // registration order and the duplicate-title check are read off it.
142
+ const astScenarios = (feature) => {
143
+ const scenarios = [];
144
+
145
+ eachChild(feature.children, (child) => {
146
+ if (child.scenario) scenarios.push(child.scenario);
147
+ });
148
+
149
+ return scenarios;
150
+ };
151
+
152
+ const byId = (scenarios) =>
153
+ new Map(scenarios.map((scenario) => [scenario.id, scenario]));
154
+
155
+ // Titles declared more than once, in the order they are first declared, each with its count.
156
+ // Compared case-insensitively, as the removed intermediary compared them
157
+ // (scenario-validation.js:8 and :14-17).
158
+ //
159
+ // DECLARED titles, never generated test names. One Scenario Outline contributes exactly one
160
+ // title however many Examples rows it has, which is the whole correctness of this check: a
161
+ // check over generated names would reject test/specs/features/scenario-outlines.feature,
162
+ // whose single outline produces three tests all named "Selling all of one", three names the
163
+ // 2.0.0 baseline records.
164
+ const duplicatedTitles = (scenarios) => {
165
+ const countByComparableTitle = new Map();
166
+
167
+ scenarios.forEach((scenario) => {
168
+ const comparable = scenario.name.trim().toLowerCase();
169
+ const seen = countByComparableTitle.get(comparable);
170
+
171
+ if (seen) seen.count += 1;
172
+ else
173
+ countByComparableTitle.set(comparable, {
174
+ title: scenario.name,
175
+ count: 1,
176
+ });
177
+ });
178
+
179
+ return [...countByComparableTitle.values()].filter((each) => each.count > 1);
180
+ };
181
+
182
+ const refuseDuplicatedTitles = (featureTitle, duplicated) => {
183
+ const entries = duplicated.map(
184
+ (each, index) =>
185
+ ` ${index + 1}. "${each.title}" is declared ${each.count} times`,
186
+ );
187
+
188
+ throw new Error(
189
+ `Duplicate scenario title: ${duplicated.length} title${
190
+ duplicated.length === 1 ? " is" : "s are"
191
+ } declared more than once in the feature "${featureTitle}".\n\n` +
192
+ `${entries.join("\n")}\n\n` +
193
+ `WHY: Fusion generates one test per scenario, named for that scenario, so two\n` +
194
+ ` scenarios declared with the same title cannot be told apart in a Jest report,\n` +
195
+ ` by a -t filter, or in a test history. Titles are compared ignoring case, and a\n` +
196
+ ` scenario inside a Rule counts with the rest.\n` +
197
+ `HOW: rename one of each pair, or pass\n` +
198
+ ` errors: { scenariosMustMatchFeatureFile: false } to accept the file as it\n` +
199
+ ` stands. A Scenario Outline counts once however many Examples rows it has, so\n` +
200
+ ` repeated ROW names are never this refusal.`,
201
+ );
202
+ };
203
+
204
+ // The first of a pickle node's astNodeIds that names something in the given index wins. A
205
+ // pickle step carries the ids of every AST node it was compiled from, and a pickle carries its
206
+ // scenario's and, on an Examples row, that row's: only some of them are in any one index.
207
+ const firstAstNodeOf = (pickleNode, byAstNodeId) =>
208
+ (pickleNode.astNodeIds || [])
209
+ .map((astNodeId) => byAstNodeId.get(astNodeId))
210
+ .find((candidate) => candidate !== undefined);
211
+
212
+ const keywordOfPickleStep = (pickleStep, stepsById, language) => {
213
+ const astStep = firstAstNodeOf(pickleStep, stepsById);
214
+
215
+ if (!astStep)
216
+ throw new Error(
217
+ `Unresolvable step: "${pickleStep.text}" carries no astNodeId that names a step of` +
218
+ ` the parsed feature, so the Gherkin keyword it was written with cannot be` +
219
+ ` recovered and Fusion cannot tell which step definitions may bind it.`,
220
+ );
221
+
222
+ return bucketForKeyword(
223
+ astStep.keyword,
224
+ gherkin.dialects[language],
225
+ language,
226
+ );
227
+ };
228
+
229
+ // Reproduce the registration order the previous engine produced: within one feature, EVERY
230
+ // plain scenario comes before ANY Examples row, each group in document order. Compiling
231
+ // pickles yields document order, so this partition is the whole of the difference, and a
232
+ // consumer's CI history is keyed on it (test/specs/baseline/test-names-2.0.0.txt).
233
+ //
234
+ // Rule-nested scenarios and outlines take part in the same two groups, flattened: the
235
+ // previous engine read one flat scenario list and one flat outline list for the whole
236
+ // feature, with no Rule in between.
237
+ const PLAIN_SCENARIOS_FIRST = 0;
238
+ const EXAMPLES_ROWS_AFTER = 1;
239
+
240
+ const scenarioOrder = (pickle, scenariosById) => {
241
+ const astScenario = firstAstNodeOf(pickle, scenariosById);
242
+
243
+ return astScenario && (astScenario.examples || []).length > 0
244
+ ? EXAMPLES_ROWS_AFTER
245
+ : PLAIN_SCENARIOS_FIRST;
246
+ };
247
+
248
+ const inRegistrationOrder = (pickles, scenariosById) =>
249
+ [PLAIN_SCENARIOS_FIRST, EXAMPLES_ROWS_AFTER].flatMap((group) =>
250
+ pickles.filter((pickle) => scenarioOrder(pickle, scenariosById) === group),
251
+ );
252
+
253
+ // The tag set a filter is evaluated against is the compiled pickle's own, which already unions
254
+ // the scenario's tags, the feature's tags and, for an Examples row, that Examples set's tags.
255
+ // One source makes that union correct by construction; the previous engine assembled it by
256
+ // hand in two places and had to keep them agreeing.
257
+ // The same shape reads a feature node's own declared tags, which is the other list a
258
+ // scenarioNameTemplate is handed. One reader of the tag shape rather than two.
259
+ const tagNamesOf = (tagged) => (tagged.tags || []).map((tag) => tag.name);
260
+
261
+ // No tagFilter means no filter is constructed and no scenario is marked, so loading is
262
+ // byte-for-byte what it was before this option did anything.
263
+ const tagFilterFrom = (options) =>
264
+ options.tagFilter
265
+ ? tagFilterFor(options.tagFilter, parseTagExpression)
266
+ : null;
267
+
268
+ // `options` is the merged configuration, which always carries a complete three-key errors
269
+ // object. Two keys govern this port: scenariosMustMatchFeatureFile for the duplicate check,
270
+ // and tagFilter for the selection.
271
+ const loadFeature = (absoluteFeatureFilePath, options) => {
272
+ const featureText = readFeatureText(absoluteFeatureFilePath);
273
+ const { document, ids } = parseFeature(featureText);
274
+
275
+ const feature = document.feature;
276
+ if (!feature) return { title: "", scenarios: [] };
277
+
278
+ const scenarios = astScenarios(feature);
279
+
280
+ // Refused before the pickles are compiled: nothing downstream can make sense of a feature
281
+ // whose scenarios cannot be told apart, so the work is not worth doing. It stays ahead of
282
+ // the tags deliberately: a duplicated title is a property of the FILE, so a selection must
283
+ // never be able to make a duplicate-title file acceptable.
284
+ if (options.errors.scenariosMustMatchFeatureFile) {
285
+ const duplicated = duplicatedTitles(scenarios);
286
+ if (duplicated.length > 0) refuseDuplicatedTitles(feature.name, duplicated);
287
+ }
288
+
289
+ // Parsed before the pickles too, so an unreadable expression is a refusal that costs the
290
+ // consumer nothing else and reaches them before anything is registered.
291
+ const selects = tagFilterFrom(options);
292
+
293
+ const pickles = compilePickles(document, absoluteFeatureFilePath, ids);
294
+ const stepsById = astStepsById(feature);
295
+ const scenariosById = byId(scenarios);
296
+
297
+ return {
298
+ title: feature.name,
299
+ // The feature's own declared tags, kept apart from the scenarios'. Only a
300
+ // scenarioNameTemplate reads them, and it needs the two lists separately.
301
+ featureTags: tagNamesOf(feature),
302
+ scenarios: inRegistrationOrder(pickles, scenariosById).map((pickle) => ({
303
+ title: pickle.name,
304
+ // Marked, never dropped: a scenario a filter excludes still has to appear in the report
305
+ // under its own name, or a tag filter is indistinguishable from deleting a scenario.
306
+ excludedByTagFilter: selects ? !selects(tagNamesOf(pickle)) : false,
307
+ // The whole tag set that reached this scenario, as the pickle unions it. The core
308
+ // subtracts the feature's tags from it where the template variables are assembled; the
309
+ // case is left exactly as the feature file wrote it, because only that assembly wants it
310
+ // lowercased.
311
+ tags: tagNamesOf(pickle),
312
+ steps: pickle.steps.map((pickleStep) => ({
313
+ keyword: keywordOfPickleStep(pickleStep, stepsById, feature.language),
314
+ stepText: pickleStep.text,
315
+ stepArgument: stepArgumentFrom(pickleStep.argument),
316
+ })),
317
+ })),
318
+ };
319
+ };
320
+
321
+ export { resolveFeaturePath };
322
+ export { loadFeature };
package/src/index.d.ts CHANGED
@@ -1,14 +1,17 @@
1
- // Type definitions for jest-cucumber-fusion 0.6
2
- // Project: https://github.com/b-yond-infinite-network/jest-cucumber-fusion#readme
3
- // Definitions by: Pelle Johnsen <https://github.com/pjoe>
4
- // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5
- // Minimum TypeScript Version: 3.4
1
+ // Type definitions for @g_package/jest-cucumber-fusion
2
+ // Project: https://github.com/gotreasa/jest-cucumber-fusion#readme
3
+ // Originally written by Pelle Johnsen <https://github.com/pjoe> for DefinitelyTyped.
6
4
 
7
- import { loadFeature } from "jest-cucumber";
5
+ /** What Fusion passes a step: a capture or a docstring as a string, a data table as its rows. */
6
+ export type StepArgument = string | Array<Record<string, string>>;
8
7
 
9
- export type CallBack = (
10
- ...args: ReadonlyArray<string | Array<Record<string, string>>>
11
- ) => void | Promise<void>;
8
+ // Declared through a method so its parameters are checked bivariantly: a step may declare
9
+ // the narrower type it receives, such as `(count: string) => ...`, which a plain function type
10
+ // refuses under strictFunctionTypes. A type Fusion never passes, such as `number`, is still
11
+ // refused, and an undeclared parameter is still a StepArgument.
12
+ export type CallBack = {
13
+ step(...args: ReadonlyArray<StepArgument>): void | Promise<void>;
14
+ }["step"];
12
15
 
13
16
  export interface StepChain {
14
17
  stepSentence: string | RegExp;
@@ -29,7 +32,114 @@ export function But(chain: StepChain): StepChain;
29
32
  export function Before(callback: () => void | Promise<void>): void;
30
33
  export function After(callback: () => void | Promise<void>): void;
31
34
 
32
- export function Fusion(
33
- feature: string,
34
- options?: Parameters<typeof loadFeature>[1]
35
- ): void;
35
+ /**
36
+ * Which of Fusion's validations are on.
37
+ *
38
+ * - `stepsMustMatchFeatureFile` (default on): a feature step that no registered definition
39
+ * matches is refused at collection, in one message naming every unbound step of the feature
40
+ * with starter code for each. Off, the scenarios holding those steps are registered through
41
+ * `test.skip`, so Jest reports them as skipped and never as passed.
42
+ * - `scenariosMustMatchFeatureFile` (default on): two scenarios of one feature file declared
43
+ * with the same title, ignoring case and counting a scenario inside a Rule, are refused at
44
+ * collection. A Scenario Outline counts once however many Examples rows it has, so repeated
45
+ * row names are never a duplicate. Off, the file is accepted as written.
46
+ * - `allowScenariosNotInFeatureFile`: accepted and vestigial. Fusion generates the scenario
47
+ * definitions from the feature file itself, so there is no scenario outside it to allow.
48
+ *
49
+ * As a boolean, `errors` is shorthand: `true` turns every key on, `false` turns every key off.
50
+ * As an object it merges KEY-WISE over the defaults, so naming one key says nothing about the
51
+ * others. Switching the step check off leaves the duplicate check exactly as it was.
52
+ */
53
+ export interface FusionErrorOptions {
54
+ stepsMustMatchFeatureFile?: boolean;
55
+ scenariosMustMatchFeatureFile?: boolean;
56
+ allowScenariosNotInFeatureFile?: boolean;
57
+ }
58
+
59
+ /**
60
+ * What a scenarioNameTemplate is handed, once per test. Unchanged from the shape consumers
61
+ * already write: these four variables and no others.
62
+ *
63
+ * `scenarioTitle` is the title of the individual test being named. For a Scenario Outline row
64
+ * that is the row's OWN substituted title, so each row gets its own name.
65
+ *
66
+ * `featureTags` are the feature's declared tags. `scenarioTags` are the rest of the tags that
67
+ * reached the scenario, which for an Examples row includes that Examples set's tags. The two
68
+ * lists are disjoint, and every tag carries its leading `@` and is lowercased. A tag declared
69
+ * on both the feature and the scenario appears in `featureTags` only.
70
+ */
71
+ export interface ScenarioNameTemplateVars {
72
+ featureTitle: string;
73
+ scenarioTitle: string;
74
+ scenarioTags: string[];
75
+ featureTags: string[];
76
+ }
77
+
78
+ export interface FusionOptions {
79
+ /**
80
+ * Accepted and ignored. Fusion always resolves a relative feature path against the
81
+ * directory of the file that called it, and hands an absolute path on from there, so there
82
+ * is nothing left for this to switch.
83
+ */
84
+ loadRelativePath?: boolean;
85
+ errors?: boolean | FusionErrorOptions;
86
+ /**
87
+ * Which scenarios to run, as a tag expression: a tag name written `@name`, combined with
88
+ * the operators `and`, `or` and `not` and grouped with parentheses. For example
89
+ * `@smoke and not @slow`, or `@shop and (@included or @draft)`.
90
+ *
91
+ * Matching ignores case on both sides: the expression and every tag are lowercased before
92
+ * they are compared, so the operators may be written in any case too.
93
+ *
94
+ * A scenario is selected on the tags its compiled pickle carries, which is the union of the
95
+ * scenario's own tags, its feature's tags and, for a Scenario Outline row, that Examples
96
+ * set's tags. A scenario the expression excludes is registered through `test.skip` under its
97
+ * own unannotated name, so Jest reports it as skipped: never run, and never absent from the
98
+ * report. Its steps are also exempt from the unmatched-step check, because a scenario you
99
+ * excluded is not one you are asking to have wired. They are still read, so a step with an
100
+ * unsupported keyword (`*`) or one that matches two definitions is refused all the same.
101
+ *
102
+ * An expression that cannot be parsed is refused at collection, before anything is
103
+ * registered, and the refusal names the expression you wrote. So is one with an operand
104
+ * that is not a tag, such as `smoke` written for `@smoke`: it could never match.
105
+ */
106
+ tagFilter?: string;
107
+ /**
108
+ * Renames every test Fusion registers, Scenario Outline rows included. It is called once per
109
+ * test while Fusion registers, never again while the tests run, and it names a test whatever
110
+ * its status: a scenario skipped because a tag filter excluded it, or because its steps do
111
+ * not bind, carries the same templated name it would have carried had it run.
112
+ *
113
+ * It must return a non-empty string. A template that throws, or that answers with anything
114
+ * else, is refused at collection rather than naming a test something Jest cannot report.
115
+ */
116
+ scenarioNameTemplate?: (vars: ScenarioNameTemplateVars) => string;
117
+ }
118
+
119
+ export function Fusion(feature: string, options?: FusionOptions): void;
120
+
121
+ /**
122
+ * Sets options for every `Fusion()` call in the current test file, so they need not be repeated
123
+ * in each one. Meant for a script listed in Jest's `setupFiles`:
124
+ *
125
+ * ```js
126
+ * // jest.config.js -> setupFiles: ["<rootDir>/jest.setup.js"]
127
+ * // jest.setup.js
128
+ * const { setFusionConfiguration } = require("@g_package/jest-cucumber-fusion");
129
+ * setFusionConfiguration({ tagFilter: "@smoke and not @slow" });
130
+ * ```
131
+ *
132
+ * Options are merged lowest to highest: the defaults, then whatever this setter holds, then the
133
+ * options passed to one `Fusion()` call. A per-call option therefore still wins for its own
134
+ * file, and `errors` merges key-wise at every layer, so naming one validation never switches
135
+ * off another. An option set to `undefined`, such as an unset environment variable forwarded
136
+ * as `{ tagFilter: process.env.TAGS }`, counts as not set and leaves the layer below in place.
137
+ *
138
+ * A second call REPLACES what the first set rather than merging into it, which is what lets a
139
+ * global be cleared or redefined. Jest gives each test file its own module registry and runs
140
+ * `setupFiles` inside it, so what is set here cannot reach another file.
141
+ *
142
+ * An argument that is not an options object is refused where it is called, before any step
143
+ * definition file has loaded. An unknown key is accepted and ignored, exactly as it is per call.
144
+ */
145
+ export function setFusionConfiguration(options: FusionOptions): void;