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 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,3 @@
1
+ export * from './compile-feature'
2
+ export * from './create-parameter-type'
3
+ export * from './step-definitions'
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "@sanity/pkg-utils/tsconfig/strictest.json",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src"]
7
+ }