racejar 0.0.1
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 +254 -0
- package/package.json +56 -0
- package/src/compile-feature.ts +142 -0
- package/src/create-parameter-type.ts +27 -0
- package/src/index.ts +3 -0
- package/src/jest/index.ts +1 -0
- package/src/jest/jest-gherkin-driver.ts +47 -0
- package/src/step-definitions.ts +86 -0
- package/src/vitest/index.ts +1 -0
- package/src/vitest/vitest-gherkin-driver.ts +47 -0
- package/tsconfig.json +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 - 2024 Sanity.io
|
|
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,254 @@
|
|
|
1
|
+
# `racejar`
|
|
2
|
+
|
|
3
|
+
> A testing framework agnostic [Gherkin](https://cucumber.io/docs/gherkin/reference/) driver
|
|
4
|
+
|
|
5
|
+
`racejar` is a thin wrapper around `@cucumber/*` that allows you to write your tests in Gherkin and run them with [Vitest](https://vitest.dev/), [Jest](https://jestjs.io/) or any other testing framework of your choice.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add --save-dev racejar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage with Vitest
|
|
12
|
+
|
|
13
|
+
Using `racejar` with Vitest requires no additional configuration. Just drop it into a new or an existing test file and get going:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// your.test.ts
|
|
17
|
+
|
|
18
|
+
// Import `Feature` from `racejar/vitest`
|
|
19
|
+
import {Feature} from 'racejar/vitest'
|
|
20
|
+
// Import your raw `.feature` file
|
|
21
|
+
import featureFile from './your.feature?raw'
|
|
22
|
+
|
|
23
|
+
// Run the feature
|
|
24
|
+
Feature({
|
|
25
|
+
featureText: featureFile,
|
|
26
|
+
stepDefinitions: [
|
|
27
|
+
// ...
|
|
28
|
+
],
|
|
29
|
+
parameterTypes: [
|
|
30
|
+
// ...
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage with Jest
|
|
36
|
+
|
|
37
|
+
Using `racejar` with Jest is similar to Vitest. Just import `Feature` from `racejar/jest` instead.
|
|
38
|
+
|
|
39
|
+
However, Jest can't import raw files out of the box. You'll need a transformer. Luckily, it's easy to write and configure a simple one:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// jest.config.ts
|
|
43
|
+
|
|
44
|
+
import type {Config} from 'jest'
|
|
45
|
+
|
|
46
|
+
const config: Config = {
|
|
47
|
+
transform: {
|
|
48
|
+
'\\.feature$': '<rootDir>/feature-file-transformer.js',
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// feature-file-transformer.js
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
process(content) {
|
|
58
|
+
return {
|
|
59
|
+
code: `module.exports = ${JSON.stringify(content)};`,
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage with \<your favourite testing framework\>
|
|
66
|
+
|
|
67
|
+
`Feature` exported from `racejar/vitest` and `racejar/jest` are convenient thin wrappers around the more generic `compileFeature`.
|
|
68
|
+
|
|
69
|
+
If you are unhappy with those wrappers or want to use `racejar` with another framework, then you can compile your feature manually and run your tests using the compiled feature:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {compileFeature} from 'racejar'
|
|
73
|
+
|
|
74
|
+
const feature = compileFeature({
|
|
75
|
+
featureText: featureFile,
|
|
76
|
+
stepDefinitions: [
|
|
77
|
+
// ...
|
|
78
|
+
],
|
|
79
|
+
parameterTypes: [
|
|
80
|
+
// ...
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
for (const scenario of feature.scenarios) {
|
|
85
|
+
// ...
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Define Steps and Parameter Types
|
|
90
|
+
|
|
91
|
+
`stepDefinitions` can be defined inline or separately. They are defined using `Given`, `When`, `Then` exported from `racejar`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import {Given, Then, When} from 'racejar'
|
|
95
|
+
|
|
96
|
+
const stepDefinitions = [Given(/* ... */), When(/* ... */), Then(/* ... */)]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`racejar` will error out and inform you if a step definition is missing or if you accidentally defined duplicate definitions.
|
|
100
|
+
|
|
101
|
+
If you use nonstandard parameter types, then you can define them yourself:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import {createParameterType} from 'racejar'
|
|
105
|
+
|
|
106
|
+
const parameterTypes = [createParameterType(/* ... */)]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Example Usage
|
|
110
|
+
|
|
111
|
+
The following example is taken from the [`editor`](/packages/editor/) package in this repository. For a full example of how to use `racejar`, head over to [/packages/editor/gherkin-tests/](/packages/editor/gherkin-tests/). The package uses `racejar` to run a [Playwright](https://playwright.dev/) E2E test suite powered by [Vitest Browser Mode](https://vitest.dev/guide/browser/).
|
|
112
|
+
|
|
113
|
+
This feature file tests that the editor can annotate text and additionally asserts the text selection after an annotation is applied. It uses 7 steps which need to be defined:
|
|
114
|
+
|
|
115
|
+
```gherkin
|
|
116
|
+
// annotations.feature
|
|
117
|
+
|
|
118
|
+
Feature: Annotations
|
|
119
|
+
|
|
120
|
+
Background:
|
|
121
|
+
Given one editor
|
|
122
|
+
And a global keymap
|
|
123
|
+
|
|
124
|
+
Scenario: Selection after adding an annotation
|
|
125
|
+
Given the text "foo bar baz"
|
|
126
|
+
When "bar" is selected
|
|
127
|
+
And "link" "l1" is toggled
|
|
128
|
+
Then "bar" has marks "l1"
|
|
129
|
+
And "bar" is selected
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Here's a rough idea of how these steps can be defined:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import {Given, Then, When} from 'racejar'
|
|
136
|
+
|
|
137
|
+
// A `context` object is passed around between steps
|
|
138
|
+
// The context can be whatever you want it to be
|
|
139
|
+
type Context = {
|
|
140
|
+
locator: Locator
|
|
141
|
+
keyMap: Map<string, string>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const stepDefinitions = [
|
|
145
|
+
Given('one editor', async (context: Context) => {
|
|
146
|
+
render(<Editor />)
|
|
147
|
+
const locator = page.getByTestId('<editor test ID>')
|
|
148
|
+
context.locator = locator
|
|
149
|
+
await vi.waitFor(() => expect.element(locator).toBeInTheDocument())
|
|
150
|
+
}),
|
|
151
|
+
Given('a global keymap', (context: Context) => {
|
|
152
|
+
context.keyMap = new Map()
|
|
153
|
+
}),
|
|
154
|
+
Given('the text {string}', async (context: Context, text: string) => {
|
|
155
|
+
await userEvent.click(context.locator)
|
|
156
|
+
await userEvent.type(context.locator, text)
|
|
157
|
+
}),
|
|
158
|
+
Given('{string} is selected', async (context: Context, text: string) => {
|
|
159
|
+
// Select `text` in the editor
|
|
160
|
+
}),
|
|
161
|
+
When(
|
|
162
|
+
'{annotation} {keys} is toggled',
|
|
163
|
+
async (
|
|
164
|
+
context: Context,
|
|
165
|
+
annotation: 'comment' | 'link',
|
|
166
|
+
keyKeys: Array<string>,
|
|
167
|
+
) => {
|
|
168
|
+
// Toggle the `annotation` and store the resulting keys on the `context.keyMap`
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
Then(
|
|
172
|
+
'{string} has marks {marks}',
|
|
173
|
+
async (context: Context, text: string, marks: Array<string>) => {
|
|
174
|
+
// Get the actual marks on the `text` and compare them with `marks`
|
|
175
|
+
},
|
|
176
|
+
),
|
|
177
|
+
Then('{text} is selected', async (context: Context, text: Array<string>) => {
|
|
178
|
+
// Assert that the current editor selection matches `text`
|
|
179
|
+
}),
|
|
180
|
+
]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
As you can see, the step definitions declare a few custom parameters, `{annotation}`, `{keys}` and `{text}`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import {createParameterType} from 'racejar'
|
|
187
|
+
|
|
188
|
+
export const parameterTypes = [
|
|
189
|
+
createParameterType({
|
|
190
|
+
name: 'annotation',
|
|
191
|
+
matcher: /"(comment|link)"/,
|
|
192
|
+
}),
|
|
193
|
+
createParameterType({
|
|
194
|
+
name: 'keys',
|
|
195
|
+
matcher: /"(([a-z]\d)(,([a-z]\d))*)"/,
|
|
196
|
+
type: Array,
|
|
197
|
+
transform: (input) => input.split(','),
|
|
198
|
+
}),
|
|
199
|
+
createParameterType({
|
|
200
|
+
name: 'text',
|
|
201
|
+
matcher: /"([a-z-,#>\\n |\[\]]*)"/,
|
|
202
|
+
type: Array,
|
|
203
|
+
transform: parseGherkinTextParameter,
|
|
204
|
+
}),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
function parseGherkinTextParameter(text: string) {
|
|
208
|
+
return text
|
|
209
|
+
.replace(/\|/g, ',|,')
|
|
210
|
+
.split(',')
|
|
211
|
+
.map((span) => span.replace(/\\n/g, '\n'))
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Now, let's run the test using our defined steps and custom parameter types:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// annotations.test.ts
|
|
219
|
+
|
|
220
|
+
import annotationsFeature from './annotations.feature?raw'
|
|
221
|
+
import {parameterTypes} from './parameter-types'
|
|
222
|
+
import {stepDefinitions} from './step-definitions'
|
|
223
|
+
|
|
224
|
+
Feature({
|
|
225
|
+
featureText: annotationsFeature,
|
|
226
|
+
stepDefinitions,
|
|
227
|
+
parameterTypes,
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Tips
|
|
232
|
+
|
|
233
|
+
If TypeScript errors out with `Cannot find module '.your.feature?raw' or its corresponding type declarations.` then you can declare `.*feature?raw` files as modules:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// global.d.ts
|
|
237
|
+
|
|
238
|
+
declare module '*.feature?raw' {
|
|
239
|
+
const content: string
|
|
240
|
+
export default content
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
Use [prettier-plugin-gherkin](https://github.com/mapado/prettier-plugin-gherkin) to automatically format your `.feature` files.
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
// .prettierrc
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
"plugins": ["prettier-plugin-gherkin"]
|
|
253
|
+
}
|
|
254
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "racejar",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A testing framework agnostic Gherkin driver",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cucumber",
|
|
7
|
+
"gherkin",
|
|
8
|
+
"jest",
|
|
9
|
+
"test",
|
|
10
|
+
"vitest"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://www.sanity.io/",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/portabletext/editor/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/portabletext/editor.git",
|
|
19
|
+
"directory": "packages/racejar"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"default": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"./jest": {
|
|
30
|
+
"default": "./src/jest/index.ts",
|
|
31
|
+
"types": "./src/jest/index.ts"
|
|
32
|
+
},
|
|
33
|
+
"./vitest": {
|
|
34
|
+
"default": "./src/vitest/index.ts",
|
|
35
|
+
"types": "./src/vitest/index.ts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@cucumber/cucumber-expressions": "^18.0.1",
|
|
40
|
+
"@cucumber/gherkin": "^30.0.4",
|
|
41
|
+
"@cucumber/messages": "^27.0.2",
|
|
42
|
+
"@jest/globals": "^29.7.0",
|
|
43
|
+
"@sanity/pkg-utils": "^6.11.12",
|
|
44
|
+
"typescript": "5.6.3",
|
|
45
|
+
"vitest": "^2.1.5"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@jest/globals": "^29.7.0",
|
|
49
|
+
"vitest": "^2.1.1"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"check:lint": "biome lint .",
|
|
53
|
+
"check:types": "tsc",
|
|
54
|
+
"lint:fix": "biome lint --write ."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {ParameterType} from '@cucumber/cucumber-expressions'
|
|
2
|
+
import {
|
|
3
|
+
CucumberExpression,
|
|
4
|
+
ParameterTypeRegistry,
|
|
5
|
+
} from '@cucumber/cucumber-expressions'
|
|
6
|
+
import * as Gherkin from '@cucumber/gherkin'
|
|
7
|
+
import * as Messages from '@cucumber/messages'
|
|
8
|
+
import type {
|
|
9
|
+
StepDefinition,
|
|
10
|
+
StepDefinitionCallbackParameters,
|
|
11
|
+
} from './step-definitions'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export type CompiledFeature = {
|
|
17
|
+
name: string
|
|
18
|
+
tag?: 'only' | 'skip'
|
|
19
|
+
scenarios: Array<{
|
|
20
|
+
name: string
|
|
21
|
+
tag?: 'only' | 'skip'
|
|
22
|
+
steps: Array<() => Promise<void>>
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function compileFeature<TContext extends Record<string, any> = object>({
|
|
30
|
+
featureText,
|
|
31
|
+
stepDefinitions,
|
|
32
|
+
parameterTypes,
|
|
33
|
+
}: {
|
|
34
|
+
featureText: string
|
|
35
|
+
stepDefinitions: Array<StepDefinition<TContext, any, any, any>>
|
|
36
|
+
parameterTypes: Array<ParameterType<unknown>>
|
|
37
|
+
}): CompiledFeature {
|
|
38
|
+
const uuidFn = Messages.IdGenerator.uuid()
|
|
39
|
+
const builder = new Gherkin.AstBuilder(uuidFn)
|
|
40
|
+
const matcher = new Gherkin.GherkinClassicTokenMatcher()
|
|
41
|
+
const parser = new Gherkin.Parser(builder, matcher)
|
|
42
|
+
|
|
43
|
+
const gherkinDocument = parser.parse(featureText)
|
|
44
|
+
const pickles = Gherkin.compile(
|
|
45
|
+
gherkinDocument,
|
|
46
|
+
(gherkinDocument.feature?.name ?? '').replace(' ', '-'),
|
|
47
|
+
uuidFn,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const parameterTypeRegistry = new ParameterTypeRegistry()
|
|
51
|
+
parameterTypes.forEach((parameterType) =>
|
|
52
|
+
parameterTypeRegistry.defineParameterType(parameterType),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (!gherkinDocument.feature) {
|
|
56
|
+
throw new Error('No feature found')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stepImplementations = stepDefinitions.map((stepDefinition) => {
|
|
60
|
+
const expression = new CucumberExpression(
|
|
61
|
+
stepDefinition.text,
|
|
62
|
+
parameterTypeRegistry,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
type: stepDefinition.type,
|
|
67
|
+
text: stepDefinition.text,
|
|
68
|
+
expression,
|
|
69
|
+
callback: stepDefinition.callback,
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const skippedFeature = gherkinDocument.feature.tags.some(
|
|
74
|
+
(tag) => tag.name === '@skip',
|
|
75
|
+
)
|
|
76
|
+
const onlyFeature = gherkinDocument.feature.tags.some(
|
|
77
|
+
(tag) => tag.name === '@only',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (skippedFeature && onlyFeature) {
|
|
81
|
+
throw new Error('Feature cannot have both @skip and @only tags')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const scenarios = pickles.map((pickle) => {
|
|
85
|
+
const skippedPickle = pickle.tags.some((tag) => tag.name === '@skip')
|
|
86
|
+
const onlyPickle = pickle.tags.some((tag) => tag.name === '@only')
|
|
87
|
+
const context = {} as TContext
|
|
88
|
+
|
|
89
|
+
const steps = pickle.steps.map((step) => {
|
|
90
|
+
const matchingSteps = stepImplementations
|
|
91
|
+
.filter((stepImplementation) => stepImplementation.type === step.type)
|
|
92
|
+
.flatMap((stepImplementation) => {
|
|
93
|
+
const args = stepImplementation.expression.match(step.text)
|
|
94
|
+
|
|
95
|
+
if (args) {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
...stepImplementation,
|
|
99
|
+
args,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return []
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const matchingStep = matchingSteps[0]
|
|
108
|
+
|
|
109
|
+
if (!matchingStep) {
|
|
110
|
+
throw new Error(`No implementation found for step: ${step.text}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (matchingSteps.length > 1) {
|
|
114
|
+
throw new Error(`Multiple implementations found for step: ${step.text}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const args = matchingStep.args.map((arg) =>
|
|
118
|
+
arg.getValue(matchingStep),
|
|
119
|
+
) as StepDefinitionCallbackParameters<any, any, any>
|
|
120
|
+
|
|
121
|
+
return () => matchingStep.callback(context, ...args)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: pickle.name,
|
|
126
|
+
tag: skippedFeature
|
|
127
|
+
? ('skip' as const)
|
|
128
|
+
: skippedPickle
|
|
129
|
+
? ('skip' as const)
|
|
130
|
+
: onlyPickle
|
|
131
|
+
? ('only' as const)
|
|
132
|
+
: undefined,
|
|
133
|
+
steps,
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
tag: skippedFeature ? 'skip' : onlyFeature ? 'only' : undefined,
|
|
139
|
+
name: gherkinDocument.feature.name,
|
|
140
|
+
scenarios,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {ParameterType, type RegExps} from '@cucumber/cucumber-expressions'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export type ParameterTypeConfig<TType = string> = {
|
|
7
|
+
readonly name: string
|
|
8
|
+
matcher: RegExps
|
|
9
|
+
type?: (...args: unknown[]) => TType
|
|
10
|
+
transform?: (...match: string[]) => TType
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export function createParameterType<TType = string>(
|
|
17
|
+
config: ParameterTypeConfig<TType>,
|
|
18
|
+
): ParameterType<TType> {
|
|
19
|
+
return new ParameterType(
|
|
20
|
+
config.name,
|
|
21
|
+
config.matcher,
|
|
22
|
+
config.type ?? String,
|
|
23
|
+
config.transform,
|
|
24
|
+
false,
|
|
25
|
+
true,
|
|
26
|
+
)
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jest-gherkin-driver'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {ParameterType} from '@cucumber/cucumber-expressions'
|
|
2
|
+
import {describe, test} from '@jest/globals'
|
|
3
|
+
import {compileFeature} from '../compile-feature'
|
|
4
|
+
import type {StepDefinition} from '../step-definitions'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export function Feature<TContext extends Record<string, any> = object>({
|
|
10
|
+
featureText,
|
|
11
|
+
stepDefinitions,
|
|
12
|
+
parameterTypes,
|
|
13
|
+
}: {
|
|
14
|
+
featureText: string
|
|
15
|
+
stepDefinitions: Array<StepDefinition<TContext, any, any, any>>
|
|
16
|
+
parameterTypes: Array<ParameterType<unknown>>
|
|
17
|
+
}) {
|
|
18
|
+
const feature = compileFeature({
|
|
19
|
+
featureText,
|
|
20
|
+
stepDefinitions,
|
|
21
|
+
parameterTypes,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const describeFn =
|
|
25
|
+
feature.tag === 'only'
|
|
26
|
+
? describe.only
|
|
27
|
+
: feature.tag === 'skip'
|
|
28
|
+
? describe.skip
|
|
29
|
+
: describe
|
|
30
|
+
|
|
31
|
+
describeFn(feature.name, () => {
|
|
32
|
+
for (const scenario of feature.scenarios) {
|
|
33
|
+
const testFn =
|
|
34
|
+
scenario.tag === 'only'
|
|
35
|
+
? test.only
|
|
36
|
+
: scenario.tag === 'skip'
|
|
37
|
+
? test.skip
|
|
38
|
+
: test
|
|
39
|
+
|
|
40
|
+
testFn(scenario.name, async () => {
|
|
41
|
+
for (const step of scenario.steps) {
|
|
42
|
+
await step()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @public
|
|
3
|
+
*/
|
|
4
|
+
export type StepDefinitionCallbackParameters<
|
|
5
|
+
TParamA = undefined,
|
|
6
|
+
TParamB = undefined,
|
|
7
|
+
TParamC = undefined,
|
|
8
|
+
> = TParamA extends undefined
|
|
9
|
+
? []
|
|
10
|
+
: TParamB extends undefined
|
|
11
|
+
? [TParamA]
|
|
12
|
+
: TParamC extends undefined
|
|
13
|
+
? [TParamA, TParamB]
|
|
14
|
+
: [TParamA, TParamB, TParamC]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export type StepDefinitionCallback<
|
|
20
|
+
TContext extends Record<string, any> = object,
|
|
21
|
+
TParamA = undefined,
|
|
22
|
+
TParamB = undefined,
|
|
23
|
+
TParamC = undefined,
|
|
24
|
+
> = (
|
|
25
|
+
context: TContext,
|
|
26
|
+
...args: StepDefinitionCallbackParameters<TParamA, TParamB, TParamC>
|
|
27
|
+
) => Promise<void>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export type StepDefinition<
|
|
33
|
+
TContext extends Record<string, any> = object,
|
|
34
|
+
TParamA = undefined,
|
|
35
|
+
TParamB = undefined,
|
|
36
|
+
TParamC = undefined,
|
|
37
|
+
> = {
|
|
38
|
+
type: 'Context' | 'Action' | 'Outcome'
|
|
39
|
+
text: string
|
|
40
|
+
callback: StepDefinitionCallback<TContext, TParamA, TParamB, TParamC>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export function Given<
|
|
47
|
+
TContext extends Record<string, any> = object,
|
|
48
|
+
TParamA = undefined,
|
|
49
|
+
TParamB = undefined,
|
|
50
|
+
TParamC = undefined,
|
|
51
|
+
>(
|
|
52
|
+
text: string,
|
|
53
|
+
callback: StepDefinitionCallback<TContext, TParamA, TParamB, TParamC>,
|
|
54
|
+
): StepDefinition<TContext, TParamA, TParamB, TParamC> {
|
|
55
|
+
return {type: 'Context', text, callback}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export function When<
|
|
62
|
+
TContext extends Record<string, any> = object,
|
|
63
|
+
TParamA = undefined,
|
|
64
|
+
TParamB = undefined,
|
|
65
|
+
TParamC = undefined,
|
|
66
|
+
>(
|
|
67
|
+
text: string,
|
|
68
|
+
callback: StepDefinitionCallback<TContext, TParamA, TParamB, TParamC>,
|
|
69
|
+
): StepDefinition<TContext, TParamA, TParamB, TParamC> {
|
|
70
|
+
return {type: 'Action', text, callback}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
export function Then<
|
|
77
|
+
TContext extends Record<string, any> = object,
|
|
78
|
+
TParamA = undefined,
|
|
79
|
+
TParamB = undefined,
|
|
80
|
+
TParamC = undefined,
|
|
81
|
+
>(
|
|
82
|
+
text: string,
|
|
83
|
+
callback: StepDefinitionCallback<TContext, TParamA, TParamB, TParamC>,
|
|
84
|
+
): StepDefinition<TContext, TParamA, TParamB, TParamC> {
|
|
85
|
+
return {type: 'Outcome', text, callback}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vitest-gherkin-driver'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {ParameterType} from '@cucumber/cucumber-expressions'
|
|
2
|
+
import {describe, test} from 'vitest'
|
|
3
|
+
import {compileFeature} from '../compile-feature'
|
|
4
|
+
import type {StepDefinition} from '../step-definitions'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export function Feature<TContext extends Record<string, any> = object>({
|
|
10
|
+
featureText,
|
|
11
|
+
stepDefinitions,
|
|
12
|
+
parameterTypes,
|
|
13
|
+
}: {
|
|
14
|
+
featureText: string
|
|
15
|
+
stepDefinitions: Array<StepDefinition<TContext, any, any, any>>
|
|
16
|
+
parameterTypes: Array<ParameterType<unknown>>
|
|
17
|
+
}) {
|
|
18
|
+
const feature = compileFeature({
|
|
19
|
+
featureText,
|
|
20
|
+
stepDefinitions,
|
|
21
|
+
parameterTypes,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const describeFn =
|
|
25
|
+
feature.tag === 'only'
|
|
26
|
+
? describe.only
|
|
27
|
+
: feature.tag === 'skip'
|
|
28
|
+
? describe.skip
|
|
29
|
+
: describe
|
|
30
|
+
|
|
31
|
+
describeFn(feature.name, () => {
|
|
32
|
+
for (const scenario of feature.scenarios) {
|
|
33
|
+
const testFn =
|
|
34
|
+
scenario.tag === 'only'
|
|
35
|
+
? test.only
|
|
36
|
+
: scenario.tag === 'skip'
|
|
37
|
+
? test.skip
|
|
38
|
+
: test
|
|
39
|
+
|
|
40
|
+
testFn(scenario.name, async () => {
|
|
41
|
+
for (const step of scenario.steps) {
|
|
42
|
+
await step()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|