@step-forge/step-forge 0.0.19 → 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.
@@ -1,4 +1,4 @@
1
- import { Given, Then, When } from "@cucumber/cucumber";
1
+ import { o as globalRegistry, r as globalHookRegistry } from "./hooks-CywugMQQ.js";
2
2
  import _ from "lodash";
3
3
  import { glob } from "node:fs/promises";
4
4
  import * as path from "node:path";
@@ -10,25 +10,36 @@ import * as messages from "@cucumber/messages";
10
10
  const isString = (statement) => typeof statement === "string";
11
11
  //#endregion
12
12
  //#region src/parsers.ts
13
- /** Matches a quoted string (`{string}`) and passes the unquoted contents through. */
13
+ /** Matches a quoted string (`{string}`) and strips the surrounding quotes. */
14
14
  const stringParser = {
15
- parse: (value) => String(value),
16
- gherkin: "{string}"
15
+ name: "string",
16
+ regexp: [/"([^"\\]*(\\.[^"\\]*)*)"/, /'([^'\\]*(\\.[^'\\]*)*)'/],
17
+ parse: (value) => {
18
+ const match = /^"([\s\S]*)"$/.exec(value) ?? /^'([\s\S]*)'$/.exec(value);
19
+ return match ? match[1].replace(/\\(["'])/g, "$1") : value;
20
+ }
17
21
  };
18
22
  /** Matches an unquoted integer (`{int}`). */
19
23
  const intParser = {
20
- parse: (value) => typeof value === "number" ? value : parseInt(String(value), 10),
21
- gherkin: "{int}"
24
+ name: "int",
25
+ regexp: /-?\d+/,
26
+ parse: (value) => parseInt(value, 10)
22
27
  };
23
28
  /** Matches an unquoted floating point number (`{float}`). */
24
29
  const numberParser = {
25
- parse: (value) => typeof value === "number" ? value : parseFloat(String(value)),
26
- gherkin: "{float}"
30
+ name: "float",
31
+ regexp: /-?\d*\.?\d+/,
32
+ parse: (value) => parseFloat(value)
27
33
  };
28
- /** Matches an unquoted `true`/`false` word (`{word}`) and parses it to a boolean. */
34
+ /**
35
+ * Matches an unquoted `true`/`false` word (`{boolean}`) and parses it to a
36
+ * boolean. Unlike the others this is a genuine custom parameter type — cucumber
37
+ * has no built-in `boolean` — so its `regexp`/`parse` are what the matcher uses.
38
+ */
29
39
  const booleanParser = {
30
- parse: (value) => value === true || value === "true",
31
- gherkin: "{word}"
40
+ name: "boolean",
41
+ regexp: /true|false/,
42
+ parse: (value) => value === "true"
32
43
  };
33
44
  //#endregion
34
45
  //#region src/utils.ts
@@ -67,151 +78,123 @@ const requireFromThen = (keys, world) => {
67
78
  };
68
79
  //#endregion
69
80
  //#region src/common.ts
70
- const cucFunctionMap = {
71
- given: Given,
72
- when: When,
73
- then: Then
74
- };
81
+ /**
82
+ * The runtime core of the builder chain. `addStep` is deliberately phase-agnostic:
83
+ * the calling builder (given/when/then) has already computed the exact type of the
84
+ * step function via its two type parameters:
85
+ *
86
+ * - `StepFnInput` — the `{ variables, given, when, then }` object the step receives,
87
+ * with each phase already narrowed to its declared dependencies.
88
+ * - `StepFnOutput` — the phase-appropriate return type (`Partial<State>`, or `void`).
89
+ *
90
+ * Everything else is plain runtime data (a statement function, the step type, the
91
+ * dependency map, the parsers), so `addStep` carries no generics for them.
92
+ */
75
93
  const addStep = (statement, stepType, dependencies = {
76
94
  given: {},
77
95
  when: {},
78
96
  then: {}
79
97
  }, declaredParsers) => (stepFunction) => {
80
- const statementFunction = statement;
81
98
  const { given: givenDependencies, when: whenDependencies, then: thenDependencies } = dependencies;
82
- const argCount = statementFunction.length;
99
+ const argCount = statement.length;
83
100
  const parsers = declaredParsers ?? Array.from({ length: argCount }, () => stringParser);
84
- const expression = statementFunction(...parsers.map((parser) => parser.gherkin));
101
+ const expression = statement(...parsers.map((parser) => `{${parser.name}}`));
102
+ const execute = async (world, capturedArgs) => {
103
+ const requiredKeys = (deps) => Object.entries(deps).filter(([, value]) => value === "required").map(([key]) => key);
104
+ const result = await stepFunction({
105
+ variables: capturedArgs,
106
+ given: {
107
+ ..._.pick(world.given, Object.keys(givenDependencies)),
108
+ ...requireFromGiven(requiredKeys(givenDependencies), world)
109
+ },
110
+ when: {
111
+ ..._.pick(world.when, Object.keys(whenDependencies)),
112
+ ...requireFromWhen(requiredKeys(whenDependencies), world)
113
+ },
114
+ then: {
115
+ ..._.pick(world.then, Object.keys(thenDependencies)),
116
+ ...requireFromThen(requiredKeys(thenDependencies), world)
117
+ }
118
+ });
119
+ world[stepType].merge({ ...result });
120
+ };
121
+ globalRegistry.add({
122
+ stepType,
123
+ expression,
124
+ parsers,
125
+ execute
126
+ });
85
127
  return {
86
128
  statement,
87
129
  expression,
88
130
  dependencies,
89
131
  stepType,
90
- stepFunction,
91
- register: () => {
92
- const cucStepFunction = Object.defineProperty(async function(...args) {
93
- const coercedArgs = parsers.map((parser, index) => parser.parse(args[index]));
94
- const ensuredGivenValues = requireFromGiven(Object.entries(givenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
95
- const narrowedGiven = {
96
- ..._.pick(this.given, Object.keys(givenDependencies ?? {})),
97
- ...ensuredGivenValues
98
- };
99
- const ensuredWhenValues = requireFromWhen(Object.entries(whenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
100
- const narrowedWhen = {
101
- ..._.pick(this.when, Object.keys(whenDependencies ?? {})),
102
- ...ensuredWhenValues
103
- };
104
- const ensuredThenValues = requireFromThen(Object.entries(thenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
105
- const result = await stepFunction({
106
- variables: coercedArgs,
107
- given: narrowedGiven,
108
- when: narrowedWhen,
109
- then: {
110
- ..._.pick(this.then, Object.keys(thenDependencies ?? {})),
111
- ...ensuredThenValues
112
- }
113
- });
114
- this[stepType].merge({ ...result });
115
- }, "length", {
116
- value: argCount,
117
- configurable: true
118
- });
119
- const cucStep = cucFunctionMap[stepType];
120
- cucStep(expression, cucStepFunction);
121
- return {
122
- stepType,
123
- expression,
124
- dependencies,
125
- statement: statementFunction,
126
- stepFunction
127
- };
128
- }
132
+ stepFunction
129
133
  };
130
134
  };
131
135
  //#endregion
132
136
  //#region src/given.ts
133
- const givenDependencies = (statement, stepType, parsers) => (dependencies) => {
134
- return { step: addStep(statement, stepType, {
135
- ...dependencies,
136
- when: {},
137
- then: {}
138
- }, parsers) };
139
- };
140
- const givenParsers = (statement, stepType) => (parsers) => {
141
- return {
142
- dependencies: givenDependencies(statement, stepType, parsers),
143
- step: addStep(statement, stepType, void 0, parsers)
144
- };
145
- };
146
- const givenStatement = (stepType) => (statement) => {
147
- let normalizedStatement;
148
- if (isString(statement)) normalizedStatement = (() => statement);
149
- else normalizedStatement = statement;
137
+ const GIVEN = "given";
138
+ const givenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, GIVEN, {
139
+ ...dependencies,
140
+ when: {},
141
+ then: {}
142
+ }, parsers) });
143
+ const givenParsers = (statement) => (parsers) => ({
144
+ dependencies: givenDependencies(statement, parsers),
145
+ step: addStep(statement, GIVEN, void 0, parsers)
146
+ });
147
+ const givenStatement = () => (statement) => {
148
+ const normalizedStatement = isString(statement) ? () => statement : statement;
150
149
  return {
151
- dependencies: givenDependencies(normalizedStatement, stepType),
152
- parsers: givenParsers(normalizedStatement, stepType),
153
- step: addStep(normalizedStatement, stepType)
150
+ dependencies: givenDependencies(normalizedStatement),
151
+ parsers: givenParsers(normalizedStatement),
152
+ step: addStep(normalizedStatement, GIVEN)
154
153
  };
155
154
  };
156
- const givenBuilder = () => {
157
- return { statement: givenStatement("given") };
158
- };
155
+ const givenBuilder = () => ({ statement: givenStatement() });
159
156
  //#endregion
160
157
  //#region src/when.ts
161
- const whenDependencies = (statement, stepType, parsers) => (dependencies) => {
162
- return { step: addStep(statement, stepType, {
163
- given: dependencies.given ?? {},
164
- when: dependencies.when ?? {},
165
- then: {}
166
- }, parsers) };
167
- };
168
- const whenParsers = (statement, stepType) => (parsers) => {
169
- return {
170
- dependencies: whenDependencies(statement, stepType, parsers),
171
- step: addStep(statement, stepType, void 0, parsers)
172
- };
173
- };
174
- const whenStatement = (stepType) => (statement) => {
175
- let normalizedStatement;
176
- if (isString(statement)) normalizedStatement = (() => statement);
177
- else normalizedStatement = statement;
158
+ const WHEN = "when";
159
+ const whenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, WHEN, {
160
+ given: dependencies.given ?? {},
161
+ when: dependencies.when ?? {},
162
+ then: {}
163
+ }, parsers) });
164
+ const whenParsers = (statement) => (parsers) => ({
165
+ dependencies: whenDependencies(statement, parsers),
166
+ step: addStep(statement, WHEN, void 0, parsers)
167
+ });
168
+ const whenStatement = () => (statement) => {
169
+ const normalizedStatement = isString(statement) ? () => statement : statement;
178
170
  return {
179
- dependencies: whenDependencies(normalizedStatement, stepType),
180
- parsers: whenParsers(normalizedStatement, stepType),
181
- step: addStep(normalizedStatement, stepType)
171
+ dependencies: whenDependencies(normalizedStatement),
172
+ parsers: whenParsers(normalizedStatement),
173
+ step: addStep(normalizedStatement, WHEN)
182
174
  };
183
175
  };
184
- const whenBuilder = () => {
185
- return { statement: whenStatement("when") };
186
- };
176
+ const whenBuilder = () => ({ statement: whenStatement() });
187
177
  //#endregion
188
178
  //#region src/then.ts
189
- const thenDependencies = (statement, stepType, parsers) => (dependencies) => {
190
- return { step: addStep(statement, stepType, {
191
- given: dependencies.given ?? {},
192
- when: dependencies.when ?? {},
193
- then: dependencies.then ?? {}
194
- }, parsers) };
195
- };
196
- const thenParsers = (statement, stepType) => (parsers) => {
197
- return {
198
- dependencies: thenDependencies(statement, stepType, parsers),
199
- step: addStep(statement, stepType, void 0, parsers)
200
- };
201
- };
202
- const thenStatement = (stepType) => (statement) => {
203
- let normalizedStatement;
204
- if (isString(statement)) normalizedStatement = (() => statement);
205
- else normalizedStatement = statement;
179
+ const THEN = "then";
180
+ const thenDependencies = (statement, parsers) => (dependencies) => ({ step: addStep(statement, THEN, {
181
+ given: dependencies.given ?? {},
182
+ when: dependencies.when ?? {},
183
+ then: dependencies.then ?? {}
184
+ }, parsers) });
185
+ const thenParsers = (statement) => (parsers) => ({
186
+ dependencies: thenDependencies(statement, parsers),
187
+ step: addStep(statement, THEN, void 0, parsers)
188
+ });
189
+ const thenStatement = () => (statement) => {
190
+ const normalizedStatement = isString(statement) ? () => statement : statement;
206
191
  return {
207
- dependencies: thenDependencies(normalizedStatement, stepType),
208
- parsers: thenParsers(normalizedStatement, stepType),
209
- step: addStep(normalizedStatement, stepType)
192
+ dependencies: thenDependencies(normalizedStatement),
193
+ parsers: thenParsers(normalizedStatement),
194
+ step: addStep(normalizedStatement, THEN)
210
195
  };
211
196
  };
212
- const thenBuilder = () => {
213
- return { statement: thenStatement("then") };
214
- };
197
+ const thenBuilder = () => ({ statement: thenStatement() });
215
198
  //#endregion
216
199
  //#region src/world.ts
217
200
  function mergeCustomizer(objValue, srcValue) {
@@ -283,7 +266,7 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
283
266
  function extractFromSourceFile(sourceFile, checker) {
284
267
  const results = [];
285
268
  function visit(node) {
286
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "register") {
269
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
287
270
  const meta = extractFromRegisterCall(node, sourceFile, checker);
288
271
  if (meta) results.push(meta);
289
272
  }
@@ -480,20 +463,27 @@ function parseFeatureContent(content, filePath) {
480
463
  if (!feature) return [];
481
464
  const featureBackground = [];
482
465
  const scenarios = [];
466
+ const featureTags = tagNames(feature.tags);
483
467
  for (const child of feature.children) {
484
468
  if (child.background) featureBackground.push(...child.background.steps);
485
- if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath));
469
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
486
470
  if (child.rule) {
487
471
  const ruleBackground = [...featureBackground];
472
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
488
473
  for (const ruleChild of child.rule.children) {
489
474
  if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
490
- if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath));
475
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
491
476
  }
492
477
  }
493
478
  }
494
479
  return scenarios;
495
480
  }
496
- function expandScenario(scenario, backgroundSteps, filePath) {
481
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
482
+ function tagNames(tags) {
483
+ return [...new Set((tags ?? []).map((t) => t.name))];
484
+ }
485
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
486
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
497
487
  if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
498
488
  const bgParsed = convertSteps(backgroundSteps);
499
489
  const scenarioParsed = convertSteps(scenario.steps);
@@ -501,13 +491,15 @@ function expandScenario(scenario, backgroundSteps, filePath) {
501
491
  return [{
502
492
  name: scenario.name,
503
493
  file: filePath,
504
- steps: allSteps
494
+ steps: allSteps,
495
+ tags: scenarioTags
505
496
  }];
506
497
  }
507
498
  const results = [];
508
499
  for (const example of scenario.examples) {
509
500
  if (!example.tableHeader || example.tableBody.length === 0) continue;
510
501
  const headers = example.tableHeader.cells.map((c) => c.value);
502
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
511
503
  for (const row of example.tableBody) {
512
504
  const values = row.cells.map((c) => c.value);
513
505
  const substitution = {};
@@ -521,9 +513,11 @@ function expandScenario(scenario, backgroundSteps, filePath) {
521
513
  }));
522
514
  const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
523
515
  results.push({
524
- name: `${scenario.name} (${values.join(", ")})`,
516
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
525
517
  file: filePath,
526
- steps: allSteps
518
+ steps: allSteps,
519
+ tags: exampleTags,
520
+ outline: { name: scenario.name }
527
521
  });
528
522
  }
529
523
  }
@@ -727,6 +721,65 @@ const createBuilders = () => {
727
721
  };
728
722
  };
729
723
  //#endregion
724
+ //#region src/hooks.ts
725
+ /**
726
+ * Run before every scenario, with access to that scenario's fresh world. For
727
+ * side effects only (reset a mock, seed an external system) — return values are
728
+ * ignored; seed test *state* with given steps so the dependency graph stays the
729
+ * single source of truth.
730
+ */
731
+ function beforeScenario(fn) {
732
+ globalHookRegistry.add({
733
+ scope: "scenario",
734
+ timing: "before",
735
+ fn
736
+ });
737
+ }
738
+ /**
739
+ * Run after every scenario (even when a step failed), with access to that
740
+ * scenario's world. Runs in reverse registration order so teardown unwinds
741
+ * setup. For cleanup side effects only.
742
+ */
743
+ function afterScenario(fn) {
744
+ globalHookRegistry.add({
745
+ scope: "scenario",
746
+ timing: "after",
747
+ fn
748
+ });
749
+ }
750
+ /** Run once at the start of each feature file. No world exists at this boundary. */
751
+ function beforeFeature(fn) {
752
+ globalHookRegistry.add({
753
+ scope: "feature",
754
+ timing: "before",
755
+ fn
756
+ });
757
+ }
758
+ /** Run once at the end of each feature file (reverse registration order). */
759
+ function afterFeature(fn) {
760
+ globalHookRegistry.add({
761
+ scope: "feature",
762
+ timing: "after",
763
+ fn
764
+ });
765
+ }
766
+ /** Run once before the entire test run, across all feature files. */
767
+ function beforeAll(fn) {
768
+ globalHookRegistry.add({
769
+ scope: "global",
770
+ timing: "before",
771
+ fn
772
+ });
773
+ }
774
+ /** Run once after the entire test run (reverse registration order). */
775
+ function afterAll(fn) {
776
+ globalHookRegistry.add({
777
+ scope: "global",
778
+ timing: "after",
779
+ fn
780
+ });
781
+ }
782
+ //#endregion
730
783
  //#region src/index.ts
731
784
  const analyzer = {
732
785
  analyze,
@@ -738,6 +791,6 @@ const analyzer = {
738
791
  defaultRules
739
792
  };
740
793
  //#endregion
741
- export { BasicWorld, analyzer, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
794
+ export { BasicWorld, afterAll, afterFeature, afterScenario, analyzer, beforeAll, beforeFeature, beforeScenario, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
742
795
 
743
796
  //# sourceMappingURL=step-forge.js.map