@step-forge/step-forge 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/RUNTIME.md +212 -0
- package/dist/{analyzer-DJyJbU_V.js → analyzer-DYfdoSIS.js} +151 -11
- package/dist/analyzer-DYfdoSIS.js.map +1 -0
- package/dist/analyzer-cli.js +1 -1
- package/dist/analyzer.js +1 -2
- package/dist/cli.cjs +470 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +471 -0
- package/dist/cli.js.map +1 -0
- package/dist/engine-DPRxs6eC.js +181 -0
- package/dist/engine-DPRxs6eC.js.map +1 -0
- package/dist/engine-DWAIlwWp.cjs +204 -0
- package/dist/engine-DWAIlwWp.cjs.map +1 -0
- package/dist/gherkinParser-CHpkEwii.cjs +243 -0
- package/dist/gherkinParser-CHpkEwii.cjs.map +1 -0
- package/dist/{gherkinParser-Dp2d7JNr.js → gherkinParser-CKARHgt4.js} +74 -2
- package/dist/gherkinParser-CKARHgt4.js.map +1 -0
- package/dist/runtime.cjs +5 -160
- package/dist/runtime.d.cts +80 -7
- package/dist/runtime.d.ts +80 -7
- package/dist/runtime.js +2 -158
- package/dist/step-forge.cjs +9 -189
- package/dist/step-forge.cjs.map +1 -1
- package/dist/step-forge.d.cts +6 -6
- package/dist/step-forge.d.ts +6 -6
- package/dist/step-forge.js +3 -158
- package/dist/step-forge.js.map +1 -1
- package/package.json +12 -18
- package/dist/analyzer-DJyJbU_V.js.map +0 -1
- package/dist/gherkinParser-Dp2d7JNr.js.map +0 -1
- package/dist/runtime.cjs.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/vitest.d.ts +0 -74
- package/dist/vitest.js +0 -136
- package/dist/vitest.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gherkinParser-CHpkEwii.cjs","names":["_","path","fs","Parser","AstBuilder","messages","GherkinClassicTokenMatcher"],"sources":["../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["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 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 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,6 +1,78 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import { glob, stat } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
1
4
|
import * as fs from "node:fs";
|
|
2
5
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
3
6
|
import * as messages from "@cucumber/messages";
|
|
7
|
+
//#region src/world.ts
|
|
8
|
+
function mergeCustomizer(objValue, srcValue) {
|
|
9
|
+
if (_.isArray(objValue)) return objValue.concat(srcValue);
|
|
10
|
+
else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
|
|
11
|
+
return objValue;
|
|
12
|
+
}
|
|
13
|
+
var BasicWorld = class {
|
|
14
|
+
givenState = {};
|
|
15
|
+
whenState = {};
|
|
16
|
+
thenState = {};
|
|
17
|
+
get given() {
|
|
18
|
+
return {
|
|
19
|
+
...this.givenState,
|
|
20
|
+
merge: (newState) => {
|
|
21
|
+
this.givenState = _.merge({ ...this.givenState }, newState, mergeCustomizer);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
get when() {
|
|
26
|
+
return {
|
|
27
|
+
...this.whenState,
|
|
28
|
+
merge: (newState) => {
|
|
29
|
+
this.whenState = _.merge({ ...this.whenState }, newState, mergeCustomizer);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
get then() {
|
|
34
|
+
return {
|
|
35
|
+
...this.thenState,
|
|
36
|
+
merge: (newState) => {
|
|
37
|
+
this.thenState = _.merge({ ...this.thenState }, newState, mergeCustomizer);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/globFiles.ts
|
|
44
|
+
/** Glob metacharacters. A pattern with none of these is a literal path. */
|
|
45
|
+
const MAGIC = /[*?[\]{}!()]/;
|
|
46
|
+
async function isFile(p) {
|
|
47
|
+
try {
|
|
48
|
+
return (await stat(p)).isFile();
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve glob patterns to a de-duplicated list of absolute file paths, with one
|
|
55
|
+
* important portability guarantee: a **literal absolute path** (no glob magic)
|
|
56
|
+
* is returned directly if it exists, without being handed to `glob()`.
|
|
57
|
+
*
|
|
58
|
+
* This exists because `node:fs`'s `glob` diverges between runtimes — Node
|
|
59
|
+
* matches an absolute-path pattern, Bun returns nothing for one. Rather than
|
|
60
|
+
* depend on that behaviour, we only ever glob relative patterns (against `cwd`)
|
|
61
|
+
* and short-circuit concrete absolute paths ourselves, so callers get identical
|
|
62
|
+
* results under Node and Bun.
|
|
63
|
+
*/
|
|
64
|
+
async function globFiles(patterns, cwd = process.cwd()) {
|
|
65
|
+
const files = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const pattern of patterns) {
|
|
67
|
+
if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {
|
|
68
|
+
if (await isFile(pattern)) files.add(pattern);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for await (const match of glob(pattern, { cwd })) files.add(path.resolve(cwd, match));
|
|
72
|
+
}
|
|
73
|
+
return [...files];
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
4
76
|
//#region src/analyzer/gherkinParser.ts
|
|
5
77
|
function parseFeatureFiles(filePaths) {
|
|
6
78
|
const scenarios = [];
|
|
@@ -111,6 +183,6 @@ function substituteExampleValues(text, substitution) {
|
|
|
111
183
|
return result;
|
|
112
184
|
}
|
|
113
185
|
//#endregion
|
|
114
|
-
export { parseFeatureFiles as n, parseFeatureContent as t };
|
|
186
|
+
export { BasicWorld as i, parseFeatureFiles as n, globFiles as r, parseFeatureContent as t };
|
|
115
187
|
|
|
116
|
-
//# sourceMappingURL=gherkinParser-
|
|
188
|
+
//# sourceMappingURL=gherkinParser-CKARHgt4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gherkinParser-CKARHgt4.js","names":[],"sources":["../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["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 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 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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"}
|
package/dist/runtime.cjs
CHANGED
|
@@ -1,168 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_hooks = require("./hooks-CGYzwDOv.cjs");
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
|
3
|
+
const require_engine = require("./engine-DWAIlwWp.cjs");
|
|
4
|
+
exports.AmbiguousStepError = require_engine.AmbiguousStepError;
|
|
159
5
|
exports.HookRegistry = require_hooks.HookRegistry;
|
|
160
6
|
exports.StepRegistry = require_hooks.StepRegistry;
|
|
161
|
-
exports.UndefinedStepError = UndefinedStepError;
|
|
7
|
+
exports.UndefinedStepError = require_engine.UndefinedStepError;
|
|
8
|
+
exports.compileRegistry = require_engine.compileRegistry;
|
|
162
9
|
exports.ensureGlobalHooks = require_hooks.ensureGlobalHooks;
|
|
163
10
|
exports.globalHookRegistry = require_hooks.globalHookRegistry;
|
|
164
11
|
exports.globalRegistry = require_hooks.globalRegistry;
|
|
165
12
|
exports.runHooks = require_hooks.runHooks;
|
|
166
|
-
exports.runScenario = runScenario;
|
|
167
|
-
|
|
168
|
-
//# sourceMappingURL=runtime.cjs.map
|
|
13
|
+
exports.runScenario = require_engine.runScenario;
|
package/dist/runtime.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
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
|
+
import { CucumberExpression } from "@cucumber/cucumber-expressions";
|
|
2
3
|
|
|
3
4
|
//#region src/runtime/registry.d.ts
|
|
4
5
|
type StepType = "given" | "when" | "then";
|
|
@@ -35,6 +36,11 @@ declare class StepRegistry {
|
|
|
35
36
|
declare const globalRegistry: StepRegistry;
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region src/runtime/engine.d.ts
|
|
39
|
+
/** A registered step paired with its compiled Cucumber expression. */
|
|
40
|
+
interface CompiledStep {
|
|
41
|
+
step: RegisteredStep;
|
|
42
|
+
expression: CucumberExpression;
|
|
43
|
+
}
|
|
38
44
|
declare class UndefinedStepError extends Error {
|
|
39
45
|
readonly step: ParsedStep;
|
|
40
46
|
constructor(step: ParsedStep);
|
|
@@ -44,6 +50,13 @@ declare class AmbiguousStepError extends Error {
|
|
|
44
50
|
readonly matches: RegisteredStep[];
|
|
45
51
|
constructor(step: ParsedStep, matches: RegisteredStep[]);
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Compile a registry's steps into matchable Cucumber expressions **once** per
|
|
55
|
+
* run. The result is reused for every scenario — compilation is pure and depends
|
|
56
|
+
* only on the registry, so recompiling per scenario (as an earlier version did)
|
|
57
|
+
* was wasted work proportional to scenarios × steps.
|
|
58
|
+
*/
|
|
59
|
+
declare function compileRegistry(registry: StepRegistry): CompiledStep[];
|
|
47
60
|
interface StepResult {
|
|
48
61
|
step: ParsedStep;
|
|
49
62
|
status: "passed" | "failed" | "skipped";
|
|
@@ -54,16 +67,76 @@ interface ScenarioResult {
|
|
|
54
67
|
scenario: ParsedScenario;
|
|
55
68
|
status: "passed" | "failed";
|
|
56
69
|
steps: StepResult[];
|
|
70
|
+
/**
|
|
71
|
+
* The scenario's first error, if it failed. Usually the same object as the
|
|
72
|
+
* failing step's `error` (with a synthetic `.feature` stack frame attached),
|
|
73
|
+
* but for a hook failure there's no step to point at, so this is the only
|
|
74
|
+
* place it surfaces. Reporters read this; the runner never throws it.
|
|
75
|
+
*/
|
|
76
|
+
error?: Error;
|
|
77
|
+
/** Wall-clock duration of the whole scenario, in milliseconds. */
|
|
78
|
+
durationMs?: number;
|
|
57
79
|
}
|
|
58
80
|
/**
|
|
59
|
-
* Run one scenario against a
|
|
60
|
-
* (state never leaks between scenarios). On the first failing step
|
|
61
|
-
* remaining steps are marked skipped, matching Cucumber's execution
|
|
81
|
+
* Run one scenario against a pre-compiled step table. A fresh world is created
|
|
82
|
+
* per scenario (state never leaks between scenarios). On the first failing step
|
|
83
|
+
* the remaining steps are marked skipped, matching Cucumber's execution
|
|
84
|
+
* semantics.
|
|
85
|
+
*
|
|
86
|
+
* Never throws for step or hook failures: it always resolves to a
|
|
87
|
+
* `ScenarioResult` carrying the per-step breakdown and (on failure) the first
|
|
88
|
+
* `error` with a synthetic `.feature` stack frame attached. Callers decide what
|
|
89
|
+
* to do with a failure — the CLI runner reports it, a test-runner adapter can
|
|
90
|
+
* re-throw `result.error`. It still rejects for truly exceptional conditions
|
|
91
|
+
* (e.g. a bug in the engine itself), never for a normal test failure.
|
|
62
92
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
93
|
+
* Pass the compiled table from {@link compileRegistry} once and reuse it across
|
|
94
|
+
* every scenario in the run.
|
|
65
95
|
*/
|
|
66
|
-
declare function runScenario(scenario: ParsedScenario,
|
|
96
|
+
declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/runtime/config.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* User-facing runner configuration. The same shape is accepted from a
|
|
101
|
+
* `step-forge.config.ts` file (default export) and from CLI flags, with flags
|
|
102
|
+
* taking precedence. Everything is optional; {@link resolveConfig} fills in
|
|
103
|
+
* defaults.
|
|
104
|
+
*/
|
|
105
|
+
interface RunnerOptions {
|
|
106
|
+
/** Feature-file glob(s), relative to `cwd`. Defaults to every `.feature` file. */
|
|
107
|
+
features?: string | string[];
|
|
108
|
+
/** Step-module glob(s), relative to `cwd`. Defaults to every `.steps.ts` file. */
|
|
109
|
+
steps?: string | string[];
|
|
110
|
+
/**
|
|
111
|
+
* Module that default-exports a world factory `() => world`, relative to
|
|
112
|
+
* `cwd`. When omitted each scenario gets a fresh `BasicWorld`.
|
|
113
|
+
*/
|
|
114
|
+
world?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Max scenarios in flight at once. Defaults to `1` (serial), matching
|
|
117
|
+
* Cucumber's default execution model — scenarios often share module-level
|
|
118
|
+
* state via hooks, which only holds under serial execution. Raise it to opt
|
|
119
|
+
* into parallelism for isolated, I/O-bound suites.
|
|
120
|
+
*/
|
|
121
|
+
concurrency?: number;
|
|
122
|
+
/** Reporter name. Default `pretty`. */
|
|
123
|
+
reporter?: "pretty" | "progress";
|
|
124
|
+
/** Only run scenarios whose name matches this (string → substring/regex). */
|
|
125
|
+
name?: string;
|
|
126
|
+
/** Cucumber tag expression, e.g. `@smoke and not @wip`. */
|
|
127
|
+
tags?: string;
|
|
128
|
+
}
|
|
129
|
+
/** Fully-resolved config: no optionals, globs kept as arrays, paths absolute. */
|
|
130
|
+
interface ResolvedConfig {
|
|
131
|
+
cwd: string;
|
|
132
|
+
features: string[];
|
|
133
|
+
steps: string[];
|
|
134
|
+
world?: string;
|
|
135
|
+
concurrency: number;
|
|
136
|
+
reporter: "pretty" | "progress";
|
|
137
|
+
name?: string;
|
|
138
|
+
tags?: string;
|
|
139
|
+
}
|
|
67
140
|
//#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 };
|
|
141
|
+
export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
|
|
69
142
|
//# sourceMappingURL=runtime.d.cts.map
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
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.js";
|
|
2
|
+
import { CucumberExpression } from "@cucumber/cucumber-expressions";
|
|
2
3
|
|
|
3
4
|
//#region src/runtime/registry.d.ts
|
|
4
5
|
type StepType = "given" | "when" | "then";
|
|
@@ -35,6 +36,11 @@ declare class StepRegistry {
|
|
|
35
36
|
declare const globalRegistry: StepRegistry;
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region src/runtime/engine.d.ts
|
|
39
|
+
/** A registered step paired with its compiled Cucumber expression. */
|
|
40
|
+
interface CompiledStep {
|
|
41
|
+
step: RegisteredStep;
|
|
42
|
+
expression: CucumberExpression;
|
|
43
|
+
}
|
|
38
44
|
declare class UndefinedStepError extends Error {
|
|
39
45
|
readonly step: ParsedStep;
|
|
40
46
|
constructor(step: ParsedStep);
|
|
@@ -44,6 +50,13 @@ declare class AmbiguousStepError extends Error {
|
|
|
44
50
|
readonly matches: RegisteredStep[];
|
|
45
51
|
constructor(step: ParsedStep, matches: RegisteredStep[]);
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Compile a registry's steps into matchable Cucumber expressions **once** per
|
|
55
|
+
* run. The result is reused for every scenario — compilation is pure and depends
|
|
56
|
+
* only on the registry, so recompiling per scenario (as an earlier version did)
|
|
57
|
+
* was wasted work proportional to scenarios × steps.
|
|
58
|
+
*/
|
|
59
|
+
declare function compileRegistry(registry: StepRegistry): CompiledStep[];
|
|
47
60
|
interface StepResult {
|
|
48
61
|
step: ParsedStep;
|
|
49
62
|
status: "passed" | "failed" | "skipped";
|
|
@@ -54,16 +67,76 @@ interface ScenarioResult {
|
|
|
54
67
|
scenario: ParsedScenario;
|
|
55
68
|
status: "passed" | "failed";
|
|
56
69
|
steps: StepResult[];
|
|
70
|
+
/**
|
|
71
|
+
* The scenario's first error, if it failed. Usually the same object as the
|
|
72
|
+
* failing step's `error` (with a synthetic `.feature` stack frame attached),
|
|
73
|
+
* but for a hook failure there's no step to point at, so this is the only
|
|
74
|
+
* place it surfaces. Reporters read this; the runner never throws it.
|
|
75
|
+
*/
|
|
76
|
+
error?: Error;
|
|
77
|
+
/** Wall-clock duration of the whole scenario, in milliseconds. */
|
|
78
|
+
durationMs?: number;
|
|
57
79
|
}
|
|
58
80
|
/**
|
|
59
|
-
* Run one scenario against a
|
|
60
|
-
* (state never leaks between scenarios). On the first failing step
|
|
61
|
-
* remaining steps are marked skipped, matching Cucumber's execution
|
|
81
|
+
* Run one scenario against a pre-compiled step table. A fresh world is created
|
|
82
|
+
* per scenario (state never leaks between scenarios). On the first failing step
|
|
83
|
+
* the remaining steps are marked skipped, matching Cucumber's execution
|
|
84
|
+
* semantics.
|
|
85
|
+
*
|
|
86
|
+
* Never throws for step or hook failures: it always resolves to a
|
|
87
|
+
* `ScenarioResult` carrying the per-step breakdown and (on failure) the first
|
|
88
|
+
* `error` with a synthetic `.feature` stack frame attached. Callers decide what
|
|
89
|
+
* to do with a failure — the CLI runner reports it, a test-runner adapter can
|
|
90
|
+
* re-throw `result.error`. It still rejects for truly exceptional conditions
|
|
91
|
+
* (e.g. a bug in the engine itself), never for a normal test failure.
|
|
62
92
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
93
|
+
* Pass the compiled table from {@link compileRegistry} once and reuse it across
|
|
94
|
+
* every scenario in the run.
|
|
65
95
|
*/
|
|
66
|
-
declare function runScenario(scenario: ParsedScenario,
|
|
96
|
+
declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/runtime/config.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* User-facing runner configuration. The same shape is accepted from a
|
|
101
|
+
* `step-forge.config.ts` file (default export) and from CLI flags, with flags
|
|
102
|
+
* taking precedence. Everything is optional; {@link resolveConfig} fills in
|
|
103
|
+
* defaults.
|
|
104
|
+
*/
|
|
105
|
+
interface RunnerOptions {
|
|
106
|
+
/** Feature-file glob(s), relative to `cwd`. Defaults to every `.feature` file. */
|
|
107
|
+
features?: string | string[];
|
|
108
|
+
/** Step-module glob(s), relative to `cwd`. Defaults to every `.steps.ts` file. */
|
|
109
|
+
steps?: string | string[];
|
|
110
|
+
/**
|
|
111
|
+
* Module that default-exports a world factory `() => world`, relative to
|
|
112
|
+
* `cwd`. When omitted each scenario gets a fresh `BasicWorld`.
|
|
113
|
+
*/
|
|
114
|
+
world?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Max scenarios in flight at once. Defaults to `1` (serial), matching
|
|
117
|
+
* Cucumber's default execution model — scenarios often share module-level
|
|
118
|
+
* state via hooks, which only holds under serial execution. Raise it to opt
|
|
119
|
+
* into parallelism for isolated, I/O-bound suites.
|
|
120
|
+
*/
|
|
121
|
+
concurrency?: number;
|
|
122
|
+
/** Reporter name. Default `pretty`. */
|
|
123
|
+
reporter?: "pretty" | "progress";
|
|
124
|
+
/** Only run scenarios whose name matches this (string → substring/regex). */
|
|
125
|
+
name?: string;
|
|
126
|
+
/** Cucumber tag expression, e.g. `@smoke and not @wip`. */
|
|
127
|
+
tags?: string;
|
|
128
|
+
}
|
|
129
|
+
/** Fully-resolved config: no optionals, globs kept as arrays, paths absolute. */
|
|
130
|
+
interface ResolvedConfig {
|
|
131
|
+
cwd: string;
|
|
132
|
+
features: string[];
|
|
133
|
+
steps: string[];
|
|
134
|
+
world?: string;
|
|
135
|
+
concurrency: number;
|
|
136
|
+
reporter: "pretty" | "progress";
|
|
137
|
+
name?: string;
|
|
138
|
+
tags?: string;
|
|
139
|
+
}
|
|
67
140
|
//#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 };
|
|
141
|
+
export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
|
|
69
142
|
//# sourceMappingURL=runtime.d.ts.map
|