@step-forge/step-forge 0.0.20 → 0.0.21

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.
@@ -0,0 +1,181 @@
1
+ import { r as globalHookRegistry } from "./hooks-CywugMQQ.js";
2
+ import { CucumberExpression, ParameterType, ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
3
+ //#region src/runtime/engine.ts
4
+ const keywordToStepType = {
5
+ Given: "given",
6
+ When: "when",
7
+ Then: "then"
8
+ };
9
+ var UndefinedStepError = class extends Error {
10
+ step;
11
+ constructor(step) {
12
+ super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);
13
+ this.step = step;
14
+ this.name = "UndefinedStepError";
15
+ }
16
+ };
17
+ var AmbiguousStepError = class extends Error {
18
+ step;
19
+ matches;
20
+ constructor(step, matches) {
21
+ super(`Ambiguous step: "${step.text}" matched ${matches.length} definitions:\n` + matches.map((m) => ` - ${m.expression}`).join("\n"));
22
+ this.step = step;
23
+ this.matches = matches;
24
+ this.name = "AmbiguousStepError";
25
+ }
26
+ };
27
+ /**
28
+ * Compile a registry's steps into matchable Cucumber expressions **once** per
29
+ * run. The result is reused for every scenario — compilation is pure and depends
30
+ * only on the registry, so recompiling per scenario (as an earlier version did)
31
+ * was wasted work proportional to scenarios × steps.
32
+ */
33
+ function compileRegistry(registry) {
34
+ return registry.all().map((step) => {
35
+ const paramRegistry = new ParameterTypeRegistry();
36
+ for (const parser of step.parsers) {
37
+ if (paramRegistry.lookupByTypeName(parser.name)) continue;
38
+ const regexps = Array.isArray(parser.regexp) ? parser.regexp : [parser.regexp];
39
+ paramRegistry.defineParameterType(new ParameterType(parser.name, regexps, null, (value) => parser.parse(value)));
40
+ }
41
+ return {
42
+ step,
43
+ expression: new CucumberExpression(step.expression, paramRegistry)
44
+ };
45
+ });
46
+ }
47
+ /**
48
+ * Find the single step definition matching a Gherkin step. Matching is
49
+ * opinionated and strict: the keyword must line up with the step type, exactly
50
+ * one definition must match, and undefined/ambiguous both throw rather than
51
+ * silently skipping (unlike Cucumber's pending/undefined dance).
52
+ */
53
+ function matchStep(step, compiled) {
54
+ const expectedType = keywordToStepType[step.effectiveKeyword];
55
+ const matches = [];
56
+ for (const { step: def, expression } of compiled) {
57
+ if (def.stepType !== expectedType) continue;
58
+ const result = expression.match(step.text);
59
+ if (result) matches.push({
60
+ step: def,
61
+ args: result.map((a) => a.getValue(null))
62
+ });
63
+ }
64
+ if (matches.length === 0) throw new UndefinedStepError(step);
65
+ if (matches.length > 1) throw new AmbiguousStepError(step, matches.map((m) => m.step));
66
+ return matches[0];
67
+ }
68
+ /**
69
+ * Run one scenario against a pre-compiled step table. A fresh world is created
70
+ * per scenario (state never leaks between scenarios). On the first failing step
71
+ * the remaining steps are marked skipped, matching Cucumber's execution
72
+ * semantics.
73
+ *
74
+ * Never throws for step or hook failures: it always resolves to a
75
+ * `ScenarioResult` carrying the per-step breakdown and (on failure) the first
76
+ * `error` with a synthetic `.feature` stack frame attached. Callers decide what
77
+ * to do with a failure — the CLI runner reports it, a test-runner adapter can
78
+ * re-throw `result.error`. It still rejects for truly exceptional conditions
79
+ * (e.g. a bug in the engine itself), never for a normal test failure.
80
+ *
81
+ * Pass the compiled table from {@link compileRegistry} once and reuse it across
82
+ * every scenario in the run.
83
+ */
84
+ async function runScenario(scenario, compiled, makeWorld, hooks = globalHookRegistry) {
85
+ const start = now();
86
+ const world = makeWorld();
87
+ const scenarioInfo = {
88
+ name: scenario.name,
89
+ file: scenario.file
90
+ };
91
+ const steps = [];
92
+ let failed = false;
93
+ let firstError;
94
+ const fail = (err) => {
95
+ if (failed) return;
96
+ failed = true;
97
+ firstError = err instanceof Error ? err : new Error(String(err));
98
+ };
99
+ try {
100
+ for (const hook of hooks.for("scenario", "before")) await hook.fn({
101
+ world,
102
+ scenario: scenarioInfo
103
+ });
104
+ } catch (err) {
105
+ fail(err);
106
+ }
107
+ for (const step of scenario.steps) {
108
+ if (failed) {
109
+ steps.push({
110
+ step,
111
+ status: "skipped"
112
+ });
113
+ continue;
114
+ }
115
+ try {
116
+ const { step: def, args } = matchStep(step, compiled);
117
+ await def.execute(world, args);
118
+ steps.push({
119
+ step,
120
+ status: "passed"
121
+ });
122
+ } catch (err) {
123
+ const error = err instanceof Error ? err : new Error(String(err));
124
+ steps.push({
125
+ step,
126
+ status: "failed",
127
+ error
128
+ });
129
+ fail(error);
130
+ }
131
+ }
132
+ for (const hook of hooks.for("scenario", "after")) try {
133
+ await hook.fn({
134
+ world,
135
+ scenario: scenarioInfo
136
+ });
137
+ } catch (err) {
138
+ fail(err);
139
+ }
140
+ if (failed && firstError) {
141
+ const failing = steps.find((s) => s.status === "failed");
142
+ if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
143
+ }
144
+ return {
145
+ scenario,
146
+ status: failed ? "failed" : "passed",
147
+ steps,
148
+ error: firstError,
149
+ durationMs: now() - start
150
+ };
151
+ }
152
+ /**
153
+ * Monotonic-ish millisecond clock. `performance.now()` where available (Node &
154
+ * Bun both expose it globally), falling back to `Date.now()`. Kept in one place
155
+ * so timing is consistent across scenarios.
156
+ */
157
+ function now() {
158
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
159
+ }
160
+ /**
161
+ * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because
162
+ * the frame's file is the real `.feature` on disk, the test runner treats it as
163
+ * a source location and shows a code frame at the step — instead of us jamming
164
+ * `file:line` into the error message. The frame goes *above* the real stack, so
165
+ * the step-definition frames (the actual throw site) are preserved below it.
166
+ */
167
+ function attachFeatureFrame(error, file, step) {
168
+ const frame = ` at ${`${step.effectiveKeyword} ${step.text}`} (${file}:${step.line}:${step.column})`;
169
+ const stack = error.stack;
170
+ if (!stack) {
171
+ error.stack = `${error.name}: ${error.message}\n${frame}`;
172
+ return;
173
+ }
174
+ const firstFrame = stack.indexOf("\n at ");
175
+ if (firstFrame === -1) error.stack = `${stack}\n${frame}`;
176
+ else error.stack = stack.slice(0, firstFrame) + `\n${frame}` + stack.slice(firstFrame);
177
+ }
178
+ //#endregion
179
+ export { runScenario as i, UndefinedStepError as n, compileRegistry as r, AmbiguousStepError as t };
180
+
181
+ //# sourceMappingURL=engine-DPRxs6eC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-DPRxs6eC.js","names":[],"sources":["../../src/runtime/engine.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n CucumberExpression,\n ParameterType,\n ParameterTypeRegistry,\n} from \"@cucumber/cucumber-expressions\";\nimport { ParsedScenario, ParsedStep } from \"../analyzer/types\";\nimport { MergeableWorld } from \"../world\";\nimport { globalHookRegistry, HookRegistry, ScenarioHookFn } from \"./hooks\";\nimport { RegisteredStep, StepRegistry, StepType } from \"./registry\";\n\nconst keywordToStepType: Record<ParsedStep[\"effectiveKeyword\"], StepType> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n};\n\n/** A registered step paired with its compiled Cucumber expression. */\nexport interface CompiledStep {\n step: RegisteredStep;\n expression: CucumberExpression;\n}\n\nexport class UndefinedStepError extends Error {\n constructor(public readonly step: ParsedStep) {\n super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);\n this.name = \"UndefinedStepError\";\n }\n}\n\nexport class AmbiguousStepError extends Error {\n constructor(\n public readonly step: ParsedStep,\n public readonly matches: RegisteredStep[]\n ) {\n super(\n `Ambiguous step: \"${step.text}\" matched ${matches.length} definitions:\\n` +\n matches.map(m => ` - ${m.expression}`).join(\"\\n\")\n );\n this.name = \"AmbiguousStepError\";\n }\n}\n\n/**\n * Compile a registry's steps into matchable Cucumber expressions **once** per\n * run. The result is reused for every scenario — compilation is pure and depends\n * only on the registry, so recompiling per scenario (as an earlier version did)\n * was wasted work proportional to scenarios × steps.\n */\nexport function compileRegistry(registry: StepRegistry): CompiledStep[] {\n return registry.all().map(step => {\n // Each step gets its own parameter-type registry (seeded with the\n // built-ins). A parser whose name is already registered — the built-in\n // `{int}`/`{float}`/`{string}`, or a repeat within the same step — reuses\n // that type; a novel name (e.g. `{boolean}`, `{color}`) is registered from\n // the parser's regexp + parse, so matching and coercion happen in one pass.\n const paramRegistry = new ParameterTypeRegistry();\n for (const parser of step.parsers) {\n if (paramRegistry.lookupByTypeName(parser.name)) continue;\n const regexps = Array.isArray(parser.regexp)\n ? parser.regexp\n : [parser.regexp];\n paramRegistry.defineParameterType(\n new ParameterType(parser.name, regexps, null, (value: string) =>\n parser.parse(value)\n )\n );\n }\n return {\n step,\n expression: new CucumberExpression(step.expression, paramRegistry),\n };\n });\n}\n\n/**\n * Find the single step definition matching a Gherkin step. Matching is\n * opinionated and strict: the keyword must line up with the step type, exactly\n * one definition must match, and undefined/ambiguous both throw rather than\n * silently skipping (unlike Cucumber's pending/undefined dance).\n */\nfunction matchStep(\n step: ParsedStep,\n compiled: CompiledStep[]\n): { step: RegisteredStep; args: unknown[] } {\n const expectedType = keywordToStepType[step.effectiveKeyword];\n const matches: { step: RegisteredStep; args: unknown[] }[] = [];\n\n for (const { step: def, expression } of compiled) {\n if (def.stepType !== expectedType) continue;\n const result = expression.match(step.text);\n if (result) {\n // The parsers are registered as the expression's parameter types, so the\n // captured values are already coerced (`{int}` → number, `{color}` → the\n // parser's T). `execute` consumes them as-is.\n matches.push({ step: def, args: result.map(a => a.getValue(null)) });\n }\n }\n\n if (matches.length === 0) throw new UndefinedStepError(step);\n if (matches.length > 1) {\n throw new AmbiguousStepError(\n step,\n matches.map(m => m.step)\n );\n }\n return matches[0];\n}\n\nexport interface StepResult {\n step: ParsedStep;\n status: \"passed\" | \"failed\" | \"skipped\";\n error?: Error;\n durationMs?: number;\n}\n\nexport interface ScenarioResult {\n scenario: ParsedScenario;\n status: \"passed\" | \"failed\";\n steps: StepResult[];\n /**\n * The scenario's first error, if it failed. Usually the same object as the\n * failing step's `error` (with a synthetic `.feature` stack frame attached),\n * but for a hook failure there's no step to point at, so this is the only\n * place it surfaces. Reporters read this; the runner never throws it.\n */\n error?: Error;\n /** Wall-clock duration of the whole scenario, in milliseconds. */\n durationMs?: number;\n}\n\n/**\n * Run one scenario against a pre-compiled step table. A fresh world is created\n * per scenario (state never leaks between scenarios). On the first failing step\n * the remaining steps are marked skipped, matching Cucumber's execution\n * semantics.\n *\n * Never throws for step or hook failures: it always resolves to a\n * `ScenarioResult` carrying the per-step breakdown and (on failure) the first\n * `error` with a synthetic `.feature` stack frame attached. Callers decide what\n * to do with a failure — the CLI runner reports it, a test-runner adapter can\n * re-throw `result.error`. It still rejects for truly exceptional conditions\n * (e.g. a bug in the engine itself), never for a normal test failure.\n *\n * Pass the compiled table from {@link compileRegistry} once and reuse it across\n * every scenario in the run.\n */\nexport async function runScenario(\n scenario: ParsedScenario,\n compiled: CompiledStep[],\n makeWorld: () => MergeableWorld<any, any, any>,\n hooks: HookRegistry = globalHookRegistry\n): Promise<ScenarioResult> {\n const start = now();\n const world = makeWorld();\n const scenarioInfo = { name: scenario.name, file: scenario.file };\n const steps: StepResult[] = [];\n let failed = false;\n let firstError: Error | undefined;\n\n const fail = (err: unknown) => {\n if (failed) return;\n failed = true;\n firstError = err instanceof Error ? err : new Error(String(err));\n };\n\n // before-scenario hooks: a throw here aborts the scenario before any step.\n try {\n for (const hook of hooks.for(\"scenario\", \"before\")) {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n }\n } catch (err) {\n fail(err);\n }\n\n for (const step of scenario.steps) {\n if (failed) {\n steps.push({ step, status: \"skipped\" });\n continue;\n }\n try {\n const { step: def, args } = matchStep(step, compiled);\n await def.execute(world, args);\n steps.push({ step, status: \"passed\" });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error });\n fail(error);\n }\n }\n\n // after-scenario hooks always run (teardown), even on failure. A hook failure\n // only becomes the scenario's error if nothing else failed first.\n for (const hook of hooks.for(\"scenario\", \"after\")) {\n try {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n } catch (err) {\n fail(err);\n }\n }\n\n if (failed && firstError) {\n // Surface the failing Gherkin line as a real stack frame so reporters render\n // a code frame from the `.feature` file itself. Hook failures have no step\n // to point at, so they surface with their own stack unchanged.\n const failing = steps.find(s => s.status === \"failed\");\n if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);\n }\n\n return {\n scenario,\n status: failed ? \"failed\" : \"passed\",\n steps,\n error: firstError,\n durationMs: now() - start,\n };\n}\n\n/**\n * Monotonic-ish millisecond clock. `performance.now()` where available (Node &\n * Bun both expose it globally), falling back to `Date.now()`. Kept in one place\n * so timing is consistent across scenarios.\n */\nfunction now(): number {\n return typeof performance !== \"undefined\" ? performance.now() : Date.now();\n}\n\n/**\n * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because\n * the frame's file is the real `.feature` on disk, the test runner treats it as\n * a source location and shows a code frame at the step — instead of us jamming\n * `file:line` into the error message. The frame goes *above* the real stack, so\n * the step-definition frames (the actual throw site) are preserved below it.\n */\nfunction attachFeatureFrame(\n error: Error,\n file: string,\n step: ParsedStep\n): void {\n const label = `${step.effectiveKeyword} ${step.text}`;\n const frame = ` at ${label} (${file}:${step.line}:${step.column})`;\n const stack = error.stack;\n if (!stack) {\n error.stack = `${error.name}: ${error.message}\\n${frame}`;\n return;\n }\n // A message can span multiple lines, so split on the first frame marker\n // rather than the first newline to find where the header ends.\n const firstFrame = stack.indexOf(\"\\n at \");\n if (firstFrame === -1) {\n error.stack = `${stack}\\n${frame}`;\n } else {\n error.stack =\n stack.slice(0, firstFrame) + `\\n${frame}` + stack.slice(firstFrame);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,204 @@
1
+ const require_hooks = require("./hooks-CGYzwDOv.cjs");
2
+ let _cucumber_cucumber_expressions = require("@cucumber/cucumber-expressions");
3
+ //#region src/runtime/engine.ts
4
+ const keywordToStepType = {
5
+ Given: "given",
6
+ When: "when",
7
+ Then: "then"
8
+ };
9
+ var UndefinedStepError = class extends Error {
10
+ step;
11
+ constructor(step) {
12
+ super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);
13
+ this.step = step;
14
+ this.name = "UndefinedStepError";
15
+ }
16
+ };
17
+ var AmbiguousStepError = class extends Error {
18
+ step;
19
+ matches;
20
+ constructor(step, matches) {
21
+ super(`Ambiguous step: "${step.text}" matched ${matches.length} definitions:\n` + matches.map((m) => ` - ${m.expression}`).join("\n"));
22
+ this.step = step;
23
+ this.matches = matches;
24
+ this.name = "AmbiguousStepError";
25
+ }
26
+ };
27
+ /**
28
+ * Compile a registry's steps into matchable Cucumber expressions **once** per
29
+ * run. The result is reused for every scenario — compilation is pure and depends
30
+ * only on the registry, so recompiling per scenario (as an earlier version did)
31
+ * was wasted work proportional to scenarios × steps.
32
+ */
33
+ function compileRegistry(registry) {
34
+ return registry.all().map((step) => {
35
+ const paramRegistry = new _cucumber_cucumber_expressions.ParameterTypeRegistry();
36
+ for (const parser of step.parsers) {
37
+ if (paramRegistry.lookupByTypeName(parser.name)) continue;
38
+ const regexps = Array.isArray(parser.regexp) ? parser.regexp : [parser.regexp];
39
+ paramRegistry.defineParameterType(new _cucumber_cucumber_expressions.ParameterType(parser.name, regexps, null, (value) => parser.parse(value)));
40
+ }
41
+ return {
42
+ step,
43
+ expression: new _cucumber_cucumber_expressions.CucumberExpression(step.expression, paramRegistry)
44
+ };
45
+ });
46
+ }
47
+ /**
48
+ * Find the single step definition matching a Gherkin step. Matching is
49
+ * opinionated and strict: the keyword must line up with the step type, exactly
50
+ * one definition must match, and undefined/ambiguous both throw rather than
51
+ * silently skipping (unlike Cucumber's pending/undefined dance).
52
+ */
53
+ function matchStep(step, compiled) {
54
+ const expectedType = keywordToStepType[step.effectiveKeyword];
55
+ const matches = [];
56
+ for (const { step: def, expression } of compiled) {
57
+ if (def.stepType !== expectedType) continue;
58
+ const result = expression.match(step.text);
59
+ if (result) matches.push({
60
+ step: def,
61
+ args: result.map((a) => a.getValue(null))
62
+ });
63
+ }
64
+ if (matches.length === 0) throw new UndefinedStepError(step);
65
+ if (matches.length > 1) throw new AmbiguousStepError(step, matches.map((m) => m.step));
66
+ return matches[0];
67
+ }
68
+ /**
69
+ * Run one scenario against a pre-compiled step table. A fresh world is created
70
+ * per scenario (state never leaks between scenarios). On the first failing step
71
+ * the remaining steps are marked skipped, matching Cucumber's execution
72
+ * semantics.
73
+ *
74
+ * Never throws for step or hook failures: it always resolves to a
75
+ * `ScenarioResult` carrying the per-step breakdown and (on failure) the first
76
+ * `error` with a synthetic `.feature` stack frame attached. Callers decide what
77
+ * to do with a failure — the CLI runner reports it, a test-runner adapter can
78
+ * re-throw `result.error`. It still rejects for truly exceptional conditions
79
+ * (e.g. a bug in the engine itself), never for a normal test failure.
80
+ *
81
+ * Pass the compiled table from {@link compileRegistry} once and reuse it across
82
+ * every scenario in the run.
83
+ */
84
+ async function runScenario(scenario, compiled, makeWorld, hooks = require_hooks.globalHookRegistry) {
85
+ const start = now();
86
+ const world = makeWorld();
87
+ const scenarioInfo = {
88
+ name: scenario.name,
89
+ file: scenario.file
90
+ };
91
+ const steps = [];
92
+ let failed = false;
93
+ let firstError;
94
+ const fail = (err) => {
95
+ if (failed) return;
96
+ failed = true;
97
+ firstError = err instanceof Error ? err : new Error(String(err));
98
+ };
99
+ try {
100
+ for (const hook of hooks.for("scenario", "before")) await hook.fn({
101
+ world,
102
+ scenario: scenarioInfo
103
+ });
104
+ } catch (err) {
105
+ fail(err);
106
+ }
107
+ for (const step of scenario.steps) {
108
+ if (failed) {
109
+ steps.push({
110
+ step,
111
+ status: "skipped"
112
+ });
113
+ continue;
114
+ }
115
+ try {
116
+ const { step: def, args } = matchStep(step, compiled);
117
+ await def.execute(world, args);
118
+ steps.push({
119
+ step,
120
+ status: "passed"
121
+ });
122
+ } catch (err) {
123
+ const error = err instanceof Error ? err : new Error(String(err));
124
+ steps.push({
125
+ step,
126
+ status: "failed",
127
+ error
128
+ });
129
+ fail(error);
130
+ }
131
+ }
132
+ for (const hook of hooks.for("scenario", "after")) try {
133
+ await hook.fn({
134
+ world,
135
+ scenario: scenarioInfo
136
+ });
137
+ } catch (err) {
138
+ fail(err);
139
+ }
140
+ if (failed && firstError) {
141
+ const failing = steps.find((s) => s.status === "failed");
142
+ if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
143
+ }
144
+ return {
145
+ scenario,
146
+ status: failed ? "failed" : "passed",
147
+ steps,
148
+ error: firstError,
149
+ durationMs: now() - start
150
+ };
151
+ }
152
+ /**
153
+ * Monotonic-ish millisecond clock. `performance.now()` where available (Node &
154
+ * Bun both expose it globally), falling back to `Date.now()`. Kept in one place
155
+ * so timing is consistent across scenarios.
156
+ */
157
+ function now() {
158
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
159
+ }
160
+ /**
161
+ * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because
162
+ * the frame's file is the real `.feature` on disk, the test runner treats it as
163
+ * a source location and shows a code frame at the step — instead of us jamming
164
+ * `file:line` into the error message. The frame goes *above* the real stack, so
165
+ * the step-definition frames (the actual throw site) are preserved below it.
166
+ */
167
+ function attachFeatureFrame(error, file, step) {
168
+ const frame = ` at ${`${step.effectiveKeyword} ${step.text}`} (${file}:${step.line}:${step.column})`;
169
+ const stack = error.stack;
170
+ if (!stack) {
171
+ error.stack = `${error.name}: ${error.message}\n${frame}`;
172
+ return;
173
+ }
174
+ const firstFrame = stack.indexOf("\n at ");
175
+ if (firstFrame === -1) error.stack = `${stack}\n${frame}`;
176
+ else error.stack = stack.slice(0, firstFrame) + `\n${frame}` + stack.slice(firstFrame);
177
+ }
178
+ //#endregion
179
+ Object.defineProperty(exports, "AmbiguousStepError", {
180
+ enumerable: true,
181
+ get: function() {
182
+ return AmbiguousStepError;
183
+ }
184
+ });
185
+ Object.defineProperty(exports, "UndefinedStepError", {
186
+ enumerable: true,
187
+ get: function() {
188
+ return UndefinedStepError;
189
+ }
190
+ });
191
+ Object.defineProperty(exports, "compileRegistry", {
192
+ enumerable: true,
193
+ get: function() {
194
+ return compileRegistry;
195
+ }
196
+ });
197
+ Object.defineProperty(exports, "runScenario", {
198
+ enumerable: true,
199
+ get: function() {
200
+ return runScenario;
201
+ }
202
+ });
203
+
204
+ //# sourceMappingURL=engine-DWAIlwWp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-DWAIlwWp.cjs","names":["ParameterTypeRegistry","ParameterType","CucumberExpression","globalHookRegistry"],"sources":["../../src/runtime/engine.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n CucumberExpression,\n ParameterType,\n ParameterTypeRegistry,\n} from \"@cucumber/cucumber-expressions\";\nimport { ParsedScenario, ParsedStep } from \"../analyzer/types\";\nimport { MergeableWorld } from \"../world\";\nimport { globalHookRegistry, HookRegistry, ScenarioHookFn } from \"./hooks\";\nimport { RegisteredStep, StepRegistry, StepType } from \"./registry\";\n\nconst keywordToStepType: Record<ParsedStep[\"effectiveKeyword\"], StepType> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n};\n\n/** A registered step paired with its compiled Cucumber expression. */\nexport interface CompiledStep {\n step: RegisteredStep;\n expression: CucumberExpression;\n}\n\nexport class UndefinedStepError extends Error {\n constructor(public readonly step: ParsedStep) {\n super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);\n this.name = \"UndefinedStepError\";\n }\n}\n\nexport class AmbiguousStepError extends Error {\n constructor(\n public readonly step: ParsedStep,\n public readonly matches: RegisteredStep[]\n ) {\n super(\n `Ambiguous step: \"${step.text}\" matched ${matches.length} definitions:\\n` +\n matches.map(m => ` - ${m.expression}`).join(\"\\n\")\n );\n this.name = \"AmbiguousStepError\";\n }\n}\n\n/**\n * Compile a registry's steps into matchable Cucumber expressions **once** per\n * run. The result is reused for every scenario — compilation is pure and depends\n * only on the registry, so recompiling per scenario (as an earlier version did)\n * was wasted work proportional to scenarios × steps.\n */\nexport function compileRegistry(registry: StepRegistry): CompiledStep[] {\n return registry.all().map(step => {\n // Each step gets its own parameter-type registry (seeded with the\n // built-ins). A parser whose name is already registered — the built-in\n // `{int}`/`{float}`/`{string}`, or a repeat within the same step — reuses\n // that type; a novel name (e.g. `{boolean}`, `{color}`) is registered from\n // the parser's regexp + parse, so matching and coercion happen in one pass.\n const paramRegistry = new ParameterTypeRegistry();\n for (const parser of step.parsers) {\n if (paramRegistry.lookupByTypeName(parser.name)) continue;\n const regexps = Array.isArray(parser.regexp)\n ? parser.regexp\n : [parser.regexp];\n paramRegistry.defineParameterType(\n new ParameterType(parser.name, regexps, null, (value: string) =>\n parser.parse(value)\n )\n );\n }\n return {\n step,\n expression: new CucumberExpression(step.expression, paramRegistry),\n };\n });\n}\n\n/**\n * Find the single step definition matching a Gherkin step. Matching is\n * opinionated and strict: the keyword must line up with the step type, exactly\n * one definition must match, and undefined/ambiguous both throw rather than\n * silently skipping (unlike Cucumber's pending/undefined dance).\n */\nfunction matchStep(\n step: ParsedStep,\n compiled: CompiledStep[]\n): { step: RegisteredStep; args: unknown[] } {\n const expectedType = keywordToStepType[step.effectiveKeyword];\n const matches: { step: RegisteredStep; args: unknown[] }[] = [];\n\n for (const { step: def, expression } of compiled) {\n if (def.stepType !== expectedType) continue;\n const result = expression.match(step.text);\n if (result) {\n // The parsers are registered as the expression's parameter types, so the\n // captured values are already coerced (`{int}` → number, `{color}` → the\n // parser's T). `execute` consumes them as-is.\n matches.push({ step: def, args: result.map(a => a.getValue(null)) });\n }\n }\n\n if (matches.length === 0) throw new UndefinedStepError(step);\n if (matches.length > 1) {\n throw new AmbiguousStepError(\n step,\n matches.map(m => m.step)\n );\n }\n return matches[0];\n}\n\nexport interface StepResult {\n step: ParsedStep;\n status: \"passed\" | \"failed\" | \"skipped\";\n error?: Error;\n durationMs?: number;\n}\n\nexport interface ScenarioResult {\n scenario: ParsedScenario;\n status: \"passed\" | \"failed\";\n steps: StepResult[];\n /**\n * The scenario's first error, if it failed. Usually the same object as the\n * failing step's `error` (with a synthetic `.feature` stack frame attached),\n * but for a hook failure there's no step to point at, so this is the only\n * place it surfaces. Reporters read this; the runner never throws it.\n */\n error?: Error;\n /** Wall-clock duration of the whole scenario, in milliseconds. */\n durationMs?: number;\n}\n\n/**\n * Run one scenario against a pre-compiled step table. A fresh world is created\n * per scenario (state never leaks between scenarios). On the first failing step\n * the remaining steps are marked skipped, matching Cucumber's execution\n * semantics.\n *\n * Never throws for step or hook failures: it always resolves to a\n * `ScenarioResult` carrying the per-step breakdown and (on failure) the first\n * `error` with a synthetic `.feature` stack frame attached. Callers decide what\n * to do with a failure — the CLI runner reports it, a test-runner adapter can\n * re-throw `result.error`. It still rejects for truly exceptional conditions\n * (e.g. a bug in the engine itself), never for a normal test failure.\n *\n * Pass the compiled table from {@link compileRegistry} once and reuse it across\n * every scenario in the run.\n */\nexport async function runScenario(\n scenario: ParsedScenario,\n compiled: CompiledStep[],\n makeWorld: () => MergeableWorld<any, any, any>,\n hooks: HookRegistry = globalHookRegistry\n): Promise<ScenarioResult> {\n const start = now();\n const world = makeWorld();\n const scenarioInfo = { name: scenario.name, file: scenario.file };\n const steps: StepResult[] = [];\n let failed = false;\n let firstError: Error | undefined;\n\n const fail = (err: unknown) => {\n if (failed) return;\n failed = true;\n firstError = err instanceof Error ? err : new Error(String(err));\n };\n\n // before-scenario hooks: a throw here aborts the scenario before any step.\n try {\n for (const hook of hooks.for(\"scenario\", \"before\")) {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n }\n } catch (err) {\n fail(err);\n }\n\n for (const step of scenario.steps) {\n if (failed) {\n steps.push({ step, status: \"skipped\" });\n continue;\n }\n try {\n const { step: def, args } = matchStep(step, compiled);\n await def.execute(world, args);\n steps.push({ step, status: \"passed\" });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error });\n fail(error);\n }\n }\n\n // after-scenario hooks always run (teardown), even on failure. A hook failure\n // only becomes the scenario's error if nothing else failed first.\n for (const hook of hooks.for(\"scenario\", \"after\")) {\n try {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n } catch (err) {\n fail(err);\n }\n }\n\n if (failed && firstError) {\n // Surface the failing Gherkin line as a real stack frame so reporters render\n // a code frame from the `.feature` file itself. Hook failures have no step\n // to point at, so they surface with their own stack unchanged.\n const failing = steps.find(s => s.status === \"failed\");\n if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);\n }\n\n return {\n scenario,\n status: failed ? \"failed\" : \"passed\",\n steps,\n error: firstError,\n durationMs: now() - start,\n };\n}\n\n/**\n * Monotonic-ish millisecond clock. `performance.now()` where available (Node &\n * Bun both expose it globally), falling back to `Date.now()`. Kept in one place\n * so timing is consistent across scenarios.\n */\nfunction now(): number {\n return typeof performance !== \"undefined\" ? performance.now() : Date.now();\n}\n\n/**\n * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because\n * the frame's file is the real `.feature` on disk, the test runner treats it as\n * a source location and shows a code frame at the step — instead of us jamming\n * `file:line` into the error message. The frame goes *above* the real stack, so\n * the step-definition frames (the actual throw site) are preserved below it.\n */\nfunction attachFeatureFrame(\n error: Error,\n file: string,\n step: ParsedStep\n): void {\n const label = `${step.effectiveKeyword} ${step.text}`;\n const frame = ` at ${label} (${file}:${step.line}:${step.column})`;\n const stack = error.stack;\n if (!stack) {\n error.stack = `${error.name}: ${error.message}\\n${frame}`;\n return;\n }\n // A message can span multiple lines, so split on the first frame marker\n // rather than the first newline to find where the header ends.\n const firstFrame = stack.indexOf(\"\\n at \");\n if (firstFrame === -1) {\n error.stack = `${stack}\\n${frame}`;\n } else {\n error.stack =\n stack.slice(0, firstFrame) + `\\n${frame}` + stack.slice(firstFrame);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let lodash = require("lodash");
24
+ lodash = __toESM(lodash, 1);
25
+ let node_fs_promises = require("node:fs/promises");
26
+ let node_path = require("node:path");
27
+ node_path = __toESM(node_path, 1);
28
+ let node_fs = require("node:fs");
29
+ node_fs = __toESM(node_fs, 1);
30
+ let _cucumber_gherkin = require("@cucumber/gherkin");
31
+ let _cucumber_messages = require("@cucumber/messages");
32
+ _cucumber_messages = __toESM(_cucumber_messages, 1);
33
+ //#region src/world.ts
34
+ function mergeCustomizer(objValue, srcValue) {
35
+ if (lodash.default.isArray(objValue)) return objValue.concat(srcValue);
36
+ else if (objValue && !lodash.default.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
37
+ return objValue;
38
+ }
39
+ var BasicWorld = class {
40
+ givenState = {};
41
+ whenState = {};
42
+ thenState = {};
43
+ get given() {
44
+ return {
45
+ ...this.givenState,
46
+ merge: (newState) => {
47
+ this.givenState = lodash.default.merge({ ...this.givenState }, newState, mergeCustomizer);
48
+ }
49
+ };
50
+ }
51
+ get when() {
52
+ return {
53
+ ...this.whenState,
54
+ merge: (newState) => {
55
+ this.whenState = lodash.default.merge({ ...this.whenState }, newState, mergeCustomizer);
56
+ }
57
+ };
58
+ }
59
+ get then() {
60
+ return {
61
+ ...this.thenState,
62
+ merge: (newState) => {
63
+ this.thenState = lodash.default.merge({ ...this.thenState }, newState, mergeCustomizer);
64
+ }
65
+ };
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/globFiles.ts
70
+ /** Glob metacharacters. A pattern with none of these is a literal path. */
71
+ const MAGIC = /[*?[\]{}!()]/;
72
+ async function isFile(p) {
73
+ try {
74
+ return (await (0, node_fs_promises.stat)(p)).isFile();
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * Resolve glob patterns to a de-duplicated list of absolute file paths, with one
81
+ * important portability guarantee: a **literal absolute path** (no glob magic)
82
+ * is returned directly if it exists, without being handed to `glob()`.
83
+ *
84
+ * This exists because `node:fs`'s `glob` diverges between runtimes — Node
85
+ * matches an absolute-path pattern, Bun returns nothing for one. Rather than
86
+ * depend on that behaviour, we only ever glob relative patterns (against `cwd`)
87
+ * and short-circuit concrete absolute paths ourselves, so callers get identical
88
+ * results under Node and Bun.
89
+ */
90
+ async function globFiles(patterns, cwd = process.cwd()) {
91
+ const files = /* @__PURE__ */ new Set();
92
+ for (const pattern of patterns) {
93
+ if (node_path.isAbsolute(pattern) && !MAGIC.test(pattern)) {
94
+ if (await isFile(pattern)) files.add(pattern);
95
+ continue;
96
+ }
97
+ for await (const match of (0, node_fs_promises.glob)(pattern, { cwd })) files.add(node_path.resolve(cwd, match));
98
+ }
99
+ return [...files];
100
+ }
101
+ //#endregion
102
+ //#region src/analyzer/gherkinParser.ts
103
+ function parseFeatureFiles(filePaths) {
104
+ const scenarios = [];
105
+ for (const filePath of filePaths) {
106
+ const parsed = parseFeatureContent(node_fs.readFileSync(filePath, "utf-8"), filePath);
107
+ scenarios.push(...parsed);
108
+ }
109
+ return scenarios;
110
+ }
111
+ function parseFeatureContent(content, filePath) {
112
+ const feature = new _cucumber_gherkin.Parser(new _cucumber_gherkin.AstBuilder(_cucumber_messages.IdGenerator.uuid()), new _cucumber_gherkin.GherkinClassicTokenMatcher()).parse(content).feature;
113
+ if (!feature) return [];
114
+ const featureBackground = [];
115
+ const scenarios = [];
116
+ const featureTags = tagNames(feature.tags);
117
+ for (const child of feature.children) {
118
+ if (child.background) featureBackground.push(...child.background.steps);
119
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
120
+ if (child.rule) {
121
+ const ruleBackground = [...featureBackground];
122
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
123
+ for (const ruleChild of child.rule.children) {
124
+ if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
125
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
126
+ }
127
+ }
128
+ }
129
+ return scenarios;
130
+ }
131
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
132
+ function tagNames(tags) {
133
+ return [...new Set((tags ?? []).map((t) => t.name))];
134
+ }
135
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
136
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
137
+ if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
138
+ const bgParsed = convertSteps(backgroundSteps);
139
+ const scenarioParsed = convertSteps(scenario.steps);
140
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
141
+ return [{
142
+ name: scenario.name,
143
+ file: filePath,
144
+ steps: allSteps,
145
+ tags: scenarioTags
146
+ }];
147
+ }
148
+ const results = [];
149
+ for (const example of scenario.examples) {
150
+ if (!example.tableHeader || example.tableBody.length === 0) continue;
151
+ const headers = example.tableHeader.cells.map((c) => c.value);
152
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
153
+ for (const row of example.tableBody) {
154
+ const values = row.cells.map((c) => c.value);
155
+ const substitution = {};
156
+ headers.forEach((h, i) => {
157
+ substitution[h] = values[i];
158
+ });
159
+ const bgParsed = convertSteps(backgroundSteps);
160
+ const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
161
+ ...step,
162
+ text: substituteExampleValues(step.text, substitution)
163
+ }));
164
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
165
+ results.push({
166
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
167
+ file: filePath,
168
+ steps: allSteps,
169
+ tags: exampleTags,
170
+ outline: { name: scenario.name }
171
+ });
172
+ }
173
+ }
174
+ return results;
175
+ }
176
+ function convertSteps(steps) {
177
+ return steps.map((step) => ({
178
+ keyword: normalizeKeyword(step.keyword),
179
+ text: step.text,
180
+ line: step.location.line,
181
+ column: (step.location.column ?? 1) + step.keyword.length
182
+ }));
183
+ }
184
+ function normalizeKeyword(keyword) {
185
+ const trimmed = keyword.trim();
186
+ if (trimmed === "Given") return "Given";
187
+ if (trimmed === "When") return "When";
188
+ if (trimmed === "Then") return "Then";
189
+ if (trimmed === "And") return "And";
190
+ if (trimmed === "But") return "But";
191
+ return "Given";
192
+ }
193
+ function resolveEffectiveKeywords(steps) {
194
+ let lastEffective = "Given";
195
+ return steps.map((step) => {
196
+ let effectiveKeyword;
197
+ if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
198
+ else effectiveKeyword = step.keyword;
199
+ lastEffective = effectiveKeyword;
200
+ return {
201
+ ...step,
202
+ effectiveKeyword
203
+ };
204
+ });
205
+ }
206
+ function substituteExampleValues(text, substitution) {
207
+ let result = text;
208
+ for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
209
+ return result;
210
+ }
211
+ //#endregion
212
+ Object.defineProperty(exports, "BasicWorld", {
213
+ enumerable: true,
214
+ get: function() {
215
+ return BasicWorld;
216
+ }
217
+ });
218
+ Object.defineProperty(exports, "__toESM", {
219
+ enumerable: true,
220
+ get: function() {
221
+ return __toESM;
222
+ }
223
+ });
224
+ Object.defineProperty(exports, "globFiles", {
225
+ enumerable: true,
226
+ get: function() {
227
+ return globFiles;
228
+ }
229
+ });
230
+ Object.defineProperty(exports, "parseFeatureContent", {
231
+ enumerable: true,
232
+ get: function() {
233
+ return parseFeatureContent;
234
+ }
235
+ });
236
+ Object.defineProperty(exports, "parseFeatureFiles", {
237
+ enumerable: true,
238
+ get: function() {
239
+ return parseFeatureFiles;
240
+ }
241
+ });
242
+
243
+ //# sourceMappingURL=gherkinParser-CHpkEwii.cjs.map