@step-forge/step-forge 0.0.19 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/{analyzer-QH03uejK.js → analyzer-DJyJbU_V.js} +4 -105
- package/dist/analyzer-DJyJbU_V.js.map +1 -0
- package/dist/analyzer-cli.js +1 -1
- package/dist/analyzer-cli.js.map +1 -1
- package/dist/analyzer.d.ts +14 -0
- package/dist/analyzer.js +2 -1
- package/dist/gherkinParser-Dp2d7JNr.js +116 -0
- package/dist/gherkinParser-Dp2d7JNr.js.map +1 -0
- package/dist/hooks-CGYzwDOv.cjs +117 -0
- package/dist/hooks-CGYzwDOv.cjs.map +1 -0
- package/dist/hooks-CywugMQQ.js +82 -0
- package/dist/hooks-CywugMQQ.js.map +1 -0
- package/dist/hooks-Dar49TtT.d.cts +189 -0
- package/dist/hooks-Dar49TtT.d.ts +189 -0
- package/dist/runtime.cjs +168 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +69 -0
- package/dist/runtime.d.ts +69 -0
- package/dist/runtime.js +159 -0
- package/dist/runtime.js.map +1 -0
- package/dist/step-forge.cjs +193 -134
- package/dist/step-forge.cjs.map +1 -1
- package/dist/step-forge.d.cts +187 -727
- package/dist/step-forge.d.ts +187 -727
- package/dist/step-forge.js +188 -135
- package/dist/step-forge.js.map +1 -1
- package/dist/vitest.d.ts +74 -0
- package/dist/vitest.js +136 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +26 -9
- package/dist/analyzer-QH03uejK.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks-CywugMQQ.js","names":[],"sources":["../../src/runtime/registry.ts","../../src/runtime/hooks.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Parser } from \"../parsers\";\nimport { MergeableWorld } from \"../world\";\n\nexport type StepType = \"given\" | \"when\" | \"then\";\n\n/**\n * A step definition as it exists at *runtime*, independent of any test runner.\n *\n * `execute` is the fully-wired step body: given a world and the raw values\n * captured from a Gherkin step, it applies parsers, validates + narrows\n * dependencies, runs the user's step function, and merges the result back into\n * the world. The engine never needs to know how any of that works — it just\n * matches text to a step and calls `execute`.\n */\nexport interface RegisteredStep {\n stepType: StepType;\n /** The Cucumber-expression source, e.g. `a user named {string}`. */\n expression: string;\n /** Parsers, one per captured variable, applied after expression capture. */\n parsers: Parser<any>[];\n execute: (\n world: MergeableWorld<any, any, any>,\n capturedArgs: unknown[]\n ) => Promise<void>;\n /** Where the step was defined, for ambiguous-match diagnostics. */\n source?: string;\n}\n\n/**\n * A collection of registered steps. Deliberately a plain instance (not a hidden\n * module global) so tests and the Vitest plugin can create isolated registries.\n * `globalRegistry` is the default sink that `.step()` writes to.\n */\nexport class StepRegistry {\n private steps: RegisteredStep[] = [];\n\n add(step: RegisteredStep): void {\n this.steps.push(step);\n }\n\n all(): readonly RegisteredStep[] {\n return this.steps;\n }\n\n clear(): void {\n this.steps = [];\n }\n}\n\nexport const globalRegistry = new StepRegistry();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MergeableWorld } from \"../world\";\n\n/**\n * Hooks are side-effect callbacks that run around scenarios, feature files, or\n * the whole run. Unlike steps they never seed state: state is owned end to end\n * by the typed dependency graph (given/when/then), and hooks exist for setup and\n * teardown (opening a DB, starting a server, resetting mocks). A scenario hook\n * may *read* the world, but its return value is ignored.\n */\nexport type HookScope = \"scenario\" | \"feature\" | \"global\";\nexport type HookTiming = \"before\" | \"after\";\n\n/** Lightweight scenario identity handed to per-scenario hooks. */\nexport interface ScenarioInfo {\n name: string;\n file: string;\n}\n\n/** A per-scenario hook: gets the scenario's world (read-only in spirit). */\nexport type ScenarioHookFn = (context: {\n world: MergeableWorld<any, any, any>;\n scenario: ScenarioInfo;\n}) => void | Promise<void>;\n\n/** A per-feature-file or global hook: no world exists at these boundaries. */\nexport type PlainHookFn = () => void | Promise<void>;\n\nexport interface RegisteredHook {\n scope: HookScope;\n timing: HookTiming;\n fn: ScenarioHookFn | PlainHookFn;\n}\n\n/**\n * Collection of registered hooks. Like {@link StepRegistry}, a plain instance\n * (not a hidden global) so tests can isolate; `globalHookRegistry` is the\n * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.\n */\nexport class HookRegistry {\n private hooks: RegisteredHook[] = [];\n\n add(hook: RegisteredHook): void {\n this.hooks.push(hook);\n }\n\n /**\n * Hooks for a scope+timing. `before` hooks run in registration order;\n * `after` hooks run in reverse (LIFO), so teardown unwinds setup.\n */\n for(scope: HookScope, timing: HookTiming): RegisteredHook[] {\n const matching = this.hooks.filter(\n h => h.scope === scope && h.timing === timing\n );\n return timing === \"after\" ? matching.reverse() : matching;\n }\n\n clear(): void {\n this.hooks = [];\n }\n}\n\nexport const globalHookRegistry = new HookRegistry();\n\n/**\n * Run every registered feature/global hook of a scope+timing in order. Used by\n * the generated test modules (feature hooks). Throws if a hook throws, so the\n * runner reports it against the enclosing boundary.\n */\nexport async function runHooks(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n for (const hook of registry.for(scope, timing)) {\n await (hook.fn as PlainHookFn)();\n }\n}\n\n// Process-global (via Symbol.for so it survives module duplication) guard so\n// global hooks fire exactly once per worker, no matter how many feature modules\n// call in.\nconst GLOBAL_GUARD = Symbol.for(\"step-forge.globalHooksStarted\");\n\n/**\n * Run global before-hooks once per worker, ahead of that worker's first\n * scenario, and schedule global after-hooks for worker exit. Idempotent: every\n * feature module calls this in a `beforeAll`, but only the first call in a given\n * worker does anything.\n *\n * Semantics & caveats (the once-per-worker model):\n * - Runs in the *same* realm as steps, so global setup may touch in-process\n * state that steps later read.\n * - \"Once per worker\", not strictly once per run — with multiple workers it runs\n * in each. Size global setup to be worker-safe (e.g. a server per worker).\n * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and\n * are not awaited by the runner, so keep them fast/synchronous.\n */\nexport async function ensureGlobalHooks(\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n const store = globalThis as Record<symbol, boolean>;\n if (store[GLOBAL_GUARD]) return;\n store[GLOBAL_GUARD] = true;\n\n for (const hook of registry.for(\"global\", \"before\")) {\n await (hook.fn as PlainHookFn)();\n }\n\n process.once(\"beforeExit\", () => {\n void (async () => {\n for (const hook of registry.for(\"global\", \"after\")) {\n await (hook.fn as PlainHookFn)();\n }\n })();\n });\n}\n"],"mappings":";;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;CAEA,MAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,iBAAiB,IAAI,aAAa;;;;;;;;ACX/C,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;;;;;CAMA,IAAI,OAAkB,QAAsC;EAC1D,MAAM,WAAW,KAAK,MAAM,QAC1B,MAAK,EAAE,UAAU,SAAS,EAAE,WAAW,MACzC;EACA,OAAO,WAAW,UAAU,SAAS,QAAQ,IAAI;CACnD;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,qBAAqB,IAAI,aAAa;;;;;;AAOnD,eAAsB,SACpB,OACA,QACA,WAAyB,oBACV;CACf,KAAK,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,GAC3C,MAAO,KAAK,GAAmB;AAEnC;AAKA,MAAM,eAAe,OAAO,IAAI,+BAA+B;;;;;;;;;;;;;;;AAgB/D,eAAsB,kBACpB,WAAyB,oBACV;CACf,MAAM,QAAQ;CACd,IAAI,MAAM,eAAe;CACzB,MAAM,gBAAgB;CAEtB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,QAAQ,GAChD,MAAO,KAAK,GAAmB;CAGjC,QAAQ,KAAK,oBAAoB;EAC/B,CAAM,YAAY;GAChB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,OAAO,GAC/C,MAAO,KAAK,GAAmB;EAEnC,EAAA,CAAG;CACL,CAAC;AACH"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//#region src/parsers.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A Parser is a typed cucumber-expression *parameter type*: it declares how a
|
|
4
|
+
* value is recognised in a Gherkin step (`regexp`) and how the matched text is
|
|
5
|
+
* turned into a TypeScript value (`parse`). The step expression uses `{name}`
|
|
6
|
+
* as the placeholder.
|
|
7
|
+
*
|
|
8
|
+
* The engine folds each parser straight into the cucumber-expression matcher —
|
|
9
|
+
* `parse` is the parameter type's transform, so coercion happens exactly once,
|
|
10
|
+
* during matching (no second pass). This is also what lets a parser introduce a
|
|
11
|
+
* brand-new placeholder like `{color}`: it is registered as a real parameter
|
|
12
|
+
* type, so text that doesn't match `regexp` simply doesn't match the step.
|
|
13
|
+
*
|
|
14
|
+
* The four built-ins below reuse cucumber-expressions' own built-in parameter
|
|
15
|
+
* types (`{int}`, `{float}`, `{string}`) — those names are already registered,
|
|
16
|
+
* so the library performs the match + coercion and each parser's `regexp`/
|
|
17
|
+
* `parse` serve to document intent and to drive TypeScript inference of the
|
|
18
|
+
* variable type. `booleanParser` has no built-in equivalent, so it is a genuine
|
|
19
|
+
* custom parameter type whose `parse` runs at match time.
|
|
20
|
+
*/
|
|
21
|
+
type Parser<T> = {
|
|
22
|
+
/** Parameter-type name; the step expression uses `{name}` as the placeholder. */name: string; /** How cucumber-expressions recognises the value in the step text. */
|
|
23
|
+
regexp: RegExp | RegExp[]; /** Transform the matched text into the typed value. */
|
|
24
|
+
parse: (value: string) => T;
|
|
25
|
+
};
|
|
26
|
+
/** Matches a quoted string (`{string}`) and strips the surrounding quotes. */
|
|
27
|
+
declare const stringParser: Parser<string>;
|
|
28
|
+
/** Matches an unquoted integer (`{int}`). */
|
|
29
|
+
declare const intParser: Parser<number>;
|
|
30
|
+
/** Matches an unquoted floating point number (`{float}`). */
|
|
31
|
+
declare const numberParser: Parser<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Matches an unquoted `true`/`false` word (`{boolean}`) and parses it to a
|
|
34
|
+
* boolean. Unlike the others this is a genuine custom parameter type — cucumber
|
|
35
|
+
* has no built-in `boolean` — so its `regexp`/`parse` are what the matcher uses.
|
|
36
|
+
*/
|
|
37
|
+
declare const booleanParser: Parser<boolean>;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/world.d.ts
|
|
40
|
+
type WorldState<State> = { readonly [K in keyof State]?: State[K] };
|
|
41
|
+
type MergeableWorldState<T> = WorldState<T> & {
|
|
42
|
+
merge: (newState: Partial<T>) => void;
|
|
43
|
+
};
|
|
44
|
+
type MergeableWorld<Given, When, Then> = {
|
|
45
|
+
given: MergeableWorldState<Given>;
|
|
46
|
+
when: MergeableWorldState<When>;
|
|
47
|
+
then: MergeableWorldState<Then>;
|
|
48
|
+
};
|
|
49
|
+
declare class BasicWorld<Given, When, Then> {
|
|
50
|
+
private givenState;
|
|
51
|
+
private whenState;
|
|
52
|
+
private thenState;
|
|
53
|
+
get given(): MergeableWorldState<Given>;
|
|
54
|
+
get when(): MergeableWorldState<When>;
|
|
55
|
+
get then(): MergeableWorldState<Then>;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/analyzer/types.d.ts
|
|
59
|
+
interface StepDefinitionMeta {
|
|
60
|
+
stepType: "given" | "when" | "then";
|
|
61
|
+
expression: string;
|
|
62
|
+
dependencies: {
|
|
63
|
+
given: Record<string, "required" | "optional">;
|
|
64
|
+
when: Record<string, "required" | "optional">;
|
|
65
|
+
then: Record<string, "required" | "optional">;
|
|
66
|
+
};
|
|
67
|
+
produces: string[];
|
|
68
|
+
sourceFile: string;
|
|
69
|
+
line: number;
|
|
70
|
+
}
|
|
71
|
+
interface ParsedScenario {
|
|
72
|
+
name: string;
|
|
73
|
+
file: string;
|
|
74
|
+
steps: ParsedStep[];
|
|
75
|
+
/**
|
|
76
|
+
* Gherkin tags in effect for this scenario, each including the leading `@`
|
|
77
|
+
* (feature + scenario tags, plus the Examples-block tags for outline rows).
|
|
78
|
+
* The Vitest plugin maps `@skip`/`@only` onto `test.skip`/`test.only`.
|
|
79
|
+
*/
|
|
80
|
+
tags: string[];
|
|
81
|
+
/**
|
|
82
|
+
* Present only for rows expanded from a `Scenario Outline`. Carries the base
|
|
83
|
+
* outline name so the plugin can group its rows under one `describe`, with
|
|
84
|
+
* each row a separate `test` labelled by its example values.
|
|
85
|
+
*/
|
|
86
|
+
outline?: {
|
|
87
|
+
name: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
interface ParsedStep {
|
|
91
|
+
keyword: "Given" | "When" | "Then" | "And" | "But";
|
|
92
|
+
effectiveKeyword: "Given" | "When" | "Then";
|
|
93
|
+
text: string;
|
|
94
|
+
line: number;
|
|
95
|
+
column: number;
|
|
96
|
+
}
|
|
97
|
+
interface MatchedStep extends ParsedStep {
|
|
98
|
+
definitions: StepDefinitionMeta[];
|
|
99
|
+
}
|
|
100
|
+
interface Diagnostic {
|
|
101
|
+
file: string;
|
|
102
|
+
range: {
|
|
103
|
+
startLine: number;
|
|
104
|
+
startColumn: number;
|
|
105
|
+
endLine: number;
|
|
106
|
+
endColumn: number;
|
|
107
|
+
};
|
|
108
|
+
severity: "error" | "warning" | "info";
|
|
109
|
+
message: string;
|
|
110
|
+
rule: string;
|
|
111
|
+
source: "step-forge";
|
|
112
|
+
}
|
|
113
|
+
interface AnalysisRule {
|
|
114
|
+
name: string;
|
|
115
|
+
check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[];
|
|
116
|
+
}
|
|
117
|
+
interface AnalyzerConfig {
|
|
118
|
+
stepFiles: string[];
|
|
119
|
+
featureFiles: string[];
|
|
120
|
+
tsConfigPath?: string;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/runtime/hooks.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Hooks are side-effect callbacks that run around scenarios, feature files, or
|
|
126
|
+
* the whole run. Unlike steps they never seed state: state is owned end to end
|
|
127
|
+
* by the typed dependency graph (given/when/then), and hooks exist for setup and
|
|
128
|
+
* teardown (opening a DB, starting a server, resetting mocks). A scenario hook
|
|
129
|
+
* may *read* the world, but its return value is ignored.
|
|
130
|
+
*/
|
|
131
|
+
type HookScope = "scenario" | "feature" | "global";
|
|
132
|
+
type HookTiming = "before" | "after";
|
|
133
|
+
/** Lightweight scenario identity handed to per-scenario hooks. */
|
|
134
|
+
interface ScenarioInfo {
|
|
135
|
+
name: string;
|
|
136
|
+
file: string;
|
|
137
|
+
}
|
|
138
|
+
/** A per-scenario hook: gets the scenario's world (read-only in spirit). */
|
|
139
|
+
type ScenarioHookFn = (context: {
|
|
140
|
+
world: MergeableWorld<any, any, any>;
|
|
141
|
+
scenario: ScenarioInfo;
|
|
142
|
+
}) => void | Promise<void>;
|
|
143
|
+
/** A per-feature-file or global hook: no world exists at these boundaries. */
|
|
144
|
+
type PlainHookFn = () => void | Promise<void>;
|
|
145
|
+
interface RegisteredHook {
|
|
146
|
+
scope: HookScope;
|
|
147
|
+
timing: HookTiming;
|
|
148
|
+
fn: ScenarioHookFn | PlainHookFn;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Collection of registered hooks. Like {@link StepRegistry}, a plain instance
|
|
152
|
+
* (not a hidden global) so tests can isolate; `globalHookRegistry` is the
|
|
153
|
+
* default sink the public `beforeScenario`/`afterAll`/etc helpers write to.
|
|
154
|
+
*/
|
|
155
|
+
declare class HookRegistry {
|
|
156
|
+
private hooks;
|
|
157
|
+
add(hook: RegisteredHook): void;
|
|
158
|
+
/**
|
|
159
|
+
* Hooks for a scope+timing. `before` hooks run in registration order;
|
|
160
|
+
* `after` hooks run in reverse (LIFO), so teardown unwinds setup.
|
|
161
|
+
*/
|
|
162
|
+
for(scope: HookScope, timing: HookTiming): RegisteredHook[];
|
|
163
|
+
clear(): void;
|
|
164
|
+
}
|
|
165
|
+
declare const globalHookRegistry: HookRegistry;
|
|
166
|
+
/**
|
|
167
|
+
* Run every registered feature/global hook of a scope+timing in order. Used by
|
|
168
|
+
* the generated test modules (feature hooks). Throws if a hook throws, so the
|
|
169
|
+
* runner reports it against the enclosing boundary.
|
|
170
|
+
*/
|
|
171
|
+
declare function runHooks(scope: "feature" | "global", timing: HookTiming, registry?: HookRegistry): Promise<void>;
|
|
172
|
+
/**
|
|
173
|
+
* Run global before-hooks once per worker, ahead of that worker's first
|
|
174
|
+
* scenario, and schedule global after-hooks for worker exit. Idempotent: every
|
|
175
|
+
* feature module calls this in a `beforeAll`, but only the first call in a given
|
|
176
|
+
* worker does anything.
|
|
177
|
+
*
|
|
178
|
+
* Semantics & caveats (the once-per-worker model):
|
|
179
|
+
* - Runs in the *same* realm as steps, so global setup may touch in-process
|
|
180
|
+
* state that steps later read.
|
|
181
|
+
* - "Once per worker", not strictly once per run — with multiple workers it runs
|
|
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.
|
|
185
|
+
*/
|
|
186
|
+
declare function ensureGlobalHooks(registry?: HookRegistry): Promise<void>;
|
|
187
|
+
//#endregion
|
|
188
|
+
export { numberParser as C, intParser as S, StepDefinitionMeta as _, RegisteredHook as a, Parser as b, ensureGlobalHooks as c, AnalysisRule as d, AnalyzerConfig as f, ParsedStep as g, ParsedScenario as h, PlainHookFn as i, globalHookRegistry as l, MatchedStep as m, HookScope as n, ScenarioHookFn as o, Diagnostic as p, HookTiming as r, ScenarioInfo as s, HookRegistry as t, runHooks as u, BasicWorld as v, stringParser as w, booleanParser as x, MergeableWorld as y };
|
|
189
|
+
//# sourceMappingURL=hooks-Dar49TtT.d.cts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//#region src/parsers.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A Parser is a typed cucumber-expression *parameter type*: it declares how a
|
|
4
|
+
* value is recognised in a Gherkin step (`regexp`) and how the matched text is
|
|
5
|
+
* turned into a TypeScript value (`parse`). The step expression uses `{name}`
|
|
6
|
+
* as the placeholder.
|
|
7
|
+
*
|
|
8
|
+
* The engine folds each parser straight into the cucumber-expression matcher —
|
|
9
|
+
* `parse` is the parameter type's transform, so coercion happens exactly once,
|
|
10
|
+
* during matching (no second pass). This is also what lets a parser introduce a
|
|
11
|
+
* brand-new placeholder like `{color}`: it is registered as a real parameter
|
|
12
|
+
* type, so text that doesn't match `regexp` simply doesn't match the step.
|
|
13
|
+
*
|
|
14
|
+
* The four built-ins below reuse cucumber-expressions' own built-in parameter
|
|
15
|
+
* types (`{int}`, `{float}`, `{string}`) — those names are already registered,
|
|
16
|
+
* so the library performs the match + coercion and each parser's `regexp`/
|
|
17
|
+
* `parse` serve to document intent and to drive TypeScript inference of the
|
|
18
|
+
* variable type. `booleanParser` has no built-in equivalent, so it is a genuine
|
|
19
|
+
* custom parameter type whose `parse` runs at match time.
|
|
20
|
+
*/
|
|
21
|
+
type Parser<T> = {
|
|
22
|
+
/** Parameter-type name; the step expression uses `{name}` as the placeholder. */name: string; /** How cucumber-expressions recognises the value in the step text. */
|
|
23
|
+
regexp: RegExp | RegExp[]; /** Transform the matched text into the typed value. */
|
|
24
|
+
parse: (value: string) => T;
|
|
25
|
+
};
|
|
26
|
+
/** Matches a quoted string (`{string}`) and strips the surrounding quotes. */
|
|
27
|
+
declare const stringParser: Parser<string>;
|
|
28
|
+
/** Matches an unquoted integer (`{int}`). */
|
|
29
|
+
declare const intParser: Parser<number>;
|
|
30
|
+
/** Matches an unquoted floating point number (`{float}`). */
|
|
31
|
+
declare const numberParser: Parser<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Matches an unquoted `true`/`false` word (`{boolean}`) and parses it to a
|
|
34
|
+
* boolean. Unlike the others this is a genuine custom parameter type — cucumber
|
|
35
|
+
* has no built-in `boolean` — so its `regexp`/`parse` are what the matcher uses.
|
|
36
|
+
*/
|
|
37
|
+
declare const booleanParser: Parser<boolean>;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/world.d.ts
|
|
40
|
+
type WorldState<State> = { readonly [K in keyof State]?: State[K] };
|
|
41
|
+
type MergeableWorldState<T> = WorldState<T> & {
|
|
42
|
+
merge: (newState: Partial<T>) => void;
|
|
43
|
+
};
|
|
44
|
+
type MergeableWorld<Given, When, Then> = {
|
|
45
|
+
given: MergeableWorldState<Given>;
|
|
46
|
+
when: MergeableWorldState<When>;
|
|
47
|
+
then: MergeableWorldState<Then>;
|
|
48
|
+
};
|
|
49
|
+
declare class BasicWorld<Given, When, Then> {
|
|
50
|
+
private givenState;
|
|
51
|
+
private whenState;
|
|
52
|
+
private thenState;
|
|
53
|
+
get given(): MergeableWorldState<Given>;
|
|
54
|
+
get when(): MergeableWorldState<When>;
|
|
55
|
+
get then(): MergeableWorldState<Then>;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/analyzer/types.d.ts
|
|
59
|
+
interface StepDefinitionMeta {
|
|
60
|
+
stepType: "given" | "when" | "then";
|
|
61
|
+
expression: string;
|
|
62
|
+
dependencies: {
|
|
63
|
+
given: Record<string, "required" | "optional">;
|
|
64
|
+
when: Record<string, "required" | "optional">;
|
|
65
|
+
then: Record<string, "required" | "optional">;
|
|
66
|
+
};
|
|
67
|
+
produces: string[];
|
|
68
|
+
sourceFile: string;
|
|
69
|
+
line: number;
|
|
70
|
+
}
|
|
71
|
+
interface ParsedScenario {
|
|
72
|
+
name: string;
|
|
73
|
+
file: string;
|
|
74
|
+
steps: ParsedStep[];
|
|
75
|
+
/**
|
|
76
|
+
* Gherkin tags in effect for this scenario, each including the leading `@`
|
|
77
|
+
* (feature + scenario tags, plus the Examples-block tags for outline rows).
|
|
78
|
+
* The Vitest plugin maps `@skip`/`@only` onto `test.skip`/`test.only`.
|
|
79
|
+
*/
|
|
80
|
+
tags: string[];
|
|
81
|
+
/**
|
|
82
|
+
* Present only for rows expanded from a `Scenario Outline`. Carries the base
|
|
83
|
+
* outline name so the plugin can group its rows under one `describe`, with
|
|
84
|
+
* each row a separate `test` labelled by its example values.
|
|
85
|
+
*/
|
|
86
|
+
outline?: {
|
|
87
|
+
name: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
interface ParsedStep {
|
|
91
|
+
keyword: "Given" | "When" | "Then" | "And" | "But";
|
|
92
|
+
effectiveKeyword: "Given" | "When" | "Then";
|
|
93
|
+
text: string;
|
|
94
|
+
line: number;
|
|
95
|
+
column: number;
|
|
96
|
+
}
|
|
97
|
+
interface MatchedStep extends ParsedStep {
|
|
98
|
+
definitions: StepDefinitionMeta[];
|
|
99
|
+
}
|
|
100
|
+
interface Diagnostic {
|
|
101
|
+
file: string;
|
|
102
|
+
range: {
|
|
103
|
+
startLine: number;
|
|
104
|
+
startColumn: number;
|
|
105
|
+
endLine: number;
|
|
106
|
+
endColumn: number;
|
|
107
|
+
};
|
|
108
|
+
severity: "error" | "warning" | "info";
|
|
109
|
+
message: string;
|
|
110
|
+
rule: string;
|
|
111
|
+
source: "step-forge";
|
|
112
|
+
}
|
|
113
|
+
interface AnalysisRule {
|
|
114
|
+
name: string;
|
|
115
|
+
check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[];
|
|
116
|
+
}
|
|
117
|
+
interface AnalyzerConfig {
|
|
118
|
+
stepFiles: string[];
|
|
119
|
+
featureFiles: string[];
|
|
120
|
+
tsConfigPath?: string;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/runtime/hooks.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Hooks are side-effect callbacks that run around scenarios, feature files, or
|
|
126
|
+
* the whole run. Unlike steps they never seed state: state is owned end to end
|
|
127
|
+
* by the typed dependency graph (given/when/then), and hooks exist for setup and
|
|
128
|
+
* teardown (opening a DB, starting a server, resetting mocks). A scenario hook
|
|
129
|
+
* may *read* the world, but its return value is ignored.
|
|
130
|
+
*/
|
|
131
|
+
type HookScope = "scenario" | "feature" | "global";
|
|
132
|
+
type HookTiming = "before" | "after";
|
|
133
|
+
/** Lightweight scenario identity handed to per-scenario hooks. */
|
|
134
|
+
interface ScenarioInfo {
|
|
135
|
+
name: string;
|
|
136
|
+
file: string;
|
|
137
|
+
}
|
|
138
|
+
/** A per-scenario hook: gets the scenario's world (read-only in spirit). */
|
|
139
|
+
type ScenarioHookFn = (context: {
|
|
140
|
+
world: MergeableWorld<any, any, any>;
|
|
141
|
+
scenario: ScenarioInfo;
|
|
142
|
+
}) => void | Promise<void>;
|
|
143
|
+
/** A per-feature-file or global hook: no world exists at these boundaries. */
|
|
144
|
+
type PlainHookFn = () => void | Promise<void>;
|
|
145
|
+
interface RegisteredHook {
|
|
146
|
+
scope: HookScope;
|
|
147
|
+
timing: HookTiming;
|
|
148
|
+
fn: ScenarioHookFn | PlainHookFn;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Collection of registered hooks. Like {@link StepRegistry}, a plain instance
|
|
152
|
+
* (not a hidden global) so tests can isolate; `globalHookRegistry` is the
|
|
153
|
+
* default sink the public `beforeScenario`/`afterAll`/etc helpers write to.
|
|
154
|
+
*/
|
|
155
|
+
declare class HookRegistry {
|
|
156
|
+
private hooks;
|
|
157
|
+
add(hook: RegisteredHook): void;
|
|
158
|
+
/**
|
|
159
|
+
* Hooks for a scope+timing. `before` hooks run in registration order;
|
|
160
|
+
* `after` hooks run in reverse (LIFO), so teardown unwinds setup.
|
|
161
|
+
*/
|
|
162
|
+
for(scope: HookScope, timing: HookTiming): RegisteredHook[];
|
|
163
|
+
clear(): void;
|
|
164
|
+
}
|
|
165
|
+
declare const globalHookRegistry: HookRegistry;
|
|
166
|
+
/**
|
|
167
|
+
* Run every registered feature/global hook of a scope+timing in order. Used by
|
|
168
|
+
* the generated test modules (feature hooks). Throws if a hook throws, so the
|
|
169
|
+
* runner reports it against the enclosing boundary.
|
|
170
|
+
*/
|
|
171
|
+
declare function runHooks(scope: "feature" | "global", timing: HookTiming, registry?: HookRegistry): Promise<void>;
|
|
172
|
+
/**
|
|
173
|
+
* Run global before-hooks once per worker, ahead of that worker's first
|
|
174
|
+
* scenario, and schedule global after-hooks for worker exit. Idempotent: every
|
|
175
|
+
* feature module calls this in a `beforeAll`, but only the first call in a given
|
|
176
|
+
* worker does anything.
|
|
177
|
+
*
|
|
178
|
+
* Semantics & caveats (the once-per-worker model):
|
|
179
|
+
* - Runs in the *same* realm as steps, so global setup may touch in-process
|
|
180
|
+
* state that steps later read.
|
|
181
|
+
* - "Once per worker", not strictly once per run — with multiple workers it runs
|
|
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.
|
|
185
|
+
*/
|
|
186
|
+
declare function ensureGlobalHooks(registry?: HookRegistry): Promise<void>;
|
|
187
|
+
//#endregion
|
|
188
|
+
export { numberParser as C, intParser as S, StepDefinitionMeta as _, RegisteredHook as a, Parser as b, ensureGlobalHooks as c, AnalysisRule as d, AnalyzerConfig as f, ParsedStep as g, ParsedScenario as h, PlainHookFn as i, globalHookRegistry as l, MatchedStep as m, HookScope as n, ScenarioHookFn as o, Diagnostic as p, HookTiming as r, ScenarioInfo as s, HookRegistry as t, runHooks as u, BasicWorld as v, stringParser as w, booleanParser as x, MergeableWorld as y };
|
|
189
|
+
//# sourceMappingURL=hooks-Dar49TtT.d.ts.map
|
package/dist/runtime.cjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_hooks = require("./hooks-CGYzwDOv.cjs");
|
|
3
|
+
let _cucumber_cucumber_expressions = require("@cucumber/cucumber-expressions");
|
|
4
|
+
//#region src/runtime/engine.ts
|
|
5
|
+
const keywordToStepType = {
|
|
6
|
+
Given: "given",
|
|
7
|
+
When: "when",
|
|
8
|
+
Then: "then"
|
|
9
|
+
};
|
|
10
|
+
var UndefinedStepError = class extends Error {
|
|
11
|
+
step;
|
|
12
|
+
constructor(step) {
|
|
13
|
+
super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);
|
|
14
|
+
this.step = step;
|
|
15
|
+
this.name = "UndefinedStepError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var AmbiguousStepError = class extends Error {
|
|
19
|
+
step;
|
|
20
|
+
matches;
|
|
21
|
+
constructor(step, matches) {
|
|
22
|
+
super(`Ambiguous step: "${step.text}" matched ${matches.length} definitions:\n` + matches.map((m) => ` - ${m.expression}`).join("\n"));
|
|
23
|
+
this.step = step;
|
|
24
|
+
this.matches = matches;
|
|
25
|
+
this.name = "AmbiguousStepError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function compile(registry) {
|
|
29
|
+
return registry.all().map((step) => {
|
|
30
|
+
const paramRegistry = new _cucumber_cucumber_expressions.ParameterTypeRegistry();
|
|
31
|
+
for (const parser of step.parsers) {
|
|
32
|
+
if (paramRegistry.lookupByTypeName(parser.name)) continue;
|
|
33
|
+
const regexps = Array.isArray(parser.regexp) ? parser.regexp : [parser.regexp];
|
|
34
|
+
paramRegistry.defineParameterType(new _cucumber_cucumber_expressions.ParameterType(parser.name, regexps, null, (value) => parser.parse(value)));
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
step,
|
|
38
|
+
expression: new _cucumber_cucumber_expressions.CucumberExpression(step.expression, paramRegistry)
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find the single step definition matching a Gherkin step. Matching is
|
|
44
|
+
* opinionated and strict: the keyword must line up with the step type, exactly
|
|
45
|
+
* one definition must match, and undefined/ambiguous both throw rather than
|
|
46
|
+
* silently skipping (unlike Cucumber's pending/undefined dance).
|
|
47
|
+
*/
|
|
48
|
+
function matchStep(step, compiled) {
|
|
49
|
+
const expectedType = keywordToStepType[step.effectiveKeyword];
|
|
50
|
+
const matches = [];
|
|
51
|
+
for (const { step: def, expression } of compiled) {
|
|
52
|
+
if (def.stepType !== expectedType) continue;
|
|
53
|
+
const result = expression.match(step.text);
|
|
54
|
+
if (result) matches.push({
|
|
55
|
+
step: def,
|
|
56
|
+
args: result.map((a) => a.getValue(null))
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (matches.length === 0) throw new UndefinedStepError(step);
|
|
60
|
+
if (matches.length > 1) throw new AmbiguousStepError(step, matches.map((m) => m.step));
|
|
61
|
+
return matches[0];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run one scenario against a registry. A fresh world is created per scenario
|
|
65
|
+
* (state never leaks between scenarios). On the first failing step the
|
|
66
|
+
* remaining steps are marked skipped, matching Cucumber's execution semantics.
|
|
67
|
+
*
|
|
68
|
+
* Throws on failure so it maps cleanly onto a test runner's `test()` body, but
|
|
69
|
+
* the returned/attached `ScenarioResult` carries the per-step breakdown.
|
|
70
|
+
*/
|
|
71
|
+
async function runScenario(scenario, registry, makeWorld, hooks = require_hooks.globalHookRegistry) {
|
|
72
|
+
const compiled = compile(registry);
|
|
73
|
+
const world = makeWorld();
|
|
74
|
+
const scenarioInfo = {
|
|
75
|
+
name: scenario.name,
|
|
76
|
+
file: scenario.file
|
|
77
|
+
};
|
|
78
|
+
const steps = [];
|
|
79
|
+
let failed = false;
|
|
80
|
+
let firstError;
|
|
81
|
+
const fail = (err) => {
|
|
82
|
+
if (failed) return;
|
|
83
|
+
failed = true;
|
|
84
|
+
firstError = err instanceof Error ? err : new Error(String(err));
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
for (const hook of hooks.for("scenario", "before")) await hook.fn({
|
|
88
|
+
world,
|
|
89
|
+
scenario: scenarioInfo
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
fail(err);
|
|
93
|
+
}
|
|
94
|
+
for (const step of scenario.steps) {
|
|
95
|
+
if (failed) {
|
|
96
|
+
steps.push({
|
|
97
|
+
step,
|
|
98
|
+
status: "skipped"
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const { step: def, args } = matchStep(step, compiled);
|
|
104
|
+
await def.execute(world, args);
|
|
105
|
+
steps.push({
|
|
106
|
+
step,
|
|
107
|
+
status: "passed"
|
|
108
|
+
});
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
111
|
+
steps.push({
|
|
112
|
+
step,
|
|
113
|
+
status: "failed",
|
|
114
|
+
error
|
|
115
|
+
});
|
|
116
|
+
fail(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const hook of hooks.for("scenario", "after")) try {
|
|
120
|
+
await hook.fn({
|
|
121
|
+
world,
|
|
122
|
+
scenario: scenarioInfo
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
fail(err);
|
|
126
|
+
}
|
|
127
|
+
const result = {
|
|
128
|
+
scenario,
|
|
129
|
+
status: failed ? "failed" : "passed",
|
|
130
|
+
steps
|
|
131
|
+
};
|
|
132
|
+
if (failed && firstError) {
|
|
133
|
+
const failing = steps.find((s) => s.status === "failed");
|
|
134
|
+
if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
|
|
135
|
+
throw firstError;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Prepend a synthetic stack frame pointing at the failing Gherkin step. Because
|
|
141
|
+
* the frame's file is the real `.feature` on disk, the test runner treats it as
|
|
142
|
+
* a source location and shows a code frame at the step — instead of us jamming
|
|
143
|
+
* `file:line` into the error message. The frame goes *above* the real stack, so
|
|
144
|
+
* the step-definition frames (the actual throw site) are preserved below it.
|
|
145
|
+
*/
|
|
146
|
+
function attachFeatureFrame(error, file, step) {
|
|
147
|
+
const frame = ` at ${`${step.effectiveKeyword} ${step.text}`} (${file}:${step.line}:${step.column})`;
|
|
148
|
+
const stack = error.stack;
|
|
149
|
+
if (!stack) {
|
|
150
|
+
error.stack = `${error.name}: ${error.message}\n${frame}`;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const firstFrame = stack.indexOf("\n at ");
|
|
154
|
+
if (firstFrame === -1) error.stack = `${stack}\n${frame}`;
|
|
155
|
+
else error.stack = stack.slice(0, firstFrame) + `\n${frame}` + stack.slice(firstFrame);
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
exports.AmbiguousStepError = AmbiguousStepError;
|
|
159
|
+
exports.HookRegistry = require_hooks.HookRegistry;
|
|
160
|
+
exports.StepRegistry = require_hooks.StepRegistry;
|
|
161
|
+
exports.UndefinedStepError = UndefinedStepError;
|
|
162
|
+
exports.ensureGlobalHooks = require_hooks.ensureGlobalHooks;
|
|
163
|
+
exports.globalHookRegistry = require_hooks.globalHookRegistry;
|
|
164
|
+
exports.globalRegistry = require_hooks.globalRegistry;
|
|
165
|
+
exports.runHooks = require_hooks.runHooks;
|
|
166
|
+
exports.runScenario = runScenario;
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=runtime.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.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. */\ninterface 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\nfunction compile(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\n/**\n * Run one scenario against a registry. A fresh world is created per scenario\n * (state never leaks between scenarios). On the first failing step the\n * remaining steps are marked skipped, matching Cucumber's execution semantics.\n *\n * Throws on failure so it maps cleanly onto a test runner's `test()` body, but\n * the returned/attached `ScenarioResult` carries the per-step breakdown.\n */\nexport async function runScenario(\n scenario: ParsedScenario,\n registry: StepRegistry,\n makeWorld: () => MergeableWorld<any, any, any>,\n hooks: HookRegistry = globalHookRegistry\n): Promise<ScenarioResult> {\n const compiled = compile(registry);\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 const result: ScenarioResult = {\n scenario,\n status: failed ? \"failed\" : \"passed\",\n steps,\n };\n\n if (failed && firstError) {\n // Surface the failing Gherkin line as a real stack frame so the runner\n // renders a code frame from the `.feature` file itself. Hook failures have\n // no step 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 throw firstError;\n }\n\n return result;\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,69 @@
|
|
|
1
|
+
import { a as RegisteredHook, b as Parser, c as ensureGlobalHooks, g as ParsedStep, h as ParsedScenario, i as PlainHookFn, l as globalHookRegistry, n as HookScope, o as ScenarioHookFn, r as HookTiming, s as ScenarioInfo, t as HookRegistry, u as runHooks, y as MergeableWorld } from "./hooks-Dar49TtT.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/registry.d.ts
|
|
4
|
+
type StepType = "given" | "when" | "then";
|
|
5
|
+
/**
|
|
6
|
+
* A step definition as it exists at *runtime*, independent of any test runner.
|
|
7
|
+
*
|
|
8
|
+
* `execute` is the fully-wired step body: given a world and the raw values
|
|
9
|
+
* captured from a Gherkin step, it applies parsers, validates + narrows
|
|
10
|
+
* dependencies, runs the user's step function, and merges the result back into
|
|
11
|
+
* the world. The engine never needs to know how any of that works — it just
|
|
12
|
+
* matches text to a step and calls `execute`.
|
|
13
|
+
*/
|
|
14
|
+
interface RegisteredStep {
|
|
15
|
+
stepType: StepType;
|
|
16
|
+
/** The Cucumber-expression source, e.g. `a user named {string}`. */
|
|
17
|
+
expression: string;
|
|
18
|
+
/** Parsers, one per captured variable, applied after expression capture. */
|
|
19
|
+
parsers: Parser<any>[];
|
|
20
|
+
execute: (world: MergeableWorld<any, any, any>, capturedArgs: unknown[]) => Promise<void>;
|
|
21
|
+
/** Where the step was defined, for ambiguous-match diagnostics. */
|
|
22
|
+
source?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A collection of registered steps. Deliberately a plain instance (not a hidden
|
|
26
|
+
* module global) so tests and the Vitest plugin can create isolated registries.
|
|
27
|
+
* `globalRegistry` is the default sink that `.step()` writes to.
|
|
28
|
+
*/
|
|
29
|
+
declare class StepRegistry {
|
|
30
|
+
private steps;
|
|
31
|
+
add(step: RegisteredStep): void;
|
|
32
|
+
all(): readonly RegisteredStep[];
|
|
33
|
+
clear(): void;
|
|
34
|
+
}
|
|
35
|
+
declare const globalRegistry: StepRegistry;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/runtime/engine.d.ts
|
|
38
|
+
declare class UndefinedStepError extends Error {
|
|
39
|
+
readonly step: ParsedStep;
|
|
40
|
+
constructor(step: ParsedStep);
|
|
41
|
+
}
|
|
42
|
+
declare class AmbiguousStepError extends Error {
|
|
43
|
+
readonly step: ParsedStep;
|
|
44
|
+
readonly matches: RegisteredStep[];
|
|
45
|
+
constructor(step: ParsedStep, matches: RegisteredStep[]);
|
|
46
|
+
}
|
|
47
|
+
interface StepResult {
|
|
48
|
+
step: ParsedStep;
|
|
49
|
+
status: "passed" | "failed" | "skipped";
|
|
50
|
+
error?: Error;
|
|
51
|
+
durationMs?: number;
|
|
52
|
+
}
|
|
53
|
+
interface ScenarioResult {
|
|
54
|
+
scenario: ParsedScenario;
|
|
55
|
+
status: "passed" | "failed";
|
|
56
|
+
steps: StepResult[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run one scenario against a registry. A fresh world is created per scenario
|
|
60
|
+
* (state never leaks between scenarios). On the first failing step the
|
|
61
|
+
* remaining steps are marked skipped, matching Cucumber's execution semantics.
|
|
62
|
+
*
|
|
63
|
+
* Throws on failure so it maps cleanly onto a test runner's `test()` body, but
|
|
64
|
+
* the returned/attached `ScenarioResult` carries the per-step breakdown.
|
|
65
|
+
*/
|
|
66
|
+
declare function runScenario(scenario: ParsedScenario, registry: StepRegistry, makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { AmbiguousStepError, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
|
|
69
|
+
//# sourceMappingURL=runtime.d.cts.map
|