@letsrunit/gherker 0.1.0

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 ADDED
@@ -0,0 +1,40 @@
1
+ # Gherker Package (`@letsrunit/gherker`)
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ npm install @letsrunit/gherker
7
+ # or
8
+ yarn add @letsrunit/gherker
9
+ ```
10
+
11
+ A lightweight Gherkin microrunner for executing scenarios step-by-step. It provides a simple and fast alternative to full-blown BDD frameworks, optimized for the `letsrunit` platform.
12
+
13
+ ## Exported Classes
14
+
15
+ ### `Runner<TWorld>`
16
+
17
+ The core class for defining and running Gherkin steps.
18
+
19
+ #### Methods:
20
+
21
+ - **`defineStep(type, expression, fn, comment)`**: Defines a new Gherkin step.
22
+ - `type`: `'Given' | 'When' | 'Then' | 'And' | 'But'`.
23
+ - `expression`: A string or regular expression. Supports Cucumber expressions.
24
+ - `fn`: The handler function. Receives arguments from the expression and the world object as `this`.
25
+ - **`defineParameterType(type)`**: Registers a custom parameter type for Cucumber expressions.
26
+ - **`parse(feature)`**: Parses a Gherkin feature and returns a list of its steps and their matches.
27
+ - **`run(feature, worldFactory, wrapRun, opts)`**: Executes a Gherkin feature.
28
+ - `feature`: The Gherkin feature text.
29
+ - `worldFactory`: A value or function that provides the `World` object.
30
+ - `wrapRun`: An optional wrapper for each step execution (useful for logging or reporting).
31
+ - `opts`: Includes an `AbortSignal`.
32
+ - **`reset()`**: Clears all defined steps and parameter types.
33
+
34
+ ## Testing
35
+
36
+ Run tests for this package:
37
+
38
+ ```bash
39
+ yarn test
40
+ ```
@@ -0,0 +1,60 @@
1
+ import { Expression, Argument, ParameterTypeRegistry } from '@cucumber/cucumber-expressions';
2
+ import { ParameterTypeDefinition } from '@letsrunit/gherkin';
3
+
4
+ type World = {
5
+ cleanup?: () => void | Promise<void>;
6
+ [_: string]: any;
7
+ };
8
+ type StepHandler<T> = (this: T, ...args: any[]) => Promise<void> | void;
9
+ type StepType = 'Given' | 'When' | 'Then';
10
+ interface StepDefinition<T> {
11
+ type: StepType;
12
+ expr: Expression;
13
+ fn: StepHandler<T>;
14
+ source: string;
15
+ comment?: string;
16
+ }
17
+ interface ParsedStep {
18
+ text: string;
19
+ def?: string;
20
+ values?: unknown[];
21
+ }
22
+ interface Result<TWorld extends World = any> {
23
+ world: TWorld;
24
+ status: 'passed' | 'failed';
25
+ steps: Array<{
26
+ text: string;
27
+ status?: 'success' | 'failure';
28
+ }>;
29
+ reason?: Error;
30
+ }
31
+
32
+ type StepResult = {
33
+ status: 'success' | 'failure';
34
+ reason?: Error;
35
+ };
36
+ type StepDescription = {
37
+ id: string;
38
+ text: string;
39
+ args: readonly Argument[];
40
+ };
41
+ type StepWrapper = (step: StepDescription, run: () => Promise<StepResult>) => Promise<StepResult>;
42
+ declare class Runner<TWorld extends World> {
43
+ private _registry;
44
+ get registry(): ParameterTypeRegistry;
45
+ private _defs;
46
+ get defs(): StepDefinition<TWorld>[];
47
+ defineStep(type: StepType, expression: string | RegExp, fn: StepHandler<TWorld>, comment?: string): void;
48
+ defineParameterType(type: ParameterTypeDefinition<unknown>): void;
49
+ parse(feature: string): ParsedStep[];
50
+ run(feature: string, worldFactory: TWorld | (() => Promise<TWorld> | TWorld), wrapRun?: StepWrapper, { signal }?: {
51
+ signal?: AbortSignal;
52
+ }): Promise<Result<TWorld>>;
53
+ reset(): void;
54
+ private match;
55
+ private describeStep;
56
+ private compile;
57
+ private runStep;
58
+ }
59
+
60
+ export { type ParsedStep, type Result, Runner, type StepDescription, type StepHandler, type StepResult };
package/dist/index.js ADDED
@@ -0,0 +1,123 @@
1
+ import { ParameterTypeRegistry, CucumberExpression, RegularExpression, ParameterType } from '@cucumber/cucumber-expressions';
2
+ import { generateMessages } from '@cucumber/gherkin';
3
+ import { SourceMediaType, IdGenerator } from '@cucumber/messages';
4
+ import { sanitizeStepDefinition } from '@letsrunit/gherkin';
5
+
6
+ // src/runner.ts
7
+ var Runner = class {
8
+ _registry = new ParameterTypeRegistry();
9
+ // Private instead of readonly, because `reset()`
10
+ get registry() {
11
+ return this._registry;
12
+ }
13
+ _defs = [];
14
+ get defs() {
15
+ return this._defs;
16
+ }
17
+ defineStep(type, expression, fn, comment) {
18
+ const expr = typeof expression === "string" ? new CucumberExpression(sanitizeStepDefinition(expression), this.registry) : new RegularExpression(expression, this.registry);
19
+ this.defs.push({ type, expr, fn, source: String(expression), comment });
20
+ }
21
+ defineParameterType(type) {
22
+ const paramType = new ParameterType(type.name, type.regexp, null, type.transformer, type.useForSnippets);
23
+ this._registry.defineParameterType(paramType);
24
+ }
25
+ parse(feature) {
26
+ const pickles = this.compile(feature);
27
+ if (pickles.length > 1) {
28
+ throw new Error("Multiple scenarios not supported");
29
+ }
30
+ const pickle = pickles[0];
31
+ return pickle.steps.map((step) => {
32
+ const match = this.match(step.text);
33
+ return {
34
+ text: step.text,
35
+ def: match?.def ? `${match?.def.type} ${match?.def.source}` : void 0,
36
+ values: match?.values
37
+ };
38
+ });
39
+ }
40
+ async run(feature, worldFactory, wrapRun, { signal } = {}) {
41
+ const pickles = this.compile(feature);
42
+ if (pickles.length > 1) {
43
+ throw new Error("Multiple scenarios not supported");
44
+ }
45
+ wrapRun ??= (_step, run) => run();
46
+ const world = typeof worldFactory === "function" ? await worldFactory() : worldFactory;
47
+ const pickle = pickles[0];
48
+ let completed = 0;
49
+ let error;
50
+ for (const step of pickle.steps) {
51
+ if (signal?.aborted) {
52
+ error = signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
53
+ break;
54
+ }
55
+ try {
56
+ const { status, reason } = await wrapRun(this.describeStep(step), () => this.runStep(world, step));
57
+ if (status === "success") completed++;
58
+ if (status === "failure") {
59
+ error = reason ?? new Error("Unknown error");
60
+ break;
61
+ }
62
+ } catch (e) {
63
+ error = e;
64
+ break;
65
+ }
66
+ }
67
+ if (typeof world.cleanup === "function") await world.cleanup();
68
+ const steps = pickle.steps.map((step, i) => ({
69
+ text: step.text,
70
+ status: completed > i ? "success" : completed === i ? "failure" : void 0
71
+ }));
72
+ return { world, status: !error ? "passed" : "failed", steps, reason: error };
73
+ }
74
+ reset() {
75
+ this._registry = new ParameterTypeRegistry();
76
+ this._defs = [];
77
+ }
78
+ match(text) {
79
+ for (const def of this.defs) {
80
+ const args = def.expr.match(text);
81
+ if (args) return { def, values: args.map((a) => a.getValue(null)) };
82
+ }
83
+ return null;
84
+ }
85
+ describeStep(step) {
86
+ const def = this.defs.find(({ expr }) => !!expr.match(step.text));
87
+ const text = def ? `${def.type} ${step.text}` : step.text;
88
+ const args = def?.expr.match(step.text) || [];
89
+ return { id: step.id, text, args };
90
+ }
91
+ compile(feature, uri = "inline.feature") {
92
+ const envelopes = generateMessages(feature, uri, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, {
93
+ newId: () => IdGenerator.uuid().toString(),
94
+ includeGherkinDocument: true,
95
+ includePickles: true,
96
+ includeSource: false
97
+ });
98
+ const pickles = envelopes.filter((e) => e.pickle).map((e) => e.pickle);
99
+ if (!pickles.length) throw new Error("No scenarios found");
100
+ return pickles;
101
+ }
102
+ async runStep(world, step) {
103
+ try {
104
+ const text = step.text;
105
+ const match = this.match(text);
106
+ if (!match) throw new Error(`Undefined step: ${text}`);
107
+ let extra;
108
+ if (step.argument?.docString) {
109
+ extra = step.argument.docString.content;
110
+ } else if (step.argument?.dataTable) {
111
+ extra = step.argument.dataTable.rows.map((r) => r.cells.map((c) => c.value));
112
+ }
113
+ await match.def.fn.apply(world, [...match.values, ...extra !== void 0 ? [extra] : []]);
114
+ return { status: "success" };
115
+ } catch (e) {
116
+ return { status: "failure", reason: e };
117
+ }
118
+ }
119
+ };
120
+
121
+ export { Runner };
122
+ //# sourceMappingURL=index.js.map
123
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n Argument,\n CucumberExpression,\n ParameterType,\n ParameterTypeRegistry,\n RegularExpression,\n} from '@cucumber/cucumber-expressions';\nimport { generateMessages } from '@cucumber/gherkin';\nimport { IdGenerator, Pickle, PickleStep, SourceMediaType } from '@cucumber/messages';\nimport { ParameterTypeDefinition, sanitizeStepDefinition } from '@letsrunit/gherkin';\nimport { ParsedStep, Result, StepDefinition, StepHandler, StepType, World } from './types';\n\nexport type StepResult = { status: 'success' | 'failure'; reason?: Error };\nexport type StepDescription = { id: string, text: string; args: readonly Argument[] };\ntype StepWrapper = (step: StepDescription, run: () => Promise<StepResult>) => Promise<StepResult>;\n\nexport class Runner<TWorld extends World> {\n private _registry = new ParameterTypeRegistry(); // Private instead of readonly, because `reset()`\n\n get registry(): ParameterTypeRegistry {\n return this._registry;\n }\n\n private _defs: StepDefinition<TWorld>[] = [];\n\n get defs(): StepDefinition<TWorld>[] {\n return this._defs;\n }\n\n defineStep(type: StepType, expression: string | RegExp, fn: StepHandler<TWorld>, comment?: string) {\n const expr =\n typeof expression === 'string'\n ? new CucumberExpression(sanitizeStepDefinition(expression), this.registry)\n : new RegularExpression(expression, this.registry);\n this.defs.push({ type, expr, fn, source: String(expression), comment });\n }\n\n defineParameterType(type: ParameterTypeDefinition<unknown>) {\n const paramType = new ParameterType(type.name, type.regexp, null, type.transformer, type.useForSnippets);\n\n this._registry.defineParameterType(paramType);\n }\n\n parse(feature: string): ParsedStep[] {\n const pickles = this.compile(feature);\n if (pickles.length > 1) {\n throw new Error('Multiple scenarios not supported');\n }\n\n const pickle = pickles[0];\n\n return pickle.steps.map((step) => {\n const match = this.match(step.text);\n\n return {\n text: step.text,\n def: match?.def ? `${match?.def.type} ${match?.def.source}` : undefined,\n values: match?.values,\n };\n });\n }\n\n async run(\n feature: string,\n worldFactory: TWorld | (() => Promise<TWorld> | TWorld),\n wrapRun?: StepWrapper,\n { signal }: { signal?: AbortSignal } = {},\n ): Promise<Result<TWorld>> {\n const pickles = this.compile(feature);\n if (pickles.length > 1) {\n throw new Error('Multiple scenarios not supported');\n }\n\n wrapRun ??= (_step, run) => run();\n\n const world = typeof worldFactory === 'function' ? await worldFactory() : worldFactory;\n const pickle = pickles[0];\n let completed = 0;\n let error: Error | undefined;\n\n for (const step of pickle.steps) {\n if (signal?.aborted) {\n error = signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));\n break;\n }\n\n try {\n const { status, reason } = await wrapRun(this.describeStep(step), () => this.runStep(world, step));\n\n if (status === 'success') completed++;\n\n if (status === 'failure') {\n error = reason ?? new Error('Unknown error');\n break;\n }\n } catch (e) {\n error = e as Error;\n break;\n }\n }\n\n if (typeof world.cleanup === 'function') await world.cleanup();\n\n const steps = pickle.steps.map((step, i) => ({\n text: step.text,\n status:\n completed > i ? 'success' : ((completed === i ? 'failure' : undefined) as 'success' | 'failure' | undefined),\n }));\n\n return { world, status: !error ? 'passed' : 'failed', steps, reason: error };\n }\n\n reset() {\n this._registry = new ParameterTypeRegistry();\n this._defs = [];\n }\n\n private match(text: string) {\n for (const def of this.defs) {\n const args = def.expr.match(text);\n if (args) return { def, values: args.map((a) => a.getValue(null)) };\n }\n return null;\n }\n\n private describeStep(step: PickleStep): StepDescription {\n const def = this.defs.find(({ expr }) => !!expr.match(step.text));\n\n const text = def ? `${def.type} ${step.text}` : step.text;\n const args = def?.expr.match(step.text) || [];\n\n return { id: step.id, text, args };\n }\n\n private compile(feature: string, uri = 'inline.feature') {\n const envelopes = generateMessages(feature, uri, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, {\n newId: () => IdGenerator.uuid().toString(),\n includeGherkinDocument: true,\n includePickles: true,\n includeSource: false,\n });\n const pickles = envelopes.filter((e) => e.pickle).map((e) => e.pickle!) as Pickle[];\n if (!pickles.length) throw new Error('No scenarios found');\n return pickles;\n }\n\n private async runStep(world: TWorld, step: PickleStep): Promise<StepResult> {\n try {\n const text = step.text;\n const match = this.match(text);\n\n if (!match) throw new Error(`Undefined step: ${text}`);\n\n // DocString/DataTable\n let extra: any | undefined;\n if (step.argument?.docString) {\n extra = step.argument.docString.content;\n } else if (step.argument?.dataTable) {\n extra = step.argument.dataTable.rows.map((r) => r.cells.map((c) => c.value));\n }\n\n await match.def.fn.apply(world, [...match.values, ...(extra !== undefined ? [extra] : [])]);\n\n return { status: 'success' };\n } catch (e) {\n return { status: 'failure', reason: e as Error };\n }\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@letsrunit/gherker",
3
+ "version": "0.1.0",
4
+ "description": "A gherkin microrunner",
5
+ "keywords": [
6
+ "testing",
7
+ "gherkin",
8
+ "runner",
9
+ "letsrunit"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/letsrunit/letsrunit.git",
15
+ "directory": "packages/gherker"
16
+ },
17
+ "bugs": "https://github.com/letsrunit/letsrunit/issues",
18
+ "homepage": "https://github.com/letsrunit/letsrunit#readme",
19
+ "private": false,
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "../../node_modules/.bin/tsup",
32
+ "test": "vitest run",
33
+ "test:cov": "vitest run --coverage",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "packageManager": "yarn@4.10.3",
37
+ "dependencies": {
38
+ "@cucumber/cucumber-expressions": "^18.1.0",
39
+ "@cucumber/gherkin": "^37.0.1",
40
+ "@cucumber/messages": "^31.2.0",
41
+ "@letsrunit/gherkin": "workspace:*"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.0.9",
45
+ "vitest": "^4.0.17"
46
+ },
47
+ "module": "./dist/index.js",
48
+ "types": "./dist/index.d.ts",
49
+ "exports": {
50
+ ".": {
51
+ "types": "./dist/index.d.ts",
52
+ "import": "./dist/index.js"
53
+ }
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './runner';
2
+ export type { Result, StepHandler, ParsedStep } from "./types";
package/src/runner.ts ADDED
@@ -0,0 +1,169 @@
1
+ import {
2
+ Argument,
3
+ CucumberExpression,
4
+ ParameterType,
5
+ ParameterTypeRegistry,
6
+ RegularExpression,
7
+ } from '@cucumber/cucumber-expressions';
8
+ import { generateMessages } from '@cucumber/gherkin';
9
+ import { IdGenerator, Pickle, PickleStep, SourceMediaType } from '@cucumber/messages';
10
+ import { ParameterTypeDefinition, sanitizeStepDefinition } from '@letsrunit/gherkin';
11
+ import { ParsedStep, Result, StepDefinition, StepHandler, StepType, World } from './types';
12
+
13
+ export type StepResult = { status: 'success' | 'failure'; reason?: Error };
14
+ export type StepDescription = { id: string, text: string; args: readonly Argument[] };
15
+ type StepWrapper = (step: StepDescription, run: () => Promise<StepResult>) => Promise<StepResult>;
16
+
17
+ export class Runner<TWorld extends World> {
18
+ private _registry = new ParameterTypeRegistry(); // Private instead of readonly, because `reset()`
19
+
20
+ get registry(): ParameterTypeRegistry {
21
+ return this._registry;
22
+ }
23
+
24
+ private _defs: StepDefinition<TWorld>[] = [];
25
+
26
+ get defs(): StepDefinition<TWorld>[] {
27
+ return this._defs;
28
+ }
29
+
30
+ defineStep(type: StepType, expression: string | RegExp, fn: StepHandler<TWorld>, comment?: string) {
31
+ const expr =
32
+ typeof expression === 'string'
33
+ ? new CucumberExpression(sanitizeStepDefinition(expression), this.registry)
34
+ : new RegularExpression(expression, this.registry);
35
+ this.defs.push({ type, expr, fn, source: String(expression), comment });
36
+ }
37
+
38
+ defineParameterType(type: ParameterTypeDefinition<unknown>) {
39
+ const paramType = new ParameterType(type.name, type.regexp, null, type.transformer, type.useForSnippets);
40
+
41
+ this._registry.defineParameterType(paramType);
42
+ }
43
+
44
+ parse(feature: string): ParsedStep[] {
45
+ const pickles = this.compile(feature);
46
+ if (pickles.length > 1) {
47
+ throw new Error('Multiple scenarios not supported');
48
+ }
49
+
50
+ const pickle = pickles[0];
51
+
52
+ return pickle.steps.map((step) => {
53
+ const match = this.match(step.text);
54
+
55
+ return {
56
+ text: step.text,
57
+ def: match?.def ? `${match?.def.type} ${match?.def.source}` : undefined,
58
+ values: match?.values,
59
+ };
60
+ });
61
+ }
62
+
63
+ async run(
64
+ feature: string,
65
+ worldFactory: TWorld | (() => Promise<TWorld> | TWorld),
66
+ wrapRun?: StepWrapper,
67
+ { signal }: { signal?: AbortSignal } = {},
68
+ ): Promise<Result<TWorld>> {
69
+ const pickles = this.compile(feature);
70
+ if (pickles.length > 1) {
71
+ throw new Error('Multiple scenarios not supported');
72
+ }
73
+
74
+ wrapRun ??= (_step, run) => run();
75
+
76
+ const world = typeof worldFactory === 'function' ? await worldFactory() : worldFactory;
77
+ const pickle = pickles[0];
78
+ let completed = 0;
79
+ let error: Error | undefined;
80
+
81
+ for (const step of pickle.steps) {
82
+ if (signal?.aborted) {
83
+ error = signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
84
+ break;
85
+ }
86
+
87
+ try {
88
+ const { status, reason } = await wrapRun(this.describeStep(step), () => this.runStep(world, step));
89
+
90
+ if (status === 'success') completed++;
91
+
92
+ if (status === 'failure') {
93
+ error = reason ?? new Error('Unknown error');
94
+ break;
95
+ }
96
+ } catch (e) {
97
+ error = e as Error;
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (typeof world.cleanup === 'function') await world.cleanup();
103
+
104
+ const steps = pickle.steps.map((step, i) => ({
105
+ text: step.text,
106
+ status:
107
+ completed > i ? 'success' : ((completed === i ? 'failure' : undefined) as 'success' | 'failure' | undefined),
108
+ }));
109
+
110
+ return { world, status: !error ? 'passed' : 'failed', steps, reason: error };
111
+ }
112
+
113
+ reset() {
114
+ this._registry = new ParameterTypeRegistry();
115
+ this._defs = [];
116
+ }
117
+
118
+ private match(text: string) {
119
+ for (const def of this.defs) {
120
+ const args = def.expr.match(text);
121
+ if (args) return { def, values: args.map((a) => a.getValue(null)) };
122
+ }
123
+ return null;
124
+ }
125
+
126
+ private describeStep(step: PickleStep): StepDescription {
127
+ const def = this.defs.find(({ expr }) => !!expr.match(step.text));
128
+
129
+ const text = def ? `${def.type} ${step.text}` : step.text;
130
+ const args = def?.expr.match(step.text) || [];
131
+
132
+ return { id: step.id, text, args };
133
+ }
134
+
135
+ private compile(feature: string, uri = 'inline.feature') {
136
+ const envelopes = generateMessages(feature, uri, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, {
137
+ newId: () => IdGenerator.uuid().toString(),
138
+ includeGherkinDocument: true,
139
+ includePickles: true,
140
+ includeSource: false,
141
+ });
142
+ const pickles = envelopes.filter((e) => e.pickle).map((e) => e.pickle!) as Pickle[];
143
+ if (!pickles.length) throw new Error('No scenarios found');
144
+ return pickles;
145
+ }
146
+
147
+ private async runStep(world: TWorld, step: PickleStep): Promise<StepResult> {
148
+ try {
149
+ const text = step.text;
150
+ const match = this.match(text);
151
+
152
+ if (!match) throw new Error(`Undefined step: ${text}`);
153
+
154
+ // DocString/DataTable
155
+ let extra: any | undefined;
156
+ if (step.argument?.docString) {
157
+ extra = step.argument.docString.content;
158
+ } else if (step.argument?.dataTable) {
159
+ extra = step.argument.dataTable.rows.map((r) => r.cells.map((c) => c.value));
160
+ }
161
+
162
+ await match.def.fn.apply(world, [...match.values, ...(extra !== undefined ? [extra] : [])]);
163
+
164
+ return { status: 'success' };
165
+ } catch (e) {
166
+ return { status: 'failure', reason: e as Error };
167
+ }
168
+ }
169
+ }
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { Expression } from '@cucumber/cucumber-expressions';
2
+
3
+ export type World = { cleanup?: () => void | Promise<void>; [_: string]: any };
4
+ export type StepHandler<T> = (this: T, ...args: any[]) => Promise<void> | void;
5
+ export type StepType = 'Given' | 'When' | 'Then';
6
+
7
+ export interface StepDefinition<T> {
8
+ type: StepType;
9
+ expr: Expression;
10
+ fn: StepHandler<T>;
11
+ source: string;
12
+ comment?: string;
13
+ }
14
+
15
+ export interface ParsedStep {
16
+ text: string;
17
+ def?: string;
18
+ values?: unknown[];
19
+ }
20
+
21
+ export interface Result<TWorld extends World = any> {
22
+ world: TWorld;
23
+ status: 'passed' | 'failed';
24
+ steps: Array<{
25
+ text: string;
26
+ status?: 'success' | 'failure';
27
+ }>;
28
+ reason?: Error;
29
+ }