vite-plugin-gherkin 0.0.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/data-table.d.ts +9 -0
- package/dist/data-table.js +40 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/internal.d.ts +10 -0
- package/dist/internal.js +102 -0
- package/dist/step-registry.d.ts +25 -0
- package/dist/step-registry.js +26 -0
- package/dist/vite-plugin-gherkin.d.ts +6 -0
- package/dist/vite-plugin-gherkin.js +118 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Computost Consulting LLC, Joshua Hogsett
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# vite-plugin-gherkin
|
|
2
|
+
|
|
3
|
+
`vite-plugin-gherkin` is a plugin for [Vitest](https://vitest.dev/) that enables you to run your Gherkin feature files as tests. This tool is an attempt to combine the best features of Cucumber/Gherkin with the paradigms and idioms of Vitest as a testing framework.
|
|
4
|
+
|
|
5
|
+
Check out [Cucumber](https://cucumber.io/) for more info regarding the Gherkin syntax.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
In your [Vitest configuration file](https://vitest.dev/config/), import and register a `vitePluginGherkin` instance. This will allow you to include Gherkin feature files in your test configuration. Also, don't forget to include your step definitions in your test setup:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { vitePluginGherkin } from "vite-plugin-gherkin";
|
|
13
|
+
import { defineConfig } from "vitest/config";
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [vitePluginGherkin()],
|
|
17
|
+
test: {
|
|
18
|
+
include: ["features/**/*.feature"],
|
|
19
|
+
setupFiles: "features/step-definitions.ts",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> Because these will all run using the Vitest runner, both JavaScript and TypeScript are supported.
|
|
26
|
+
|
|
27
|
+
## Step Definitions
|
|
28
|
+
|
|
29
|
+
When you write tests with Gherkin, your feature files will have Gherkin steps. `vite-plugin-gherkin` will match these Gherkin steps with step definitions that you will implement to manipulate the system.
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
1[Steps in Gherkin] -- matched with --> 2[Step Definitions] -- manipulates --> 3[System]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For an in-depth description on Gherkin and Behavior-Driven Development (BDD), please review [Cucumber's documentation](https://cucumber.io/docs/).
|
|
37
|
+
|
|
38
|
+
For example, a Gherkin file may have the following steps:
|
|
39
|
+
|
|
40
|
+
```gherkin
|
|
41
|
+
Scenario: I'm hungry
|
|
42
|
+
Given I have 10 pickles
|
|
43
|
+
When I eat 3 pickles
|
|
44
|
+
Then I have 7 pickles left
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In order for `vite-plugin-gherkin` to match Gherkin steps with step definitions, they need to be registered during test setup. This can be done by importing the registry functions `Given`, `When`, and/or `Then` from `vite-plugin-gherkin` and registering step patterns with an implementation.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Given, When, Then } from "vite-plugin-gherkin";
|
|
51
|
+
|
|
52
|
+
Given("I have {int} pickles", ([picklesOwned]) => {
|
|
53
|
+
// ...
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
When("I eat {int} pickles", ([picklesToEat]) => {
|
|
57
|
+
// ...
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
Then("I have {int} pickles left", ([expectedPickles]) => {
|
|
61
|
+
// ...
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`vite-plugin-gherkin` uses [Cucumber Expressions](https://github.com/cucumber/cucumber-expressions) and Regular Expressions for pattern matching. Similar to Vitest's [`test.for`](https://vitest.dev/api/#test-for), template output parameters are passed in order in an array as the 1st argument to the step implementation function.
|
|
66
|
+
|
|
67
|
+
### Sharing State between Steps
|
|
68
|
+
|
|
69
|
+
In order to track and share state between step definitions, `vite-plugin-gherkin` uses Vitest's [Test Context](https://vitest.dev/guide/test-context.html). Just like Vitest, you can [extend Test Context](https://vitest.dev/guide/test-context.html#extend-test-context) to setup and teardown fixtures to support your tests.
|
|
70
|
+
|
|
71
|
+
> [!IMPORTANT]
|
|
72
|
+
> When extending Test Context, your new test function must be exported as `test`.
|
|
73
|
+
|
|
74
|
+
For example, in JavaScript, you can define your `test-context.js` like so:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { test as base } from "vitest";
|
|
78
|
+
|
|
79
|
+
export const test = base.extend({
|
|
80
|
+
pickles: ({}, use) => use({ count: 0 }),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or in TypeScript, you can define your custom fixtures type and re-export `Given`, `When`, and `Then` for some additional type checking:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { test as base } from "vitest";
|
|
88
|
+
import {
|
|
89
|
+
Given as baseGiven,
|
|
90
|
+
Then as baseThen,
|
|
91
|
+
When as baseWhen,
|
|
92
|
+
type RegisterStep,
|
|
93
|
+
} from "vite-plugin-gherkin";
|
|
94
|
+
|
|
95
|
+
export interface PickleFixtures {
|
|
96
|
+
pickles: { count: number };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const test = base.extend<PickleFixtures>({
|
|
100
|
+
pickles: ({}, use) => use({ count: 0 }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const Given = baseGiven as RegisterStep<PickleFixtures>;
|
|
104
|
+
export const When = baseWhen as RegisterStep<PickleFixtures>;
|
|
105
|
+
export const Then = baseThen as RegisterStep<PickleFixtures>;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When extending Test Context, you must configure the `importTestFrom` option with your test context file:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { vitePluginGherkin } from "vite-plugin-gherkin";
|
|
112
|
+
import { defineConfig } from "vitest/config";
|
|
113
|
+
import { path } from "path";
|
|
114
|
+
|
|
115
|
+
export default defineConfig({
|
|
116
|
+
plugins: [
|
|
117
|
+
vitePluginGherkin({
|
|
118
|
+
importTestFrom: path.resolve(__dirname, "./features/test-context.ts"),
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
test: {
|
|
122
|
+
include: ["features/**/*.feature"],
|
|
123
|
+
setupFiles: "features/step-definitions.ts",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Sticking with the [`test.for`](https://vitest.dev/api/#test-for) analogy, step definition implementation functions will accept a 2nd argument which is the [`TestContext`](https://vitest.dev/guide/test-context.html) object.
|
|
129
|
+
|
|
130
|
+
> [!IMPORTANT]
|
|
131
|
+
> When injecting `TestContext` into a step definition, it must use the object destructuring pattern to communicate to Vitest which dependencies to resolve.
|
|
132
|
+
|
|
133
|
+
For example:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { Given, When, Then } from "./test-context";
|
|
137
|
+
|
|
138
|
+
Given("I have {int} pickles", ([picklesOwned], { pickles }) => {
|
|
139
|
+
pickles.count = picklesOwned;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
When("I eat {int} pickles", ([picklesToEat], { pickles }) => {
|
|
143
|
+
pickles.count -= picklesToEat;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
Then("I have {int} pickles left", ([expectedPickles], { pickles }) => {
|
|
147
|
+
if (pickles.count !== expectedPickles) {
|
|
148
|
+
throw new Error("I don't have the right amount of pickles!");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// borrowed directly from the cucumber-js repo with minor modifications
|
|
2
|
+
// https://github.com/cucumber/cucumber-js/blob/release/v11.3.0/src/models/data_table.ts
|
|
3
|
+
export class DataTable {
|
|
4
|
+
rawTable;
|
|
5
|
+
constructor(rawTable) {
|
|
6
|
+
this.rawTable = rawTable;
|
|
7
|
+
}
|
|
8
|
+
hashes() {
|
|
9
|
+
const copy = this.raw();
|
|
10
|
+
const keys = copy[0];
|
|
11
|
+
const valuesArray = copy.slice(1);
|
|
12
|
+
return valuesArray.map((values) => {
|
|
13
|
+
const rowObject = {};
|
|
14
|
+
keys.forEach((key, index) => (rowObject[key] = values[index]));
|
|
15
|
+
return rowObject;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
raw() {
|
|
19
|
+
return this.rawTable.slice(0);
|
|
20
|
+
}
|
|
21
|
+
rows() {
|
|
22
|
+
const copy = this.raw();
|
|
23
|
+
copy.shift();
|
|
24
|
+
return copy;
|
|
25
|
+
}
|
|
26
|
+
rowsHash() {
|
|
27
|
+
const rows = this.raw();
|
|
28
|
+
const everyRowHasTwoColumns = rows.every((row) => row.length === 2);
|
|
29
|
+
if (!everyRowHasTwoColumns) {
|
|
30
|
+
throw new Error("rowsHash can only be called on a data table where all rows have exactly two columns");
|
|
31
|
+
}
|
|
32
|
+
const result = {};
|
|
33
|
+
rows.forEach((x) => (result[x[0]] = x[1]));
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
transpose() {
|
|
37
|
+
const transposed = this.rawTable[0].map((x, i) => this.rawTable.map((y) => y[i]));
|
|
38
|
+
return new DataTable(transposed);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { DataTable } from "./data-table.ts";
|
|
2
|
+
export type { ParameterTypeArgs, RegisterStep } from "./step-registry.ts";
|
|
3
|
+
export { vitePluginGherkin } from "./vite-plugin-gherkin.ts";
|
|
4
|
+
export declare const Given: import("./step-registry.ts").RegisterStep<object>;
|
|
5
|
+
export declare const When: import("./step-registry.ts").RegisterStep<object>;
|
|
6
|
+
export declare const Then: import("./step-registry.ts").RegisterStep<object>;
|
|
7
|
+
declare module "vitest" {
|
|
8
|
+
interface TaskMeta {
|
|
9
|
+
tags?: string[];
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TestAPI, TestContext } from "vitest";
|
|
2
|
+
export { DataTable } from "./data-table.ts";
|
|
3
|
+
export declare function gherkinContext(test: TestAPI): TestAPI<{
|
|
4
|
+
task: Readonly<import("vitest").RunnerTestCase<object>>;
|
|
5
|
+
__gherkin_tags: string[];
|
|
6
|
+
}>;
|
|
7
|
+
export declare function buildTestFunction(testSteps: <T>(step: (text: string, doc?: string) => T) => Generator<T>): (() => void) | {
|
|
8
|
+
(context: TestContext & unknown): Promise<void>;
|
|
9
|
+
toString(): string;
|
|
10
|
+
};
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { stripLiteral } from "strip-literal";
|
|
2
|
+
import { getStep } from "./step-registry.js";
|
|
3
|
+
export { DataTable } from "./data-table.js";
|
|
4
|
+
export function gherkinContext(test) {
|
|
5
|
+
return test.extend({
|
|
6
|
+
__gherkin_tags: ({}, use) => use([]),
|
|
7
|
+
task: [
|
|
8
|
+
({ __gherkin_tags, task }, use) => {
|
|
9
|
+
task.meta.tags = __gherkin_tags;
|
|
10
|
+
return use(task);
|
|
11
|
+
},
|
|
12
|
+
{ auto: true },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function buildTestFunction(testSteps) {
|
|
17
|
+
const steps = Array.from(testSteps(getStep));
|
|
18
|
+
if (!allDefined(steps)) {
|
|
19
|
+
return function reportMissingSteps() {
|
|
20
|
+
let i = 0;
|
|
21
|
+
testSteps((text) => {
|
|
22
|
+
if (!steps[i]) {
|
|
23
|
+
throw new Error(`Undefined step: ${text}`);
|
|
24
|
+
}
|
|
25
|
+
}).forEach(() => i++);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const scenarioFunction = async function scenarioFunction(context) {
|
|
29
|
+
let i = 0;
|
|
30
|
+
for (const task of testSteps((_, doc) => {
|
|
31
|
+
const step = steps[i];
|
|
32
|
+
return step.fn([...step.args.map((arg) => arg.getValue(context)), doc], context);
|
|
33
|
+
})) {
|
|
34
|
+
await task;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
scenarioFunction.toString = () => `({${[...new Set(steps.flatMap((step) => getUsedProps(step.fn)))].join(",")}}) => {}`;
|
|
39
|
+
return scenarioFunction;
|
|
40
|
+
}
|
|
41
|
+
function allDefined(arr) {
|
|
42
|
+
return arr.every((e) => e != null);
|
|
43
|
+
}
|
|
44
|
+
// borrowed directly from the vitest repo with minor modifications
|
|
45
|
+
// https://github.com/vitest-dev/vitest/blob/v3.2.4/packages/runner/src/fixture.ts#L341
|
|
46
|
+
function getUsedProps(fn) {
|
|
47
|
+
let fnString = stripLiteral(fn.toString());
|
|
48
|
+
// match lowered async function and strip it off
|
|
49
|
+
// example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ
|
|
50
|
+
// __async(this, null, function*
|
|
51
|
+
// __async(this, arguments, function*
|
|
52
|
+
// __async(this, [_0, _1], function*
|
|
53
|
+
if (/__async\((?:this|null), (?:null|arguments|\[[_0-9, ]*\]), function\*/.test(fnString)) {
|
|
54
|
+
fnString = fnString.split(/__async\((?:this|null),/)[1];
|
|
55
|
+
}
|
|
56
|
+
const match = fnString.match(/[^(]*\(([^)]*)/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const args = splitByComma(match[1]);
|
|
61
|
+
if (args.length <= 1) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const fixtureArg = args[1];
|
|
65
|
+
if (!(fixtureArg.startsWith("{") && fixtureArg.endsWith("}"))) {
|
|
66
|
+
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${fixtureArg}".`);
|
|
67
|
+
}
|
|
68
|
+
const _first = fixtureArg.slice(1, -1).replace(/\s/g, "");
|
|
69
|
+
const props = splitByComma(_first).map((prop) => {
|
|
70
|
+
return prop.replace(/:.*|=.*/g, "");
|
|
71
|
+
});
|
|
72
|
+
const last = props.at(-1);
|
|
73
|
+
if (last && last.startsWith("...")) {
|
|
74
|
+
throw new Error(`Rest parameters are not supported in fixtures, received "${last}".`);
|
|
75
|
+
}
|
|
76
|
+
return props;
|
|
77
|
+
}
|
|
78
|
+
function splitByComma(s) {
|
|
79
|
+
const result = [];
|
|
80
|
+
const stack = [];
|
|
81
|
+
let start = 0;
|
|
82
|
+
for (let i = 0; i < s.length; i++) {
|
|
83
|
+
if (s[i] === "{" || s[i] === "[") {
|
|
84
|
+
stack.push(s[i] === "{" ? "}" : "]");
|
|
85
|
+
}
|
|
86
|
+
else if (s[i] === stack[stack.length - 1]) {
|
|
87
|
+
stack.pop();
|
|
88
|
+
}
|
|
89
|
+
else if (!stack.length && s[i] === ",") {
|
|
90
|
+
const token = s.substring(start, i).trim();
|
|
91
|
+
if (token) {
|
|
92
|
+
result.push(token);
|
|
93
|
+
}
|
|
94
|
+
start = i + 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const lastToken = s.substring(start).trim();
|
|
98
|
+
if (lastToken) {
|
|
99
|
+
result.push(lastToken);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TestContext } from "vitest";
|
|
2
|
+
import type { DataTable } from "./data-table.ts";
|
|
3
|
+
type CucumberArgs<Expression extends string> = Expression extends `${string}{${infer Arg}}${infer Post}` ? [
|
|
4
|
+
Arg extends keyof ParameterTypeArgs ? ParameterTypeArgs[Arg] : unknown,
|
|
5
|
+
...CucumberArgs<Post>
|
|
6
|
+
] : [];
|
|
7
|
+
export interface ParameterTypeArgs {
|
|
8
|
+
"": string;
|
|
9
|
+
float: number;
|
|
10
|
+
int: number;
|
|
11
|
+
string: string;
|
|
12
|
+
word: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RegisterStep<ExtraContext = unknown> {
|
|
15
|
+
<Expression extends string>(expression: Expression, step: StepFunction<ExtraContext, CucumberArgs<Expression>>): void;
|
|
16
|
+
(regExp: RegExp, step: StepFunction<ExtraContext, string[]>): void;
|
|
17
|
+
}
|
|
18
|
+
type StepArgs<Args extends unknown[]> = [...Args, DataTable] | [...Args, string] | Args;
|
|
19
|
+
export type StepFunction<ExtraContext = unknown, Args extends unknown[] = unknown[]> = (args: StepArgs<Args>, context: ExtraContext & TestContext) => Promise<void> | void;
|
|
20
|
+
export declare const registerStep: RegisterStep<object>;
|
|
21
|
+
export declare function getStep(step: string): {
|
|
22
|
+
args: readonly import("@cucumber/cucumber-expressions").Argument[];
|
|
23
|
+
fn: StepFunction<unknown, unknown[]>;
|
|
24
|
+
} | null;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CucumberExpression, ParameterTypeRegistry, RegularExpression, } from "@cucumber/cucumber-expressions";
|
|
2
|
+
const stepRegistry = [];
|
|
3
|
+
const parameterTypeRegistry = new ParameterTypeRegistry();
|
|
4
|
+
export const registerStep = (expression, step) => {
|
|
5
|
+
if (typeof expression === "string") {
|
|
6
|
+
stepRegistry.push({
|
|
7
|
+
expression: new CucumberExpression(expression, parameterTypeRegistry),
|
|
8
|
+
step: step,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
stepRegistry.push({
|
|
13
|
+
expression: new RegularExpression(expression, parameterTypeRegistry),
|
|
14
|
+
step: step,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export function getStep(step) {
|
|
19
|
+
for (const { expression, step: fn } of stepRegistry) {
|
|
20
|
+
const args = expression.match(step);
|
|
21
|
+
if (args) {
|
|
22
|
+
return { args, fn };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { AstBuilder, GherkinClassicTokenMatcher, Parser, } from "@cucumber/gherkin";
|
|
2
|
+
import { IdGenerator, } from "@cucumber/messages";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { SourceNode } from "source-map-generator";
|
|
5
|
+
const defaultConfig = {
|
|
6
|
+
importTestFrom: "vitest",
|
|
7
|
+
};
|
|
8
|
+
export function vitePluginGherkin({ importTestFrom = defaultConfig.importTestFrom, } = defaultConfig) {
|
|
9
|
+
const uuidFn = IdGenerator.uuid();
|
|
10
|
+
const builder = new AstBuilder(uuidFn);
|
|
11
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
12
|
+
const parser = new Parser(builder, matcher);
|
|
13
|
+
return {
|
|
14
|
+
name: "vite-plugin-gherkin",
|
|
15
|
+
transform(code, id) {
|
|
16
|
+
if (path.extname(id) === ".feature") {
|
|
17
|
+
const gherkinDocument = parser.parse(code);
|
|
18
|
+
if (gherkinDocument.feature) {
|
|
19
|
+
const source = new SourceNode()
|
|
20
|
+
.add(`import { describe, beforeEach } from "vitest";\n`)
|
|
21
|
+
.add(`import { test as base } from ${JSON.stringify(importTestFrom)};\n`)
|
|
22
|
+
.add(`import { buildTestFunction, DataTable, gherkinContext } from "vite-plugin-gherkin/internal";\n`)
|
|
23
|
+
.add("const test = gherkinContext(base);\n")
|
|
24
|
+
.add(buildFeature(gherkinDocument.feature))
|
|
25
|
+
.toStringWithSourceMap();
|
|
26
|
+
source.map.setSourceContent(id, code);
|
|
27
|
+
return {
|
|
28
|
+
code: source.code,
|
|
29
|
+
map: source.map.toString(),
|
|
30
|
+
moduleSideEffects: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function buildFeature(feature) {
|
|
35
|
+
return new SourceNode(feature.location.line, column(feature.location), id, [
|
|
36
|
+
"describe(",
|
|
37
|
+
JSON.stringify(feature.name),
|
|
38
|
+
", ({ scoped }) => {\n",
|
|
39
|
+
...feature.children.map((child) => {
|
|
40
|
+
if (child.rule) {
|
|
41
|
+
return buildRule(feature, child.rule);
|
|
42
|
+
}
|
|
43
|
+
if (child.background) {
|
|
44
|
+
return buildBackground(child.background);
|
|
45
|
+
}
|
|
46
|
+
if (child.scenario) {
|
|
47
|
+
return buildScenario(feature, child.scenario);
|
|
48
|
+
}
|
|
49
|
+
throw new Error("Invalid feature");
|
|
50
|
+
}),
|
|
51
|
+
"});",
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
function buildRule(feature, rule) {
|
|
55
|
+
return new SourceNode(rule.location.line, column(rule.location), id, [
|
|
56
|
+
"describe(",
|
|
57
|
+
JSON.stringify(rule.name),
|
|
58
|
+
", () => {\n",
|
|
59
|
+
...rule.children.map((ruleChild) => {
|
|
60
|
+
if (ruleChild.background) {
|
|
61
|
+
return buildBackground(ruleChild.background);
|
|
62
|
+
}
|
|
63
|
+
if (ruleChild.scenario) {
|
|
64
|
+
return buildScenario(feature, ruleChild.scenario);
|
|
65
|
+
}
|
|
66
|
+
throw new Error("Invalid rule");
|
|
67
|
+
}),
|
|
68
|
+
"});\n",
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
function buildBackground(background) {
|
|
72
|
+
return new SourceNode(background.location.line, column(background.location), id, ["beforeEach(", buildTestFunction(background.steps), ");\n"]);
|
|
73
|
+
}
|
|
74
|
+
function buildScenario(feature, scenario) {
|
|
75
|
+
return new SourceNode(scenario.location.line, column(scenario.location), id, [
|
|
76
|
+
"scoped({ __gherkin_tags: [",
|
|
77
|
+
new SourceNode()
|
|
78
|
+
.add(feature.tags
|
|
79
|
+
.concat(scenario.tags)
|
|
80
|
+
.map((tag) => new SourceNode(tag.location.line, column(tag.location), id, JSON.stringify(tag.name))))
|
|
81
|
+
.join(","),
|
|
82
|
+
"]});\n",
|
|
83
|
+
"test(",
|
|
84
|
+
JSON.stringify(scenario.name),
|
|
85
|
+
", ",
|
|
86
|
+
buildTestFunction(scenario.steps),
|
|
87
|
+
");\n",
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function buildTestFunction(steps) {
|
|
91
|
+
return new SourceNode()
|
|
92
|
+
.add("buildTestFunction(function*(step) {\n")
|
|
93
|
+
.add(steps.map((step) => new SourceNode(step.location.line, column(step.location), id, [
|
|
94
|
+
"yield step(",
|
|
95
|
+
JSON.stringify(step.text),
|
|
96
|
+
",",
|
|
97
|
+
step.dataTable
|
|
98
|
+
? new SourceNode(step.dataTable.location.line, column(step.dataTable.location), id, [
|
|
99
|
+
"new DataTable(",
|
|
100
|
+
JSON.stringify(rawTable(step.dataTable)),
|
|
101
|
+
")",
|
|
102
|
+
])
|
|
103
|
+
: step.docString
|
|
104
|
+
? JSON.stringify(step.docString.content)
|
|
105
|
+
: "undefined",
|
|
106
|
+
");\n",
|
|
107
|
+
])))
|
|
108
|
+
.add("})");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function column(location) {
|
|
114
|
+
return location.column !== undefined ? location.column - 1 : null;
|
|
115
|
+
}
|
|
116
|
+
function rawTable(dataTable) {
|
|
117
|
+
return dataTable.rows.map((row) => row.cells.map((cell) => cell.value));
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-gherkin",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
"./dist/**/*"
|
|
7
|
+
],
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./internal": {
|
|
16
|
+
"type": "./dist/internal.d.ts",
|
|
17
|
+
"default": "./dist/internal.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "^9.38.0",
|
|
26
|
+
"@eslint/json": "^0.13.2",
|
|
27
|
+
"@eslint/markdown": "^7.4.0",
|
|
28
|
+
"@tsconfig/node-ts": "^23.6.1",
|
|
29
|
+
"@tsconfig/node22": "^22.0.2",
|
|
30
|
+
"@types/node": "^24.8.1",
|
|
31
|
+
"cspell": "^9.2.1",
|
|
32
|
+
"eslint": "^9.38.0",
|
|
33
|
+
"eslint-config-prettier": "^10.1.8",
|
|
34
|
+
"eslint-plugin-perfectionist": "^4.15.1",
|
|
35
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
36
|
+
"glob": "^11.0.3",
|
|
37
|
+
"globals": "^16.4.0",
|
|
38
|
+
"jiti": "^2.6.1",
|
|
39
|
+
"lefthook": "^1.13.6",
|
|
40
|
+
"prettier": "3.6.2",
|
|
41
|
+
"prettier-plugin-gherkin": "^3.1.3",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"typescript-eslint": "^8.46.1",
|
|
44
|
+
"vite-plugin-gherkin": "link:",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@cucumber/cucumber-expressions": "^18.0.1",
|
|
49
|
+
"@cucumber/gherkin": "^36.0.0",
|
|
50
|
+
"@cucumber/messages": "^30.1.0",
|
|
51
|
+
"source-map-generator": "^2.0.2",
|
|
52
|
+
"strip-literal": "^3.1.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"vitest": "^3.2.4"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"pretest": "pnpm build",
|
|
59
|
+
"test": "vitest",
|
|
60
|
+
"build": "tsc --project tsconfig.build.json",
|
|
61
|
+
"spellcheck": "cspell lint . --config .vscode/cspell.json",
|
|
62
|
+
"format-gherkin": "prettier --check **/*.feature",
|
|
63
|
+
"lint": "eslint",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|