racejar

1.1.0 • Public • Published

racejar

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

Usage with Vitest

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: [
    // ...
  ],
})

Usage with Jest

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)};`,
    }
  },
}

Usage with <your favourite testing framework>

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) {
  // ...
}

Define Steps and Parameter Types

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(/* ... */)]

Basic Example

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)
    }),
  ],
})

Advanced Example

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,
})

Tips

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"]
}

Package Sidebar

Install

npm i racejar

Weekly Downloads

57

Version

1.1.0

License

MIT

Unpacked Size

24.7 kB

Total Files

18

Last publish

Collaborators

  • sanity-io