@step-forge/step-forge 0.0.17 → 0.0.20

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.
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let _cucumber_cucumber = require("@cucumber/cucumber");
24
+ const require_hooks = require("./hooks-CGYzwDOv.cjs");
25
25
  let lodash = require("lodash");
26
26
  lodash = __toESM(lodash, 1);
27
27
  let node_fs_promises = require("node:fs/promises");
@@ -38,25 +38,36 @@ _cucumber_messages = __toESM(_cucumber_messages, 1);
38
38
  const isString = (statement) => typeof statement === "string";
39
39
  //#endregion
40
40
  //#region src/parsers.ts
41
- /** Matches a quoted string (`{string}`) and passes the unquoted contents through. */
41
+ /** Matches a quoted string (`{string}`) and strips the surrounding quotes. */
42
42
  const stringParser = {
43
- parse: (value) => String(value),
44
- gherkin: "{string}"
43
+ name: "string",
44
+ regexp: [/"([^"\\]*(\\.[^"\\]*)*)"/, /'([^'\\]*(\\.[^'\\]*)*)'/],
45
+ parse: (value) => {
46
+ const match = /^"([\s\S]*)"$/.exec(value) ?? /^'([\s\S]*)'$/.exec(value);
47
+ return match ? match[1].replace(/\\(["'])/g, "$1") : value;
48
+ }
45
49
  };
46
50
  /** Matches an unquoted integer (`{int}`). */
47
51
  const intParser = {
48
- parse: (value) => typeof value === "number" ? value : parseInt(String(value), 10),
49
- gherkin: "{int}"
52
+ name: "int",
53
+ regexp: /-?\d+/,
54
+ parse: (value) => parseInt(value, 10)
50
55
  };
51
56
  /** Matches an unquoted floating point number (`{float}`). */
52
57
  const numberParser = {
53
- parse: (value) => typeof value === "number" ? value : parseFloat(String(value)),
54
- gherkin: "{float}"
58
+ name: "float",
59
+ regexp: /-?\d*\.?\d+/,
60
+ parse: (value) => parseFloat(value)
55
61
  };
56
- /** Matches an unquoted `true`/`false` word (`{word}`) and parses it to a boolean. */
62
+ /**
63
+ * Matches an unquoted `true`/`false` word (`{boolean}`) and parses it to a
64
+ * boolean. Unlike the others this is a genuine custom parameter type — cucumber
65
+ * has no built-in `boolean` — so its `regexp`/`parse` are what the matcher uses.
66
+ */
57
67
  const booleanParser = {
58
- parse: (value) => value === true || value === "true",
59
- gherkin: "{word}"
68
+ name: "boolean",
69
+ regexp: /true|false/,
70
+ parse: (value) => value === "true"
60
71
  };
61
72
  //#endregion
62
73
  //#region src/utils.ts
@@ -95,151 +106,123 @@ const requireFromThen = (keys, world) => {
95
106
  };
96
107
  //#endregion
97
108
  //#region src/common.ts
98
- const cucFunctionMap = {
99
- given: _cucumber_cucumber.Given,
100
- when: _cucumber_cucumber.When,
101
- then: _cucumber_cucumber.Then
102
- };
109
+ /**
110
+ * The runtime core of the builder chain. `addStep` is deliberately phase-agnostic:
111
+ * the calling builder (given/when/then) has already computed the exact type of the
112
+ * step function via its two type parameters:
113
+ *
114
+ * - `StepFnInput` — the `{ variables, given, when, then }` object the step receives,
115
+ * with each phase already narrowed to its declared dependencies.
116
+ * - `StepFnOutput` — the phase-appropriate return type (`Partial<State>`, or `void`).
117
+ *
118
+ * Everything else is plain runtime data (a statement function, the step type, the
119
+ * dependency map, the parsers), so `addStep` carries no generics for them.
120
+ */
103
121
  const addStep = (statement, stepType, dependencies = {
104
122
  given: {},
105
123
  when: {},
106
124
  then: {}
107
125
  }, declaredParsers) => (stepFunction) => {
108
- const statementFunction = statement;
109
126
  const { given: givenDependencies, when: whenDependencies, then: thenDependencies } = dependencies;
110
- const argCount = statementFunction.length;
127
+ const argCount = statement.length;
111
128
  const parsers = declaredParsers ?? Array.from({ length: argCount }, () => stringParser);
112
- const expression = statementFunction(...parsers.map((parser) => parser.gherkin));
129
+ const expression = statement(...parsers.map((parser) => `{${parser.name}}`));
130
+ const execute = async (world, capturedArgs) => {
131
+ const requiredKeys = (deps) => Object.entries(deps).filter(([, value]) => value === "required").map(([key]) => key);
132
+ const result = await stepFunction({
133
+ variables: capturedArgs,
134
+ given: {
135
+ ...lodash.default.pick(world.given, Object.keys(givenDependencies)),
136
+ ...requireFromGiven(requiredKeys(givenDependencies), world)
137
+ },
138
+ when: {
139
+ ...lodash.default.pick(world.when, Object.keys(whenDependencies)),
140
+ ...requireFromWhen(requiredKeys(whenDependencies), world)
141
+ },
142
+ then: {
143
+ ...lodash.default.pick(world.then, Object.keys(thenDependencies)),
144
+ ...requireFromThen(requiredKeys(thenDependencies), world)
145
+ }
146
+ });
147
+ world[stepType].merge({ ...result });
148
+ };
149
+ require_hooks.globalRegistry.add({
150
+ stepType,
151
+ expression,
152
+ parsers,
153
+ execute
154
+ });
113
155
  return {
114
156
  statement,
115
157
  expression,
116
158
  dependencies,
117
159
  stepType,
118
- stepFunction,
119
- register: () => {
120
- const cucStepFunction = Object.defineProperty(async function(...args) {
121
- const coercedArgs = parsers.map((parser, index) => parser.parse(args[index]));
122
- const ensuredGivenValues = requireFromGiven(Object.entries(givenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
123
- const narrowedGiven = {
124
- ...lodash.default.pick(this.given, Object.keys(givenDependencies ?? {})),
125
- ...ensuredGivenValues
126
- };
127
- const ensuredWhenValues = requireFromWhen(Object.entries(whenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
128
- const narrowedWhen = {
129
- ...lodash.default.pick(this.when, Object.keys(whenDependencies ?? {})),
130
- ...ensuredWhenValues
131
- };
132
- const ensuredThenValues = requireFromThen(Object.entries(thenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
133
- const result = await stepFunction({
134
- variables: coercedArgs,
135
- given: narrowedGiven,
136
- when: narrowedWhen,
137
- then: {
138
- ...lodash.default.pick(this.then, Object.keys(thenDependencies ?? {})),
139
- ...ensuredThenValues
140
- }
141
- });
142
- this[stepType].merge({ ...result });
143
- }, "length", {
144
- value: argCount,
145
- configurable: true
146
- });
147
- const cucStep = cucFunctionMap[stepType];
148
- cucStep(expression, cucStepFunction);
149
- return {
150
- stepType,
151
- expression,
152
- dependencies,
153
- statement: statementFunction,
154
- stepFunction
155
- };
156
- }
160
+ stepFunction
157
161
  };
158
162
  };
159
163
  //#endregion
160
164
  //#region src/given.ts
161
- const givenDependencies = (statement, stepType, parsers) => (dependencies) => {
162
- return { step: addStep(statement, stepType, {
163
- ...dependencies,
164
- when: {},
165
- then: {}
166
- }, parsers) };
167
- };
168
- const givenParsers = (statement, stepType) => (parsers) => {
169
- return {
170
- dependencies: givenDependencies(statement, stepType, parsers),
171
- step: addStep(statement, stepType, void 0, parsers)
172
- };
173
- };
174
- const givenStatement = (stepType) => (statement) => {
175
- let normalizedStatement;
176
- if (isString(statement)) normalizedStatement = (() => statement);
177
- else normalizedStatement = statement;
165
+ const GIVEN = "given";
166
+ const givenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, GIVEN, {
167
+ ...dependencies,
168
+ when: {},
169
+ then: {}
170
+ }, parsers) });
171
+ const givenParsers = (statement) => (parsers) => ({
172
+ dependencies: givenDependencies(statement, parsers),
173
+ step: addStep(statement, GIVEN, void 0, parsers)
174
+ });
175
+ const givenStatement = () => (statement) => {
176
+ const normalizedStatement = isString(statement) ? () => statement : statement;
178
177
  return {
179
- dependencies: givenDependencies(normalizedStatement, stepType),
180
- parsers: givenParsers(normalizedStatement, stepType),
181
- step: addStep(normalizedStatement, stepType)
178
+ dependencies: givenDependencies(normalizedStatement),
179
+ parsers: givenParsers(normalizedStatement),
180
+ step: addStep(normalizedStatement, GIVEN)
182
181
  };
183
182
  };
184
- const givenBuilder = () => {
185
- return { statement: givenStatement("given") };
186
- };
183
+ const givenBuilder = () => ({ statement: givenStatement() });
187
184
  //#endregion
188
185
  //#region src/when.ts
189
- const whenDependencies = (statement, stepType, parsers) => (dependencies) => {
190
- return { step: addStep(statement, stepType, {
191
- given: dependencies.given ?? {},
192
- when: dependencies.when ?? {},
193
- then: {}
194
- }, parsers) };
195
- };
196
- const whenParsers = (statement, stepType) => (parsers) => {
197
- return {
198
- dependencies: whenDependencies(statement, stepType, parsers),
199
- step: addStep(statement, stepType, void 0, parsers)
200
- };
201
- };
202
- const whenStatement = (stepType) => (statement) => {
203
- let normalizedStatement;
204
- if (isString(statement)) normalizedStatement = (() => statement);
205
- else normalizedStatement = statement;
186
+ const WHEN = "when";
187
+ const whenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, WHEN, {
188
+ given: dependencies.given ?? {},
189
+ when: dependencies.when ?? {},
190
+ then: {}
191
+ }, parsers) });
192
+ const whenParsers = (statement) => (parsers) => ({
193
+ dependencies: whenDependencies(statement, parsers),
194
+ step: addStep(statement, WHEN, void 0, parsers)
195
+ });
196
+ const whenStatement = () => (statement) => {
197
+ const normalizedStatement = isString(statement) ? () => statement : statement;
206
198
  return {
207
- dependencies: whenDependencies(normalizedStatement, stepType),
208
- parsers: whenParsers(normalizedStatement, stepType),
209
- step: addStep(normalizedStatement, stepType)
199
+ dependencies: whenDependencies(normalizedStatement),
200
+ parsers: whenParsers(normalizedStatement),
201
+ step: addStep(normalizedStatement, WHEN)
210
202
  };
211
203
  };
212
- const whenBuilder = () => {
213
- return { statement: whenStatement("when") };
214
- };
204
+ const whenBuilder = () => ({ statement: whenStatement() });
215
205
  //#endregion
216
206
  //#region src/then.ts
217
- const thenDependencies = (statement, stepType, parsers) => (dependencies) => {
218
- return { step: addStep(statement, stepType, {
219
- given: dependencies.given ?? {},
220
- when: dependencies.when ?? {},
221
- then: dependencies.then ?? {}
222
- }, parsers) };
223
- };
224
- const thenParsers = (statement, stepType) => (parsers) => {
207
+ const THEN = "then";
208
+ const thenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, THEN, {
209
+ given: dependencies.given ?? {},
210
+ when: dependencies.when ?? {},
211
+ then: dependencies.then ?? {}
212
+ }, parsers) });
213
+ const thenParsers = (statement) => (parsers) => ({
214
+ dependencies: thenDependencies(statement, parsers),
215
+ step: addStep(statement, THEN, void 0, parsers)
216
+ });
217
+ const thenStatement = () => (statement) => {
218
+ const normalizedStatement = isString(statement) ? () => statement : statement;
225
219
  return {
226
- dependencies: thenDependencies(statement, stepType, parsers),
227
- step: addStep(statement, stepType, void 0, parsers)
220
+ dependencies: thenDependencies(normalizedStatement),
221
+ parsers: thenParsers(normalizedStatement),
222
+ step: addStep(normalizedStatement, THEN)
228
223
  };
229
224
  };
230
- const thenStatement = (stepType) => (statement) => {
231
- let normalizedStatement;
232
- if (isString(statement)) normalizedStatement = (() => statement);
233
- else normalizedStatement = statement;
234
- return {
235
- dependencies: thenDependencies(normalizedStatement, stepType),
236
- parsers: thenParsers(normalizedStatement, stepType),
237
- step: addStep(normalizedStatement, stepType)
238
- };
239
- };
240
- const thenBuilder = () => {
241
- return { statement: thenStatement("then") };
242
- };
225
+ const thenBuilder = () => ({ statement: thenStatement() });
243
226
  //#endregion
244
227
  //#region src/world.ts
245
228
  function mergeCustomizer(objValue, srcValue) {
@@ -311,7 +294,7 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
311
294
  function extractFromSourceFile(sourceFile, checker) {
312
295
  const results = [];
313
296
  function visit(node) {
314
- if (typescript.default.isCallExpression(node) && typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "register") {
297
+ if (typescript.default.isCallExpression(node) && typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
315
298
  const meta = extractFromRegisterCall(node, sourceFile, checker);
316
299
  if (meta) results.push(meta);
317
300
  }
@@ -508,20 +491,27 @@ function parseFeatureContent(content, filePath) {
508
491
  if (!feature) return [];
509
492
  const featureBackground = [];
510
493
  const scenarios = [];
494
+ const featureTags = tagNames(feature.tags);
511
495
  for (const child of feature.children) {
512
496
  if (child.background) featureBackground.push(...child.background.steps);
513
- if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath));
497
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
514
498
  if (child.rule) {
515
499
  const ruleBackground = [...featureBackground];
500
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
516
501
  for (const ruleChild of child.rule.children) {
517
502
  if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
518
- if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath));
503
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
519
504
  }
520
505
  }
521
506
  }
522
507
  return scenarios;
523
508
  }
524
- function expandScenario(scenario, backgroundSteps, filePath) {
509
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
510
+ function tagNames(tags) {
511
+ return [...new Set((tags ?? []).map((t) => t.name))];
512
+ }
513
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
514
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
525
515
  if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
526
516
  const bgParsed = convertSteps(backgroundSteps);
527
517
  const scenarioParsed = convertSteps(scenario.steps);
@@ -529,13 +519,15 @@ function expandScenario(scenario, backgroundSteps, filePath) {
529
519
  return [{
530
520
  name: scenario.name,
531
521
  file: filePath,
532
- steps: allSteps
522
+ steps: allSteps,
523
+ tags: scenarioTags
533
524
  }];
534
525
  }
535
526
  const results = [];
536
527
  for (const example of scenario.examples) {
537
528
  if (!example.tableHeader || example.tableBody.length === 0) continue;
538
529
  const headers = example.tableHeader.cells.map((c) => c.value);
530
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
539
531
  for (const row of example.tableBody) {
540
532
  const values = row.cells.map((c) => c.value);
541
533
  const substitution = {};
@@ -549,9 +541,11 @@ function expandScenario(scenario, backgroundSteps, filePath) {
549
541
  }));
550
542
  const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
551
543
  results.push({
552
- name: `${scenario.name} (${values.join(", ")})`,
544
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
553
545
  file: filePath,
554
- steps: allSteps
546
+ steps: allSteps,
547
+ tags: exampleTags,
548
+ outline: { name: scenario.name }
555
549
  });
556
550
  }
557
551
  }
@@ -746,6 +740,74 @@ async function resolveGlobs(patterns) {
746
740
  return [...new Set(files)];
747
741
  }
748
742
  //#endregion
743
+ //#region src/init.ts
744
+ const createBuilders = () => {
745
+ return {
746
+ Given: givenBuilder().statement,
747
+ When: whenBuilder().statement,
748
+ Then: thenBuilder().statement
749
+ };
750
+ };
751
+ //#endregion
752
+ //#region src/hooks.ts
753
+ /**
754
+ * Run before every scenario, with access to that scenario's fresh world. For
755
+ * side effects only (reset a mock, seed an external system) — return values are
756
+ * ignored; seed test *state* with given steps so the dependency graph stays the
757
+ * single source of truth.
758
+ */
759
+ function beforeScenario(fn) {
760
+ require_hooks.globalHookRegistry.add({
761
+ scope: "scenario",
762
+ timing: "before",
763
+ fn
764
+ });
765
+ }
766
+ /**
767
+ * Run after every scenario (even when a step failed), with access to that
768
+ * scenario's world. Runs in reverse registration order so teardown unwinds
769
+ * setup. For cleanup side effects only.
770
+ */
771
+ function afterScenario(fn) {
772
+ require_hooks.globalHookRegistry.add({
773
+ scope: "scenario",
774
+ timing: "after",
775
+ fn
776
+ });
777
+ }
778
+ /** Run once at the start of each feature file. No world exists at this boundary. */
779
+ function beforeFeature(fn) {
780
+ require_hooks.globalHookRegistry.add({
781
+ scope: "feature",
782
+ timing: "before",
783
+ fn
784
+ });
785
+ }
786
+ /** Run once at the end of each feature file (reverse registration order). */
787
+ function afterFeature(fn) {
788
+ require_hooks.globalHookRegistry.add({
789
+ scope: "feature",
790
+ timing: "after",
791
+ fn
792
+ });
793
+ }
794
+ /** Run once before the entire test run, across all feature files. */
795
+ function beforeAll(fn) {
796
+ require_hooks.globalHookRegistry.add({
797
+ scope: "global",
798
+ timing: "before",
799
+ fn
800
+ });
801
+ }
802
+ /** Run once after the entire test run (reverse registration order). */
803
+ function afterAll(fn) {
804
+ require_hooks.globalHookRegistry.add({
805
+ scope: "global",
806
+ timing: "after",
807
+ fn
808
+ });
809
+ }
810
+ //#endregion
749
811
  //#region src/index.ts
750
812
  const analyzer = {
751
813
  analyze,
@@ -758,8 +820,15 @@ const analyzer = {
758
820
  };
759
821
  //#endregion
760
822
  exports.BasicWorld = BasicWorld;
823
+ exports.afterAll = afterAll;
824
+ exports.afterFeature = afterFeature;
825
+ exports.afterScenario = afterScenario;
761
826
  exports.analyzer = analyzer;
827
+ exports.beforeAll = beforeAll;
828
+ exports.beforeFeature = beforeFeature;
829
+ exports.beforeScenario = beforeScenario;
762
830
  exports.booleanParser = booleanParser;
831
+ exports.createBuilders = createBuilders;
763
832
  exports.givenBuilder = givenBuilder;
764
833
  exports.intParser = intParser;
765
834
  exports.numberParser = numberParser;