@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 +40 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +2 -0
- package/src/runner.ts +169 -0
- package/src/types.ts +29 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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":";;;;;;AAgBO,IAAM,SAAN,MAAmC;AAAA,EAChC,SAAA,GAAY,IAAI,qBAAA,EAAsB;AAAA;AAAA,EAE9C,IAAI,QAAA,GAAkC;AACpC,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA,EAEQ,QAAkC,EAAC;AAAA,EAE3C,IAAI,IAAA,GAAiC;AACnC,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEA,UAAA,CAAW,IAAA,EAAgB,UAAA,EAA6B,EAAA,EAAyB,OAAA,EAAkB;AACjG,IAAA,MAAM,OACJ,OAAO,UAAA,KAAe,QAAA,GAClB,IAAI,mBAAmB,sBAAA,CAAuB,UAAU,CAAA,EAAG,IAAA,CAAK,QAAQ,CAAA,GACxE,IAAI,iBAAA,CAAkB,UAAA,EAAY,KAAK,QAAQ,CAAA;AACrD,IAAA,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,EAAE,IAAA,EAAM,IAAA,EAAM,EAAA,EAAI,MAAA,EAAQ,MAAA,CAAO,UAAU,CAAA,EAAG,OAAA,EAAS,CAAA;AAAA,EACxE;AAAA,EAEA,oBAAoB,IAAA,EAAwC;AAC1D,IAAA,MAAM,SAAA,GAAY,IAAI,aAAA,CAAc,IAAA,CAAK,IAAA,EAAM,IAAA,CAAK,MAAA,EAAQ,IAAA,EAAM,IAAA,CAAK,WAAA,EAAa,IAAA,CAAK,cAAc,CAAA;AAEvG,IAAA,IAAA,CAAK,SAAA,CAAU,oBAAoB,SAAS,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAM,OAAA,EAA+B;AACnC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AACpC,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,IACpD;AAEA,IAAA,MAAM,MAAA,GAAS,QAAQ,CAAC,CAAA;AAExB,IAAA,OAAO,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,KAAS;AAChC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAElC,MAAA,OAAO;AAAA,QACL,MAAM,IAAA,CAAK,IAAA;AAAA,QACX,GAAA,EAAK,KAAA,EAAO,GAAA,GAAM,CAAA,EAAG,KAAA,EAAO,GAAA,CAAI,IAAI,CAAA,CAAA,EAAI,KAAA,EAAO,GAAA,CAAI,MAAM,CAAA,CAAA,GAAK,MAAA;AAAA,QAC9D,QAAQ,KAAA,EAAO;AAAA,OACjB;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,IACJ,OAAA,EACA,YAAA,EACA,SACA,EAAE,MAAA,EAAO,GAA8B,EAAC,EACf;AACzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AACpC,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,IACpD;AAEA,IAAA,OAAA,KAAY,CAAC,KAAA,EAAO,GAAA,KAAQ,GAAA,EAAI;AAEhC,IAAA,MAAM,QAAQ,OAAO,YAAA,KAAiB,UAAA,GAAa,MAAM,cAAa,GAAI,YAAA;AAC1E,IAAA,MAAM,MAAA,GAAS,QAAQ,CAAC,CAAA;AACxB,IAAA,IAAI,SAAA,GAAY,CAAA;AAChB,IAAA,IAAI,KAAA;AAEJ,IAAA,KAAA,MAAW,IAAA,IAAQ,OAAO,KAAA,EAAO;AAC/B,MAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,QAAA,KAAA,GAAQ,MAAA,CAAO,MAAA,YAAkB,KAAA,GAAQ,MAAA,CAAO,MAAA,GAAS,IAAI,KAAA,CAAM,MAAA,CAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AACxF,QAAA;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,MAAM,OAAA,CAAQ,IAAA,CAAK,YAAA,CAAa,IAAI,GAAG,MAAM,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,IAAI,CAAC,CAAA;AAEjG,QAAA,IAAI,WAAW,SAAA,EAAW,SAAA,EAAA;AAE1B,QAAA,IAAI,WAAW,SAAA,EAAW;AACxB,UAAA,KAAA,GAAQ,MAAA,IAAU,IAAI,KAAA,CAAM,eAAe,CAAA;AAC3C,UAAA;AAAA,QACF;AAAA,MACF,SAAS,CAAA,EAAG;AACV,QAAA,KAAA,GAAQ,CAAA;AACR,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI,OAAO,KAAA,CAAM,OAAA,KAAY,UAAA,EAAY,MAAM,MAAM,OAAA,EAAQ;AAE7D,IAAA,MAAM,QAAQ,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,MAAM,CAAA,MAAO;AAAA,MAC3C,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,QACE,SAAA,GAAY,CAAA,GAAI,SAAA,GAAc,SAAA,KAAc,IAAI,SAAA,GAAY;AAAA,KAChE,CAAE,CAAA;AAEF,IAAA,OAAO,EAAE,OAAO,MAAA,EAAQ,CAAC,QAAQ,QAAA,GAAW,QAAA,EAAU,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAM;AAAA,EAC7E;AAAA,EAEA,KAAA,GAAQ;AACN,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,qBAAA,EAAsB;AAC3C,IAAA,IAAA,CAAK,QAAQ,EAAC;AAAA,EAChB;AAAA,EAEQ,MAAM,IAAA,EAAc;AAC1B,IAAA,KAAA,MAAW,GAAA,IAAO,KAAK,IAAA,EAAM;AAC3B,MAAA,MAAM,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAChC,MAAA,IAAI,IAAA,EAAM,OAAO,EAAE,GAAA,EAAK,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,IAAI,CAAC,CAAA,EAAE;AAAA,IACpE;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,aAAa,IAAA,EAAmC;AACtD,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,CAAC,EAAE,IAAA,EAAK,KAAM,CAAC,CAAC,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA;AAEhE,IAAA,MAAM,IAAA,GAAO,MAAM,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA,CAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAA,GAAK,IAAA,CAAK,IAAA;AACrD,IAAA,MAAM,OAAO,GAAA,EAAK,IAAA,CAAK,MAAM,IAAA,CAAK,IAAI,KAAK,EAAC;AAE5C,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,CAAK,EAAA,EAAI,MAAM,IAAA,EAAK;AAAA,EACnC;AAAA,EAEQ,OAAA,CAAQ,OAAA,EAAiB,GAAA,GAAM,gBAAA,EAAkB;AACvD,IAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,OAAA,EAAS,GAAA,EAAK,gBAAgB,6BAAA,EAA+B;AAAA,MAC9F,KAAA,EAAO,MAAM,WAAA,CAAY,IAAA,GAAO,QAAA,EAAS;AAAA,MACzC,sBAAA,EAAwB,IAAA;AAAA,MACxB,cAAA,EAAgB,IAAA;AAAA,MAChB,aAAA,EAAe;AAAA,KAChB,CAAA;AACD,IAAA,MAAM,OAAA,GAAU,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAM,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,MAAO,CAAA;AACtE,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,EAAQ,MAAM,IAAI,MAAM,oBAAoB,CAAA;AACzD,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,MAAc,OAAA,CAAQ,KAAA,EAAe,IAAA,EAAuC;AAC1E,IAAA,IAAI;AACF,MAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAClB,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAE7B,MAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,IAAI,CAAA,CAAE,CAAA;AAGrD,MAAA,IAAI,KAAA;AACJ,MAAA,IAAI,IAAA,CAAK,UAAU,SAAA,EAAW;AAC5B,QAAA,KAAA,GAAQ,IAAA,CAAK,SAAS,SAAA,CAAU,OAAA;AAAA,MAClC,CAAA,MAAA,IAAW,IAAA,CAAK,QAAA,EAAU,SAAA,EAAW;AACnC,QAAA,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,KAAK,CAAC,CAAA;AAAA,MAC7E;AAEA,MAAA,MAAM,MAAM,GAAA,CAAI,EAAA,CAAG,KAAA,CAAM,KAAA,EAAO,CAAC,GAAG,KAAA,CAAM,MAAA,EAAQ,GAAI,UAAU,KAAA,CAAA,GAAY,CAAC,KAAK,CAAA,GAAI,EAAG,CAAC,CAAA;AAE1F,MAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAAA,IAC7B,SAAS,CAAA,EAAG;AACV,MAAA,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,CAAA,EAAW;AAAA,IACjD;AAAA,EACF;AACF","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
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
|
+
}
|