@step-forge/step-forge 0.0.21 → 0.0.22
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/RUNTIME.md +42 -12
- package/dist/{analyzer-DYfdoSIS.js → analyzer-byS8yRrY.js} +52 -24
- package/dist/analyzer-byS8yRrY.js.map +1 -0
- package/dist/analyzer-cli.js +1 -1
- package/dist/analyzer.d.ts +2 -0
- package/dist/analyzer.js +1 -1
- package/dist/cli.cjs +129 -74
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +129 -74
- package/dist/cli.js.map +1 -1
- package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
- package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
- package/dist/{engine-DPRxs6eC.js → engine-DPVLEHBi.js} +8 -26
- package/dist/engine-DPVLEHBi.js.map +1 -0
- package/dist/{engine-DWAIlwWp.cjs → engine-vqA-eL_T.cjs} +8 -26
- package/dist/engine-vqA-eL_T.cjs.map +1 -0
- package/dist/{gherkinParser-CHpkEwii.cjs → gherkinParser-BT40q_i3.cjs} +97 -2
- package/dist/gherkinParser-BT40q_i3.cjs.map +1 -0
- package/dist/{gherkinParser-CKARHgt4.js → gherkinParser-NcttZgN4.js} +74 -3
- package/dist/gherkinParser-NcttZgN4.js.map +1 -0
- package/dist/hooks-BDCMKeNq.js +71 -0
- package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
- package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
- package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
- package/dist/runtime.cjs +3 -3
- package/dist/runtime.d.cts +11 -48
- package/dist/runtime.d.ts +11 -48
- package/dist/runtime.js +3 -3
- package/dist/step-forge.cjs +65 -28
- package/dist/step-forge.cjs.map +1 -1
- package/dist/step-forge.d.cts +13 -4
- package/dist/step-forge.d.ts +13 -4
- package/dist/step-forge.js +65 -28
- package/dist/step-forge.js.map +1 -1
- package/package.json +2 -2
- package/dist/analyzer-DYfdoSIS.js.map +0 -1
- package/dist/engine-DPRxs6eC.js.map +0 -1
- package/dist/engine-DWAIlwWp.cjs.map +0 -1
- package/dist/gherkinParser-CHpkEwii.cjs.map +0 -1
- package/dist/gherkinParser-CKARHgt4.js.map +0 -1
- package/dist/hooks-CywugMQQ.js +0 -82
|
@@ -71,6 +71,8 @@ interface StepDefinitionMeta {
|
|
|
71
71
|
interface ParsedScenario {
|
|
72
72
|
name: string;
|
|
73
73
|
file: string;
|
|
74
|
+
/** 1-based line of the `Scenario:`/`Scenario Outline:` keyword in the file. */
|
|
75
|
+
line?: number;
|
|
74
76
|
steps: ParsedStep[];
|
|
75
77
|
/**
|
|
76
78
|
* Gherkin tags in effect for this scenario, each including the leading `@`
|
|
@@ -164,26 +166,73 @@ declare class HookRegistry {
|
|
|
164
166
|
}
|
|
165
167
|
declare const globalHookRegistry: HookRegistry;
|
|
166
168
|
/**
|
|
167
|
-
* Run every registered
|
|
168
|
-
*
|
|
169
|
+
* Run every registered hook of a scope+timing **in registration order** (after
|
|
170
|
+
* hooks reversed by {@link HookRegistry.for}, so teardown unwinds setup). Used
|
|
171
|
+
* for feature hooks, where ordering matters. Throws if a hook throws, so the
|
|
169
172
|
* runner reports it against the enclosing boundary.
|
|
170
173
|
*/
|
|
171
174
|
declare function runHooks(scope: "feature" | "global", timing: HookTiming, registry?: HookRegistry): Promise<void>;
|
|
172
175
|
/**
|
|
173
|
-
* Run
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
176
|
+
* Run every registered hook of a scope+timing **concurrently**, resolving once
|
|
177
|
+
* all of them settle. This is how global `beforeAll`/`afterAll` run: independent
|
|
178
|
+
* setup/teardown steps fire in parallel with no ordering between them. A hook
|
|
179
|
+
* with a sequential requirement should sequence that work inside a single hook.
|
|
177
180
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* in each. Size global setup to be worker-safe (e.g. a server per worker).
|
|
183
|
-
* - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and
|
|
184
|
-
* are not awaited by the runner, so keep them fast/synchronous.
|
|
181
|
+
* The runner calls this exactly once for `beforeAll` (before any scenario
|
|
182
|
+
* starts) and once for `afterAll` (after every scenario is done), so global
|
|
183
|
+
* setup/teardown brackets the whole run deterministically. Rejects if any hook
|
|
184
|
+
* rejects (via `Promise.all`), surfacing the first failure to the caller.
|
|
185
185
|
*/
|
|
186
|
-
declare function
|
|
186
|
+
declare function runHooksParallel(scope: "feature" | "global", timing: HookTiming, registry?: HookRegistry): Promise<void>;
|
|
187
187
|
//#endregion
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
//#region src/runtime/config.d.ts
|
|
189
|
+
/**
|
|
190
|
+
* User-facing runner configuration. The same shape is accepted from a
|
|
191
|
+
* `step-forge.config.ts` file (default export) and from CLI flags, with flags
|
|
192
|
+
* taking precedence. Everything is optional; {@link resolveConfig} fills in
|
|
193
|
+
* defaults.
|
|
194
|
+
*/
|
|
195
|
+
interface RunnerOptions {
|
|
196
|
+
/** Feature-file glob(s), relative to `cwd`. Defaults to every `.feature` file. */
|
|
197
|
+
features?: string | string[];
|
|
198
|
+
/** Step-module glob(s), relative to `cwd`. Defaults to every `.steps.ts` file. */
|
|
199
|
+
steps?: string | string[];
|
|
200
|
+
/**
|
|
201
|
+
* Module that default-exports a world factory `() => world`, relative to
|
|
202
|
+
* `cwd`. When omitted each scenario gets a fresh `BasicWorld`.
|
|
203
|
+
*/
|
|
204
|
+
world?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Max scenarios in flight at once. Defaults to `1` (serial), matching
|
|
207
|
+
* Cucumber's default execution model — scenarios often share module-level
|
|
208
|
+
* state via hooks, which only holds under serial execution. Raise it to opt
|
|
209
|
+
* into parallelism for isolated, I/O-bound suites.
|
|
210
|
+
*/
|
|
211
|
+
concurrency?: number;
|
|
212
|
+
/** Reporter name. Default `pretty`. */
|
|
213
|
+
reporter?: "pretty" | "progress";
|
|
214
|
+
/**
|
|
215
|
+
* Verbose output: report every scenario (pass, fail, skip) instead of only
|
|
216
|
+
* failures. Passed to whichever reporter is active. Default `false`.
|
|
217
|
+
*/
|
|
218
|
+
verbose?: boolean;
|
|
219
|
+
/** Only run scenarios whose name matches this (string → substring/regex). */
|
|
220
|
+
name?: string;
|
|
221
|
+
/** Cucumber tag expression, e.g. `@smoke and not @wip`. */
|
|
222
|
+
tags?: string;
|
|
223
|
+
}
|
|
224
|
+
/** Fully-resolved config: no optionals, globs kept as arrays, paths absolute. */
|
|
225
|
+
interface ResolvedConfig {
|
|
226
|
+
cwd: string;
|
|
227
|
+
features: string[];
|
|
228
|
+
steps: string[];
|
|
229
|
+
world?: string;
|
|
230
|
+
concurrency: number;
|
|
231
|
+
reporter: "pretty" | "progress";
|
|
232
|
+
verbose: boolean;
|
|
233
|
+
name?: string;
|
|
234
|
+
tags?: string;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
export { booleanParser as C, stringParser as E, Parser as S, numberParser as T, ParsedScenario as _, HookTiming as a, BasicWorld as b, ScenarioHookFn as c, runHooks as d, runHooksParallel as f, MatchedStep as g, Diagnostic as h, HookScope as i, ScenarioInfo as l, AnalyzerConfig as m, RunnerOptions as n, PlainHookFn as o, AnalysisRule as p, HookRegistry as r, RegisteredHook as s, ResolvedConfig as t, globalHookRegistry as u, ParsedStep as v, intParser as w, MergeableWorld as x, StepDefinitionMeta as y };
|
|
238
|
+
//# sourceMappingURL=config-C7PCYgYy.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as globalHookRegistry } from "./hooks-BDCMKeNq.js";
|
|
2
2
|
import { CucumberExpression, ParameterType, ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
|
|
3
3
|
//#region src/runtime/engine.ts
|
|
4
4
|
const keywordToStepType = {
|
|
@@ -112,19 +112,23 @@ async function runScenario(scenario, compiled, makeWorld, hooks = globalHookRegi
|
|
|
112
112
|
});
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
|
+
let source;
|
|
115
116
|
try {
|
|
116
117
|
const { step: def, args } = matchStep(step, compiled);
|
|
118
|
+
source = def.source;
|
|
117
119
|
await def.execute(world, args);
|
|
118
120
|
steps.push({
|
|
119
121
|
step,
|
|
120
|
-
status: "passed"
|
|
122
|
+
status: "passed",
|
|
123
|
+
source
|
|
121
124
|
});
|
|
122
125
|
} catch (err) {
|
|
123
126
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
124
127
|
steps.push({
|
|
125
128
|
step,
|
|
126
129
|
status: "failed",
|
|
127
|
-
error
|
|
130
|
+
error,
|
|
131
|
+
source
|
|
128
132
|
});
|
|
129
133
|
fail(error);
|
|
130
134
|
}
|
|
@@ -137,10 +141,6 @@ async function runScenario(scenario, compiled, makeWorld, hooks = globalHookRegi
|
|
|
137
141
|
} catch (err) {
|
|
138
142
|
fail(err);
|
|
139
143
|
}
|
|
140
|
-
if (failed && firstError) {
|
|
141
|
-
const failing = steps.find((s) => s.status === "failed");
|
|
142
|
-
if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
|
|
143
|
-
}
|
|
144
144
|
return {
|
|
145
145
|
scenario,
|
|
146
146
|
status: failed ? "failed" : "passed",
|
|
@@ -157,25 +157,7 @@ async function runScenario(scenario, compiled, makeWorld, hooks = globalHookRegi
|
|
|
157
157
|
function now() {
|
|
158
158
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
159
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
160
|
//#endregion
|
|
179
161
|
export { runScenario as i, UndefinedStepError as n, compileRegistry as r, AmbiguousStepError as t };
|
|
180
162
|
|
|
181
|
-
//# sourceMappingURL=engine-
|
|
163
|
+
//# sourceMappingURL=engine-DPVLEHBi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-DPVLEHBi.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 /**\n * Absolute `file:line:column` where the matched step is *defined* (its\n * `.step(...)` call site), for Cucumber-style reporting. Absent when no step\n * matched (undefined/ambiguous) or the step was skipped.\n */\n source?: string;\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`; for a hook failure there's no step to point at, so\n * this is the only place it surfaces. Reporters read this; the runner never\n * 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 // `source` is captured before `execute` so a failing step still carries its\n // definition location; it stays undefined if matching itself throws\n // (undefined/ambiguous step).\n let source: string | undefined;\n try {\n const { step: def, args } = matchStep(step, compiled);\n source = def.source;\n await def.execute(world, args);\n steps.push({ step, status: \"passed\", source });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error, source });\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 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"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_hooks = require("./hooks-
|
|
1
|
+
const require_hooks = require("./hooks-Be0cjULN.cjs");
|
|
2
2
|
let _cucumber_cucumber_expressions = require("@cucumber/cucumber-expressions");
|
|
3
3
|
//#region src/runtime/engine.ts
|
|
4
4
|
const keywordToStepType = {
|
|
@@ -112,19 +112,23 @@ async function runScenario(scenario, compiled, makeWorld, hooks = require_hooks.
|
|
|
112
112
|
});
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
|
+
let source;
|
|
115
116
|
try {
|
|
116
117
|
const { step: def, args } = matchStep(step, compiled);
|
|
118
|
+
source = def.source;
|
|
117
119
|
await def.execute(world, args);
|
|
118
120
|
steps.push({
|
|
119
121
|
step,
|
|
120
|
-
status: "passed"
|
|
122
|
+
status: "passed",
|
|
123
|
+
source
|
|
121
124
|
});
|
|
122
125
|
} catch (err) {
|
|
123
126
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
124
127
|
steps.push({
|
|
125
128
|
step,
|
|
126
129
|
status: "failed",
|
|
127
|
-
error
|
|
130
|
+
error,
|
|
131
|
+
source
|
|
128
132
|
});
|
|
129
133
|
fail(error);
|
|
130
134
|
}
|
|
@@ -137,10 +141,6 @@ async function runScenario(scenario, compiled, makeWorld, hooks = require_hooks.
|
|
|
137
141
|
} catch (err) {
|
|
138
142
|
fail(err);
|
|
139
143
|
}
|
|
140
|
-
if (failed && firstError) {
|
|
141
|
-
const failing = steps.find((s) => s.status === "failed");
|
|
142
|
-
if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
|
|
143
|
-
}
|
|
144
144
|
return {
|
|
145
145
|
scenario,
|
|
146
146
|
status: failed ? "failed" : "passed",
|
|
@@ -157,24 +157,6 @@ async function runScenario(scenario, compiled, makeWorld, hooks = require_hooks.
|
|
|
157
157
|
function now() {
|
|
158
158
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
159
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
160
|
//#endregion
|
|
179
161
|
Object.defineProperty(exports, "AmbiguousStepError", {
|
|
180
162
|
enumerable: true,
|
|
@@ -201,4 +183,4 @@ Object.defineProperty(exports, "runScenario", {
|
|
|
201
183
|
}
|
|
202
184
|
});
|
|
203
185
|
|
|
204
|
-
//# sourceMappingURL=engine-
|
|
186
|
+
//# sourceMappingURL=engine-vqA-eL_T.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-vqA-eL_T.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 /**\n * Absolute `file:line:column` where the matched step is *defined* (its\n * `.step(...)` call site), for Cucumber-style reporting. Absent when no step\n * matched (undefined/ambiguous) or the step was skipped.\n */\n source?: string;\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`; for a hook failure there's no step to point at, so\n * this is the only place it surfaces. Reporters read this; the runner never\n * 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 // `source` is captured before `execute` so a failing step still carries its\n // definition location; it stays undefined if matching itself throws\n // (undefined/ambiguous step).\n let source: string | undefined;\n try {\n const { step: def, args } = matchStep(step, compiled);\n source = def.source;\n await def.execute(world, args);\n steps.push({ step, status: \"passed\", source });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error, source });\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 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"],"mappings":"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"}
|
|
@@ -22,14 +22,83 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
//#endregion
|
|
23
23
|
let lodash = require("lodash");
|
|
24
24
|
lodash = __toESM(lodash, 1);
|
|
25
|
-
let node_fs_promises = require("node:fs/promises");
|
|
26
25
|
let node_path = require("node:path");
|
|
27
26
|
node_path = __toESM(node_path, 1);
|
|
27
|
+
let node_url = require("node:url");
|
|
28
|
+
let node_fs_promises = require("node:fs/promises");
|
|
28
29
|
let node_fs = require("node:fs");
|
|
29
30
|
node_fs = __toESM(node_fs, 1);
|
|
30
31
|
let _cucumber_gherkin = require("@cucumber/gherkin");
|
|
31
32
|
let _cucumber_messages = require("@cucumber/messages");
|
|
32
33
|
_cucumber_messages = __toESM(_cucumber_messages, 1);
|
|
34
|
+
//#region src/sourceLocation.ts
|
|
35
|
+
/**
|
|
36
|
+
* Directory holding the library's own compiled code. In development this is the
|
|
37
|
+
* `src/` tree; in the published package it's `dist/` (this module is bundled
|
|
38
|
+
* into the shipped chunks). Stack frames under it are internal plumbing —
|
|
39
|
+
* builders, the engine, the runner — and are hidden from users so that a
|
|
40
|
+
* failure points at *their* step, not ours.
|
|
41
|
+
*/
|
|
42
|
+
const LIB_ROOT = node_path.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
43
|
+
/**
|
|
44
|
+
* A frame belongs to user code if it's an absolute path that is neither inside
|
|
45
|
+
* the library nor inside any `node_modules` (assertion libs, etc.). That leaves
|
|
46
|
+
* exactly the frames a user cares about: their own step definitions.
|
|
47
|
+
*/
|
|
48
|
+
function isUserFile(file) {
|
|
49
|
+
return node_path.isAbsolute(file) && !file.startsWith(LIB_ROOT + node_path.sep) && !file.includes(`${node_path.sep}node_modules${node_path.sep}`);
|
|
50
|
+
}
|
|
51
|
+
/** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */
|
|
52
|
+
function parseFrame(frameLine) {
|
|
53
|
+
const m = /:(\d+):(\d+)\)?\s*$/.exec(frameLine);
|
|
54
|
+
if (!m) return void 0;
|
|
55
|
+
let file = frameLine.slice(0, m.index);
|
|
56
|
+
const paren = file.lastIndexOf("(");
|
|
57
|
+
if (paren !== -1) file = file.slice(paren + 1);
|
|
58
|
+
file = file.trim().replace(/^at\s+/, "");
|
|
59
|
+
if (file.startsWith("file://")) try {
|
|
60
|
+
file = (0, node_url.fileURLToPath)(file);
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
file,
|
|
66
|
+
line: Number(m[1]),
|
|
67
|
+
column: Number(m[2])
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Every user-code frame in a stack, nearest-first, internals removed. */
|
|
71
|
+
function userFrames(stack) {
|
|
72
|
+
if (!stack) return [];
|
|
73
|
+
const frames = [];
|
|
74
|
+
for (const line of stack.split("\n")) {
|
|
75
|
+
const frame = parseFrame(line);
|
|
76
|
+
if (frame && isUserFile(frame.file)) frames.push(frame);
|
|
77
|
+
}
|
|
78
|
+
return frames;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Capture the user source location of the current call site — the first
|
|
82
|
+
* user-code frame above this function. Called from `.step()` registration so
|
|
83
|
+
* each step remembers where it was defined (Cucumber-style), independent of
|
|
84
|
+
* where an error is later thrown. Returns an absolute `file:line:column`, or
|
|
85
|
+
* `undefined` if no user frame is visible.
|
|
86
|
+
*/
|
|
87
|
+
function captureDefinitionSite() {
|
|
88
|
+
const frame = userFrames((/* @__PURE__ */ new Error()).stack)[0];
|
|
89
|
+
return frame ? `${frame.file}:${frame.line}:${frame.column}` : void 0;
|
|
90
|
+
}
|
|
91
|
+
/** Render an absolute `file:line:column` relative to `cwd` for display. */
|
|
92
|
+
function relativeLocation(location, cwd) {
|
|
93
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(location);
|
|
94
|
+
if (!m) return location;
|
|
95
|
+
return `${node_path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;
|
|
96
|
+
}
|
|
97
|
+
/** Render a frame relative to `cwd`. */
|
|
98
|
+
function relativeFrame(frame, cwd) {
|
|
99
|
+
return `${node_path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
33
102
|
//#region src/world.ts
|
|
34
103
|
function mergeCustomizer(objValue, srcValue) {
|
|
35
104
|
if (lodash.default.isArray(objValue)) return objValue.concat(srcValue);
|
|
@@ -141,6 +210,7 @@ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
|
|
|
141
210
|
return [{
|
|
142
211
|
name: scenario.name,
|
|
143
212
|
file: filePath,
|
|
213
|
+
line: scenario.location.line,
|
|
144
214
|
steps: allSteps,
|
|
145
215
|
tags: scenarioTags
|
|
146
216
|
}];
|
|
@@ -165,6 +235,7 @@ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
|
|
|
165
235
|
results.push({
|
|
166
236
|
name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
|
|
167
237
|
file: filePath,
|
|
238
|
+
line: row.location.line,
|
|
168
239
|
steps: allSteps,
|
|
169
240
|
tags: exampleTags,
|
|
170
241
|
outline: { name: scenario.name }
|
|
@@ -221,6 +292,12 @@ Object.defineProperty(exports, "__toESM", {
|
|
|
221
292
|
return __toESM;
|
|
222
293
|
}
|
|
223
294
|
});
|
|
295
|
+
Object.defineProperty(exports, "captureDefinitionSite", {
|
|
296
|
+
enumerable: true,
|
|
297
|
+
get: function() {
|
|
298
|
+
return captureDefinitionSite;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
224
301
|
Object.defineProperty(exports, "globFiles", {
|
|
225
302
|
enumerable: true,
|
|
226
303
|
get: function() {
|
|
@@ -239,5 +316,23 @@ Object.defineProperty(exports, "parseFeatureFiles", {
|
|
|
239
316
|
return parseFeatureFiles;
|
|
240
317
|
}
|
|
241
318
|
});
|
|
319
|
+
Object.defineProperty(exports, "relativeFrame", {
|
|
320
|
+
enumerable: true,
|
|
321
|
+
get: function() {
|
|
322
|
+
return relativeFrame;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
Object.defineProperty(exports, "relativeLocation", {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: function() {
|
|
328
|
+
return relativeLocation;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
Object.defineProperty(exports, "userFrames", {
|
|
332
|
+
enumerable: true,
|
|
333
|
+
get: function() {
|
|
334
|
+
return userFrames;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
242
337
|
|
|
243
|
-
//# sourceMappingURL=gherkinParser-
|
|
338
|
+
//# sourceMappingURL=gherkinParser-BT40q_i3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gherkinParser-BT40q_i3.cjs","names":["path","_","path","fs","Parser","AstBuilder","messages","GherkinClassicTokenMatcher"],"sources":["../../src/sourceLocation.ts","../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Directory holding the library's own compiled code. In development this is the\n * `src/` tree; in the published package it's `dist/` (this module is bundled\n * into the shipped chunks). Stack frames under it are internal plumbing —\n * builders, the engine, the runner — and are hidden from users so that a\n * failure points at *their* step, not ours.\n */\nconst LIB_ROOT = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface SourceFrame {\n file: string;\n line: number;\n column: number;\n}\n\n/**\n * A frame belongs to user code if it's an absolute path that is neither inside\n * the library nor inside any `node_modules` (assertion libs, etc.). That leaves\n * exactly the frames a user cares about: their own step definitions.\n */\nfunction isUserFile(file: string): boolean {\n return (\n path.isAbsolute(file) &&\n !file.startsWith(LIB_ROOT + path.sep) &&\n !file.includes(`${path.sep}node_modules${path.sep}`)\n );\n}\n\n/** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */\nfunction parseFrame(frameLine: string): SourceFrame | undefined {\n const m = /:(\\d+):(\\d+)\\)?\\s*$/.exec(frameLine);\n if (!m) return undefined;\n let file = frameLine.slice(0, m.index);\n const paren = file.lastIndexOf(\"(\");\n if (paren !== -1) file = file.slice(paren + 1);\n file = file.trim().replace(/^at\\s+/, \"\");\n if (file.startsWith(\"file://\")) {\n try {\n file = fileURLToPath(file);\n } catch {\n return undefined;\n }\n }\n return { file, line: Number(m[1]), column: Number(m[2]) };\n}\n\n/** Every user-code frame in a stack, nearest-first, internals removed. */\nexport function userFrames(stack: string | undefined): SourceFrame[] {\n if (!stack) return [];\n const frames: SourceFrame[] = [];\n for (const line of stack.split(\"\\n\")) {\n const frame = parseFrame(line);\n if (frame && isUserFile(frame.file)) frames.push(frame);\n }\n return frames;\n}\n\n/**\n * Capture the user source location of the current call site — the first\n * user-code frame above this function. Called from `.step()` registration so\n * each step remembers where it was defined (Cucumber-style), independent of\n * where an error is later thrown. Returns an absolute `file:line:column`, or\n * `undefined` if no user frame is visible.\n */\nexport function captureDefinitionSite(): string | undefined {\n const frame = userFrames(new Error().stack)[0];\n return frame ? `${frame.file}:${frame.line}:${frame.column}` : undefined;\n}\n\n/** Render an absolute `file:line:column` relative to `cwd` for display. */\nexport function relativeLocation(location: string, cwd: string): string {\n const m = /^(.*):(\\d+):(\\d+)$/.exec(location);\n if (!m) return location;\n return `${path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;\n}\n\n/** Render a frame relative to `cwd`. */\nexport function relativeFrame(frame: SourceFrame, cwd: string): string {\n return `${path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;\n}\n","import _ from \"lodash\";\n\nexport type WorldState<State> = {\n readonly [K in keyof State]?: State[K];\n};\n\nexport type MergeableWorldState<T> = WorldState<T> & {\n merge: (newState: Partial<T>) => void;\n};\n\nfunction mergeCustomizer(objValue: unknown, srcValue: unknown) {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n } else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) {\n throw new Error(\n `Merge would have destroyed previous value ${objValue} with ${srcValue}`\n );\n }\n return objValue;\n}\n\nexport const createMergeableState = <T>(\n state: WorldState<T>\n): MergeableWorldState<T> => {\n return {\n ...state,\n merge: (newState: Partial<T>) => {\n state = _.merge({ ...state }, newState, mergeCustomizer);\n },\n };\n};\n\nexport type MergeableWorld<Given, When, Then> = {\n given: MergeableWorldState<Given>;\n when: MergeableWorldState<When>;\n then: MergeableWorldState<Then>;\n};\n\nexport class BasicWorld<Given, When, Then> {\n private givenState: WorldState<Given> = {};\n private whenState: WorldState<When> = {};\n private thenState: WorldState<Then> = {};\n\n public get given(): MergeableWorldState<Given> {\n return {\n ...this.givenState,\n merge: (newState: Partial<Given>) => {\n this.givenState = _.merge(\n { ...this.givenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get when(): MergeableWorldState<When> {\n return {\n ...this.whenState,\n merge: (newState: Partial<When>) => {\n this.whenState = _.merge(\n { ...this.whenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get then(): MergeableWorldState<Then> {\n return {\n ...this.thenState,\n merge: (newState: Partial<Then>) => {\n this.thenState = _.merge(\n { ...this.thenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n}\n\nexport const createBasicWorld = <Given, When, Then>(): MergeableWorld<\n Given,\n When,\n Then\n> => {\n return new BasicWorld<Given, When, Then>();\n};\n","import { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import * as fs from \"node:fs\";\nimport { GherkinClassicTokenMatcher, Parser, AstBuilder } from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some((e) => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n line: scenario.location.line,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map((c) => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map((c) => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map((step) => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n line: row.location.line,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map((step) => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map((step) => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,78 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
|
-
import { glob, stat } from "node:fs/promises";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { glob, stat } from "node:fs/promises";
|
|
4
5
|
import * as fs from "node:fs";
|
|
5
6
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
6
7
|
import * as messages from "@cucumber/messages";
|
|
8
|
+
//#region src/sourceLocation.ts
|
|
9
|
+
/**
|
|
10
|
+
* Directory holding the library's own compiled code. In development this is the
|
|
11
|
+
* `src/` tree; in the published package it's `dist/` (this module is bundled
|
|
12
|
+
* into the shipped chunks). Stack frames under it are internal plumbing —
|
|
13
|
+
* builders, the engine, the runner — and are hidden from users so that a
|
|
14
|
+
* failure points at *their* step, not ours.
|
|
15
|
+
*/
|
|
16
|
+
const LIB_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
/**
|
|
18
|
+
* A frame belongs to user code if it's an absolute path that is neither inside
|
|
19
|
+
* the library nor inside any `node_modules` (assertion libs, etc.). That leaves
|
|
20
|
+
* exactly the frames a user cares about: their own step definitions.
|
|
21
|
+
*/
|
|
22
|
+
function isUserFile(file) {
|
|
23
|
+
return path.isAbsolute(file) && !file.startsWith(LIB_ROOT + path.sep) && !file.includes(`${path.sep}node_modules${path.sep}`);
|
|
24
|
+
}
|
|
25
|
+
/** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */
|
|
26
|
+
function parseFrame(frameLine) {
|
|
27
|
+
const m = /:(\d+):(\d+)\)?\s*$/.exec(frameLine);
|
|
28
|
+
if (!m) return void 0;
|
|
29
|
+
let file = frameLine.slice(0, m.index);
|
|
30
|
+
const paren = file.lastIndexOf("(");
|
|
31
|
+
if (paren !== -1) file = file.slice(paren + 1);
|
|
32
|
+
file = file.trim().replace(/^at\s+/, "");
|
|
33
|
+
if (file.startsWith("file://")) try {
|
|
34
|
+
file = fileURLToPath(file);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
file,
|
|
40
|
+
line: Number(m[1]),
|
|
41
|
+
column: Number(m[2])
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Every user-code frame in a stack, nearest-first, internals removed. */
|
|
45
|
+
function userFrames(stack) {
|
|
46
|
+
if (!stack) return [];
|
|
47
|
+
const frames = [];
|
|
48
|
+
for (const line of stack.split("\n")) {
|
|
49
|
+
const frame = parseFrame(line);
|
|
50
|
+
if (frame && isUserFile(frame.file)) frames.push(frame);
|
|
51
|
+
}
|
|
52
|
+
return frames;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Capture the user source location of the current call site — the first
|
|
56
|
+
* user-code frame above this function. Called from `.step()` registration so
|
|
57
|
+
* each step remembers where it was defined (Cucumber-style), independent of
|
|
58
|
+
* where an error is later thrown. Returns an absolute `file:line:column`, or
|
|
59
|
+
* `undefined` if no user frame is visible.
|
|
60
|
+
*/
|
|
61
|
+
function captureDefinitionSite() {
|
|
62
|
+
const frame = userFrames((/* @__PURE__ */ new Error()).stack)[0];
|
|
63
|
+
return frame ? `${frame.file}:${frame.line}:${frame.column}` : void 0;
|
|
64
|
+
}
|
|
65
|
+
/** Render an absolute `file:line:column` relative to `cwd` for display. */
|
|
66
|
+
function relativeLocation(location, cwd) {
|
|
67
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(location);
|
|
68
|
+
if (!m) return location;
|
|
69
|
+
return `${path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;
|
|
70
|
+
}
|
|
71
|
+
/** Render a frame relative to `cwd`. */
|
|
72
|
+
function relativeFrame(frame, cwd) {
|
|
73
|
+
return `${path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
7
76
|
//#region src/world.ts
|
|
8
77
|
function mergeCustomizer(objValue, srcValue) {
|
|
9
78
|
if (_.isArray(objValue)) return objValue.concat(srcValue);
|
|
@@ -115,6 +184,7 @@ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
|
|
|
115
184
|
return [{
|
|
116
185
|
name: scenario.name,
|
|
117
186
|
file: filePath,
|
|
187
|
+
line: scenario.location.line,
|
|
118
188
|
steps: allSteps,
|
|
119
189
|
tags: scenarioTags
|
|
120
190
|
}];
|
|
@@ -139,6 +209,7 @@ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
|
|
|
139
209
|
results.push({
|
|
140
210
|
name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
|
|
141
211
|
file: filePath,
|
|
212
|
+
line: row.location.line,
|
|
142
213
|
steps: allSteps,
|
|
143
214
|
tags: exampleTags,
|
|
144
215
|
outline: { name: scenario.name }
|
|
@@ -183,6 +254,6 @@ function substituteExampleValues(text, substitution) {
|
|
|
183
254
|
return result;
|
|
184
255
|
}
|
|
185
256
|
//#endregion
|
|
186
|
-
export { BasicWorld as i, parseFeatureFiles as n, globFiles as r, parseFeatureContent as t };
|
|
257
|
+
export { captureDefinitionSite as a, userFrames as c, BasicWorld as i, parseFeatureFiles as n, relativeFrame as o, globFiles as r, relativeLocation as s, parseFeatureContent as t };
|
|
187
258
|
|
|
188
|
-
//# sourceMappingURL=gherkinParser-
|
|
259
|
+
//# sourceMappingURL=gherkinParser-NcttZgN4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gherkinParser-NcttZgN4.js","names":[],"sources":["../../src/sourceLocation.ts","../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Directory holding the library's own compiled code. In development this is the\n * `src/` tree; in the published package it's `dist/` (this module is bundled\n * into the shipped chunks). Stack frames under it are internal plumbing —\n * builders, the engine, the runner — and are hidden from users so that a\n * failure points at *their* step, not ours.\n */\nconst LIB_ROOT = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface SourceFrame {\n file: string;\n line: number;\n column: number;\n}\n\n/**\n * A frame belongs to user code if it's an absolute path that is neither inside\n * the library nor inside any `node_modules` (assertion libs, etc.). That leaves\n * exactly the frames a user cares about: their own step definitions.\n */\nfunction isUserFile(file: string): boolean {\n return (\n path.isAbsolute(file) &&\n !file.startsWith(LIB_ROOT + path.sep) &&\n !file.includes(`${path.sep}node_modules${path.sep}`)\n );\n}\n\n/** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */\nfunction parseFrame(frameLine: string): SourceFrame | undefined {\n const m = /:(\\d+):(\\d+)\\)?\\s*$/.exec(frameLine);\n if (!m) return undefined;\n let file = frameLine.slice(0, m.index);\n const paren = file.lastIndexOf(\"(\");\n if (paren !== -1) file = file.slice(paren + 1);\n file = file.trim().replace(/^at\\s+/, \"\");\n if (file.startsWith(\"file://\")) {\n try {\n file = fileURLToPath(file);\n } catch {\n return undefined;\n }\n }\n return { file, line: Number(m[1]), column: Number(m[2]) };\n}\n\n/** Every user-code frame in a stack, nearest-first, internals removed. */\nexport function userFrames(stack: string | undefined): SourceFrame[] {\n if (!stack) return [];\n const frames: SourceFrame[] = [];\n for (const line of stack.split(\"\\n\")) {\n const frame = parseFrame(line);\n if (frame && isUserFile(frame.file)) frames.push(frame);\n }\n return frames;\n}\n\n/**\n * Capture the user source location of the current call site — the first\n * user-code frame above this function. Called from `.step()` registration so\n * each step remembers where it was defined (Cucumber-style), independent of\n * where an error is later thrown. Returns an absolute `file:line:column`, or\n * `undefined` if no user frame is visible.\n */\nexport function captureDefinitionSite(): string | undefined {\n const frame = userFrames(new Error().stack)[0];\n return frame ? `${frame.file}:${frame.line}:${frame.column}` : undefined;\n}\n\n/** Render an absolute `file:line:column` relative to `cwd` for display. */\nexport function relativeLocation(location: string, cwd: string): string {\n const m = /^(.*):(\\d+):(\\d+)$/.exec(location);\n if (!m) return location;\n return `${path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;\n}\n\n/** Render a frame relative to `cwd`. */\nexport function relativeFrame(frame: SourceFrame, cwd: string): string {\n return `${path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;\n}\n","import _ from \"lodash\";\n\nexport type WorldState<State> = {\n readonly [K in keyof State]?: State[K];\n};\n\nexport type MergeableWorldState<T> = WorldState<T> & {\n merge: (newState: Partial<T>) => void;\n};\n\nfunction mergeCustomizer(objValue: unknown, srcValue: unknown) {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n } else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) {\n throw new Error(\n `Merge would have destroyed previous value ${objValue} with ${srcValue}`\n );\n }\n return objValue;\n}\n\nexport const createMergeableState = <T>(\n state: WorldState<T>\n): MergeableWorldState<T> => {\n return {\n ...state,\n merge: (newState: Partial<T>) => {\n state = _.merge({ ...state }, newState, mergeCustomizer);\n },\n };\n};\n\nexport type MergeableWorld<Given, When, Then> = {\n given: MergeableWorldState<Given>;\n when: MergeableWorldState<When>;\n then: MergeableWorldState<Then>;\n};\n\nexport class BasicWorld<Given, When, Then> {\n private givenState: WorldState<Given> = {};\n private whenState: WorldState<When> = {};\n private thenState: WorldState<Then> = {};\n\n public get given(): MergeableWorldState<Given> {\n return {\n ...this.givenState,\n merge: (newState: Partial<Given>) => {\n this.givenState = _.merge(\n { ...this.givenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get when(): MergeableWorldState<When> {\n return {\n ...this.whenState,\n merge: (newState: Partial<When>) => {\n this.whenState = _.merge(\n { ...this.whenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get then(): MergeableWorldState<Then> {\n return {\n ...this.thenState,\n merge: (newState: Partial<Then>) => {\n this.thenState = _.merge(\n { ...this.thenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n}\n\nexport const createBasicWorld = <Given, When, Then>(): MergeableWorld<\n Given,\n When,\n Then\n> => {\n return new BasicWorld<Given, When, Then>();\n};\n","import { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import * as fs from \"node:fs\";\nimport { GherkinClassicTokenMatcher, Parser, AstBuilder } from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some((e) => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n line: scenario.location.line,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map((c) => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map((c) => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map((step) => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n line: row.location.line,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map((step) => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map((step) => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAUA,MAAM,WAAW,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC;;;;;;AAa5D,SAAS,WAAW,MAAuB;CACzC,OACE,KAAK,WAAW,IAAI,KACpB,CAAC,KAAK,WAAW,WAAW,KAAK,GAAG,KACpC,CAAC,KAAK,SAAS,GAAG,KAAK,IAAI,cAAc,KAAK,KAAK;AAEvD;;AAGA,SAAS,WAAW,WAA4C;CAC9D,MAAM,IAAI,sBAAsB,KAAK,SAAS;CAC9C,IAAI,CAAC,GAAG,OAAO,KAAA;CACf,IAAI,OAAO,UAAU,MAAM,GAAG,EAAE,KAAK;CACrC,MAAM,QAAQ,KAAK,YAAY,GAAG;CAClC,IAAI,UAAU,IAAI,OAAO,KAAK,MAAM,QAAQ,CAAC;CAC7C,OAAO,KAAK,KAAK,CAAC,CAAC,QAAQ,UAAU,EAAE;CACvC,IAAI,KAAK,WAAW,SAAS,GAC3B,IAAI;EACF,OAAO,cAAc,IAAI;CAC3B,QAAQ;EACN;CACF;CAEF,OAAO;EAAE;EAAM,MAAM,OAAO,EAAE,EAAE;EAAG,QAAQ,OAAO,EAAE,EAAE;CAAE;AAC1D;;AAGA,SAAgB,WAAW,OAA0C;CACnE,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,MAAM,SAAwB,CAAC;CAC/B,KAAK,MAAM,QAAQ,MAAM,MAAM,IAAI,GAAG;EACpC,MAAM,QAAQ,WAAW,IAAI;EAC7B,IAAI,SAAS,WAAW,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK;CACxD;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,wBAA4C;CAC1D,MAAM,QAAQ,4BAAW,IAAI,MAAM,EAAA,CAAE,KAAK,CAAC,CAAC;CAC5C,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG,MAAM,KAAK,GAAG,MAAM,WAAW,KAAA;AACjE;;AAGA,SAAgB,iBAAiB,UAAkB,KAAqB;CACtE,MAAM,IAAI,qBAAqB,KAAK,QAAQ;CAC5C,IAAI,CAAC,GAAG,OAAO;CACf,OAAO,GAAG,KAAK,SAAS,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE;AAClD;;AAGA,SAAgB,cAAc,OAAoB,KAAqB;CACrE,OAAO,GAAG,KAAK,SAAS,KAAK,MAAM,IAAI,EAAE,GAAG,MAAM,KAAK,GAAG,MAAM;AAClE;;;ACxEA,SAAS,gBAAgB,UAAmB,UAAmB;CAC7D,IAAI,EAAE,QAAQ,QAAQ,GACpB,OAAO,SAAS,OAAO,QAAQ;MAC1B,IAAI,YAAY,CAAC,EAAE,cAAc,QAAQ,KAAK,aAAa,UAChE,MAAM,IAAI,MACR,6CAA6C,SAAS,QAAQ,UAChE;CAEF,OAAO;AACT;AAmBA,IAAa,aAAb,MAA2C;CACzC,aAAwC,CAAC;CACzC,YAAsC,CAAC;CACvC,YAAsC,CAAC;CAEvC,IAAW,QAAoC;EAC7C,OAAO;GACL,GAAG,KAAK;GACR,QAAQ,aAA6B;IACnC,KAAK,aAAa,EAAE,MAClB,EAAE,GAAG,KAAK,WAAW,GACrB,UACA,eACF;GACF;EACF;CACF;CAEA,IAAW,OAAkC;EAC3C,OAAO;GACL,GAAG,KAAK;GACR,QAAQ,aAA4B;IAClC,KAAK,YAAY,EAAE,MACjB,EAAE,GAAG,KAAK,UAAU,GACpB,UACA,eACF;GACF;EACF;CACF;CAEA,IAAW,OAAkC;EAC3C,OAAO;GACL,GAAG,KAAK;GACR,QAAQ,aAA4B;IAClC,KAAK,YAAY,EAAE,MACjB,EAAE,GAAG,KAAK,UAAU,GACpB,UACA,eACF;GACF;EACF;CACF;AACF;;;;AC5EA,MAAM,QAAQ;AAEd,eAAe,OAAO,GAA6B;CACjD,IAAI;EACF,QAAQ,MAAM,KAAK,CAAC,EAAA,CAAG,OAAO;CAChC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;AAaA,eAAsB,UACpB,UACA,MAAc,QAAQ,IAAI,GACP;CACnB,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,KAAK,WAAW,OAAO,KAAK,CAAC,MAAM,KAAK,OAAO,GAAG;GACpD,IAAI,MAAM,OAAO,OAAO,GAAG,MAAM,IAAI,OAAO;GAC5C;EACF;EACA,WAAW,MAAM,SAAS,KAAK,SAAS,EAAE,IAAI,CAAC,GAC7C,MAAM,IAAI,KAAK,QAAQ,KAAK,KAAK,CAAC;CAEtC;CACA,OAAO,CAAC,GAAG,KAAK;AAClB;;;AClCA,SAAgB,kBAAkB,WAAuC;CACvE,MAAM,YAA8B,CAAC;CAErC,KAAK,MAAM,YAAY,WAAW;EAEhC,MAAM,SAAS,oBADC,GAAG,aAAa,UAAU,OACD,GAAG,QAAQ;EACpD,UAAU,KAAK,GAAG,MAAM;CAC1B;CAEA,OAAO;AACT;AAEA,SAAgB,oBACd,SACA,UACkB;CAOlB,MAAM,UAD4C,IAF/B,OAAO,IAFN,WADN,SAAS,YAAY,KACA,CAEH,GAAG,IADf,2BACqB,CAEc,CAAC,CAAC,MAAM,OACjC,CAAC,CAAC;CAChC,IAAI,CAAC,SAAS,OAAO,CAAC;CAGtB,MAAM,oBAAqC,CAAC;CAC5C,MAAM,YAA8B,CAAC;CACrC,MAAM,cAAc,SAAS,QAAQ,IAAI;CAEzC,KAAK,MAAM,SAAS,QAAQ,UAAU;EACpC,IAAI,MAAM,YACR,kBAAkB,KAAK,GAAG,MAAM,WAAW,KAAK;EAGlD,IAAI,MAAM,UACR,UAAU,KACR,GAAG,eACD,MAAM,UACN,mBACA,UACA,WACF,CACF;EAGF,IAAI,MAAM,MAAM;GAGd,MAAM,iBAAkC,CAAC,GAAG,iBAAiB;GAC7D,MAAM,WAAW,CAAC,GAAG,aAAa,GAAG,SAAS,MAAM,KAAK,IAAI,CAAC;GAC9D,KAAK,MAAM,aAAa,MAAM,KAAK,UAAU;IAC3C,IAAI,UAAU,YACZ,eAAe,KAAK,GAAG,UAAU,WAAW,KAAK;IAEnD,IAAI,UAAU,UACZ,UAAU,KACR,GAAG,eACD,UAAU,UACV,gBACA,UACA,QACF,CACF;GAEJ;EACF;CACF;CAEA,OAAO;AACT;;AAGA,SAAS,SAAS,MAAqD;CACrE,OAAO,CAAC,GAAG,IAAI,KAAK,QAAQ,CAAC,EAAA,CAAG,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC;AACnD;AAEA,SAAS,eACP,UACA,iBACA,UACA,eACkB;CAClB,MAAM,eAAe,CAAC,GAAG,eAAe,GAAG,SAAS,SAAS,IAAI,CAAC;CAKlE,IAAI,EAHF,SAAS,SAAS,SAAS,KAC3B,SAAS,SAAS,MAAM,MAAM,EAAE,UAAU,SAAS,CAAC,IAEpC;EAEhB,MAAM,WAAW,aAAa,eAAe;EAC7C,MAAM,iBAAiB,aAAa,SAAS,KAAK;EAClD,MAAM,WAAW,yBAAyB,CAAC,GAAG,UAAU,GAAG,cAAc,CAAC;EAE1E,OAAO,CACL;GACE,MAAM,SAAS;GACf,MAAM;GACN,MAAM,SAAS,SAAS;GACxB,OAAO;GACP,MAAM;EACR,CACF;CACF;CAIA,MAAM,UAA4B,CAAC;CACnC,KAAK,MAAM,WAAW,SAAS,UAAU;EACvC,IAAI,CAAC,QAAQ,eAAe,QAAQ,UAAU,WAAW,GAAG;EAC5D,MAAM,UAAU,QAAQ,YAAY,MAAM,KAAK,MAAM,EAAE,KAAK;EAC5D,MAAM,cAAc,CAAC,GAAG,cAAc,GAAG,SAAS,QAAQ,IAAI,CAAC;EAE/D,KAAK,MAAM,OAAO,QAAQ,WAAW;GACnC,MAAM,SAAS,IAAI,MAAM,KAAK,MAAM,EAAE,KAAK;GAC3C,MAAM,eAAuC,CAAC;GAC9C,QAAQ,SAAS,GAAG,MAAM;IACxB,aAAa,KAAK,OAAO;GAC3B,CAAC;GAED,MAAM,WAAW,aAAa,eAAe;GAC7C,MAAM,gBAAgB,aAAa,SAAS,KAAK,CAAC,CAAC,KAAK,UAAU;IAChE,GAAG;IACH,MAAM,wBAAwB,KAAK,MAAM,YAAY;GACvD,EAAE;GACF,MAAM,WAAW,yBAAyB,CAAC,GAAG,UAAU,GAAG,aAAa,CAAC;GAEzE,QAAQ,KAAK;IACX,MAAM,QAAQ,KAAK,GAAG,MAAM,GAAG,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI;IAC1D,MAAM;IACN,MAAM,IAAI,SAAS;IACnB,OAAO;IACP,MAAM;IACN,SAAS,EAAE,MAAM,SAAS,KAAK;GACjC,CAAC;EACH;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aACP,OACwC;CACxC,OAAO,MAAM,KAAK,UAAU;EAC1B,SAAS,iBAAiB,KAAK,OAAO;EACtC,MAAM,KAAK;EACX,MAAM,KAAK,SAAS;EAKpB,SAAS,KAAK,SAAS,UAAU,KAAK,KAAK,QAAQ;CACrD,EAAE;AACJ;AAEA,SAAS,iBAAiB,SAAiC;CACzD,MAAM,UAAU,QAAQ,KAAK;CAE7B,IAAI,YAAY,SAAS,OAAO;CAChC,IAAI,YAAY,QAAQ,OAAO;CAC/B,IAAI,YAAY,QAAQ,OAAO;CAC/B,IAAI,YAAY,OAAO,OAAO;CAC9B,IAAI,YAAY,OAAO,OAAO;CAE9B,OAAO;AACT;AAEA,SAAS,yBACP,OACc;CACd,IAAI,gBAA2C;CAE/C,OAAO,MAAM,KAAK,SAAS;EACzB,IAAI;EACJ,IAAI,KAAK,YAAY,SAAS,KAAK,YAAY,OAC7C,mBAAmB;OAEnB,mBAAmB,KAAK;EAE1B,gBAAgB;EAEhB,OAAO;GACL,GAAG;GACH;EACF;CACF,CAAC;AACH;AAEA,SAAS,wBACP,MACA,cACQ;CACR,IAAI,SAAS;CACb,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,YAAY,GACpD,SAAS,OAAO,QAAQ,IAAI,OAAO,IAAI,IAAI,IAAI,GAAG,GAAG,KAAK;CAE5D,OAAO;AACT"}
|