A testing framework agnostic Gherkin driver
racejar
is a thin wrapper around @cucumber/*
that allows you to write your tests in Gherkin and run them with Vitest, Jest or any other testing framework of your choice.
pnpm add --save-dev racejar
Using racejar
with Vitest requires no additional configuration. Just drop it into a new or an existing test file and get going:
// your.test.ts
// Import `Feature` from `racejar/vitest`
import {Feature} from 'racejar/vitest'
// Import your raw `.feature` file
import featureFile from './your.feature?raw'
// Run the feature
Feature({
featureText: featureFile,
stepDefinitions: [
// ...
],
parameterTypes: [
// ...
],
})
Using racejar
with Jest is similar to Vitest. Just import Feature
from racejar/jest
instead.
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:
// jest.config.ts
import type {Config} from 'jest'
const config: Config = {
transform: {
'\\.feature$': '<rootDir>/feature-file-transformer.js',
},
}
// feature-file-transformer.js
module.exports = {
process(content) {
return {
code: `module.exports = ${JSON.stringify(content)};`,
}
},
}
Feature
exported from racejar/vitest
and racejar/jest
are convenient thin wrappers around the more generic compileFeature
.
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:
import {compileFeature} from 'racejar'
const feature = compileFeature({
featureText: featureFile,
stepDefinitions: [
// ...
],
parameterTypes: [
// ...
],
})
for (const scenario of feature.scenarios) {
// ...
}
stepDefinitions
can be defined inline or separately. They are defined using Given
, When
, Then
exported from racejar
:
import {Given, Then, When} from 'racejar'
const stepDefinitions = [Given(/* ... */), When(/* ... */), Then(/* ... */)]
racejar
will error out and inform you if a step definition is missing or if you accidentally defined duplicate definitions.
If you use nonstandard parameter types, then you can define them yourself:
import {createParameterType} from 'racejar'
const parameterTypes = [createParameterType(/* ... */)]
import {Given, Then, When} from 'racejar'
import {Feature} from 'racejar/vitest'
import {expect} from 'vitest'
function greet(name: string) {
return `Hello, ${name}!`
}
type Context = {
person: string
greeting: string
}
Feature({
featureText: `
Feature: Greeting
Scenario: Greeting a person
Given the person "Herman"
When greeting the person
Then the greeting is "Hello, Herman!"`,
stepDefinitions: [
Given('the person {string}', (context: Context, person: string) => {
context.person = person
}),
When('greeting the person', (context: Context) => {
context.greeting = greet(context.person)
}),
Then('the greeting is {string}', (context: Context, greeting: string) => {
expect(context.greeting).toBe(greeting)
}),
],
})
The following example is taken from the editor
package in this repository. For a full example of how to use racejar
, head over to /packages/editor/gherkin-tests/. The package uses racejar
to run a Playwright E2E test suite powered by Vitest Browser Mode.
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:
// annotations.feature
Feature: Annotations
Background:
Given one editor
And a global keymap
Scenario: Selection after adding an annotation
Given the text "foo bar baz"
When "bar" is selected
And "link" "l1" is toggled
Then "bar" has marks "l1"
And "bar" is selected
Here's a rough idea of how these steps can be defined:
import {Given, Then, When} from 'racejar'
// A `context` object is passed around between steps
// The context can be whatever you want it to be
type Context = {
locator: Locator
keyMap: Map<string, string>
}
export const stepDefinitions = [
Given('one editor', async (context: Context) => {
render(<Editor />)
const locator = page.getByTestId('<editor test ID>')
context.locator = locator
await vi.waitFor(() => expect.element(locator).toBeInTheDocument())
}),
Given('a global keymap', (context: Context) => {
context.keyMap = new Map()
}),
Given('the text {string}', async (context: Context, text: string) => {
await userEvent.click(context.locator)
await userEvent.type(context.locator, text)
}),
Given('{string} is selected', async (context: Context, text: string) => {
// Select `text` in the editor
}),
When(
'{annotation} {keys} is toggled',
async (
context: Context,
annotation: 'comment' | 'link',
keyKeys: Array<string>,
) => {
// Toggle the `annotation` and store the resulting keys on the `context.keyMap`
},
),
Then(
'{string} has marks {marks}',
async (context: Context, text: string, marks: Array<string>) => {
// Get the actual marks on the `text` and compare them with `marks`
},
),
Then('{text} is selected', async (context: Context, text: Array<string>) => {
// Assert that the current editor selection matches `text`
}),
]
As you can see, the step definitions declare a few custom parameters, {annotation}
, {keys}
and {text}
:
import {createParameterType} from 'racejar'
export const parameterTypes = [
createParameterType({
name: 'annotation',
matcher: /"(comment|link)"/,
}),
createParameterType({
name: 'keys',
matcher: /"(([a-z]\d)(,([a-z]\d))*)"/,
type: Array,
transform: (input) => input.split(','),
}),
createParameterType({
name: 'text',
matcher: /"([a-z-,#>\\n |\[\]]*)"/,
type: Array,
transform: parseGherkinTextParameter,
}),
]
function parseGherkinTextParameter(text: string) {
return text
.replace(/\|/g, ',|,')
.split(',')
.map((span) => span.replace(/\\n/g, '\n'))
}
Now, let's run the test using our defined steps and custom parameter types:
// annotations.test.ts
import annotationsFeature from './annotations.feature?raw'
import {parameterTypes} from './parameter-types'
import {stepDefinitions} from './step-definitions'
Feature({
featureText: annotationsFeature,
stepDefinitions,
parameterTypes,
})
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:
// global.d.ts
declare module '*.feature?raw' {
const content: string
export default content
}
Use prettier-plugin-gherkin to automatically format your .feature
files.
// .prettierrc
{
"plugins": ["prettier-plugin-gherkin"]
}