@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
|
@@ -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
|
|
2
|
-
// Project: https://github.com/
|
|
3
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
):
|
|
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;
|