# jest-runner-cucumber

[![Build Passing](https://img.shields.io/badge/build-passing-green.svg)](https://github.com/Naereen/badges) [![Build Passing](https://img.shields.io/badge/dependencies-up_to_date-green.svg)](https://github.com/Naereen/badges)

Jest Test Runner for the Cucumber Framework

```bash
npm i jest-runner-cucumber
```

## Table of Contents

- [Gherkin Features](#gherkin-features)

- [Cucumber Features](#cucumber-features)

- [Getting Started](#getting-started)

    - [Jest Config](#jest-config)
        - [moduleFileExtensions](#modulefileextensions)
        - [runner](#runner)
        - [setupFiles (optional)](#setupfiles-optional)
        - [setupFilesAfterEnv](#setupfilesafterenv)
        - [testMatch](#testmatch)
        - [restoreMocks (optional)](#restoremocks-optional)
        
    - [Cucumber](#cucumber)   
        - [Feature](#feature)
        - [Hooks](#hooks)
        - [Steps](#steps)
        - [World](#world)
        
    - [Additional Features](#additional-features)   
        - [Gherkin Variables](#gherkin-variables)
        - [MockXHR](#mockxhr)
    
- [Working Example](https://github.com/mentierd/pekel/tree/master/examples/basic)

    - [Jest Config](https://github.com/mentierd/pekel/tree/master/examples/basic/jest.config.json)
    - [Features](https://github.com/mentierd/pekel/tree/master/examples/basic/test/features)
       
        - [Background](https://github.com/mentierd/pekel/tree/master/examples/basic/test/features/scenarioBackground.feature)
        - [Scenario](https://github.com/mentierd/pekel/tree/master/examples/basic/test/features/scenario.feature)
        - [Scenario Outline](https://github.com/mentierd/pekel/tree/master/examples/basic/test/features/scenarioOutline.feature)
    
    - [Hooks](https://github.com/mentierd/pekel/tree/master/examples/basic/test/hooks.tsx)
    - [Steps](https://github.com/mentierd/pekel/tree/master/examples/basic/test/steps.ts)
    - [World](https://github.com/mentierd/pekel/tree/master/examples/basic/test/world.ts)


## Gherkin Features

| Supported          | Feature                                                                                                                                                           | Notes                                                                      | 
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| :white_check_mark: | [And](https://cucumber.io/docs/gherkin/reference/#steps)                                                                                                          |                                                                            |
| :white_check_mark: | [Background](https://cucumber.io/docs/gherkin/reference/#background)                                                                                              |                                                                            |
| :white_check_mark: | [But](https://cucumber.io/docs/gherkin/reference/#steps)                                                                                                          |                                                                            |
| :white_check_mark: | [Comments](https://cucumber.io/docs/gherkin/reference/#descriptions)                                                                                              |                                                                            |
| :white_check_mark: | [Data Table](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/data_table_interface.md)                                                      |                                                                            |
| :white_check_mark: | [DocString](http://rmpestano.github.io/cukedoctor/cucumber-js/cucumber-js-documentation.html#_steps_accepting_a_docstring_parameter)                              | if it finds the docString is JSON, it will parse it for you             |
|                    | [Rule](https://cucumber.io/docs/gherkin/reference/#rule)                                                                                                          | haven't seen examples of this; not sure if it's worth it                   |
| :white_check_mark: | [Scenario](https://cucumber.io/docs/gherkin/reference/#descriptions)                                                                                              |                                                                            |
| :white_check_mark: | [Scenario Outline](http://rmpestano.github.io/cukedoctor/cucumber-js/cucumber-js-documentation.html#Scenario-Outlines-and-Examples)                               |                                                                            |


## Cucumber Features

| Supported          | Feature                                                                                                                                                           | Notes                                                                      | 
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| :white_check_mark: | [After](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#afteroptions-fn)                                                  | called after each scenario in a feature file                               |
| :white_check_mark: | [AfterAll](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#afteralloptions-fn)                                            | called after the feature file is completed; unlike Cucumber, you will have access to "this" context here.                                  |
|                    | [Attachments](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/attachments.md)                                                              |                                                                            |
| :white_check_mark: | [Before](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#beforeoptions-fn)                                                | called before each scenario per feature file                               |
| :white_check_mark: | [BeforeAll](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#beforealloptions-fn)                                          | called before the feature file is started; unlike Cucumber, you will have access to "this" context here.                                  |
| :white_check_mark: | [Given](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#givenpattern-options-fn)                                          |                                                                            |
|                    | [setDefaultTimeout](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#setdefaulttimeoutmilliseconds)                        | use jest.setTimeout or set the timeout property in your jest config        |
| :white_check_mark: | [setDefinitionFunctionWrapper](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#setdefinitionfunctionwrapperwrapper)       |                                                                            |
| :white_check_mark: | [setWorldConstructor](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#setworldconstructorconstructor)                     |                                                                            |
|                    | [Tags](https://github.com/cucumber/cucumber-js/blob/master/docs/cli.md#tags)                                                                                      | need to identify a way to pass tags through jest                           |
| :white_check_mark: | [Then](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#thenpattern-options-fn)                                            |                                                                            |
| :white_check_mark: | [When](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md#whenpattern-options-fn)                                            |                                                                            |


## Additional Features

| Supported          | Feature                                                                                                                                                           | Notes                                                                      | 
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| :white_check_mark: | [gherkin variables](#gherkin-variables) used to populate feature files                                                                                                     |                                                                            |


## Getting Started

### Jest Config

If you have existing jest test cases that do not use Cucumber, you have two options: 

1. create a separate configuration.
You can use the [Jest CLI](https://jestjs.io/docs/en/cli.html) to run against specific configurations:
    ```jest --config=path/to/your/config.json```
    
2. add a "[projects](https://jestjs.io/docs/en/configuration#projects-arraystring--projectconfig)" array to your existing configuration; moving any existing test configuration to inside of the projects 
array. Then, add your new jest configuration:
    ```json
     {
       "projects": [
         {
            "displayName": "Unit"
         },
         {
           "displayName": "Integration",
           "runner": "jest-runner-cucumber"
         }   
       ] 
     } 
    ```

### moduleFileExtensions:
```json
 "moduleFileExtensions": [
    "feature",
    "js",
    "jsx",
    "ts",
    "tsx"
 ]
```
\* If you are not using typescript, remove ```"ts"``` and ```"tsx"```
   
### runner:
```json
"runner": "jest-runner-cucumber"
```

### setupFiles (optional):
```json
 "setupFiles": [
    "<rootDir>/path/to/your/window-polyfill.ts"
 ]
```
\* Add your polyfills here. Here's an [example](https://github.com/mentierd/pekel/tree/master/src/mocks/window.ts)
   
### setupFilesAfterEnv:
```json
 "setupFilesAfterEnv": [
    "<rootDir>/path/to/your/world.ts",
    "<rootDir>/path/to/your/hooks.tsx",
    "<rootDir>/path/to/your/steps.ts"
 ]
```
   
### testMatch:
```json
 "testMatch": [
    "<rootDir>/path/to/your/features/*.feature"
 ]
```

### transform:
```json
"transform": {
    "^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
```
\* If you are not using typescript, remove ```"ts"``` and ```"tsx"```
   
### restoreMocks (optional):
```json
"restoreMocks": true
```
If you are planning on writing integration tests, I highly recommend that you set this to true. 
There is an open [bug](https://github.com/facebook/jest/issues/8839) for jest to fix an issue where 
it does not unset manual mocks that are defined using \_\_mock__ folders. However, if this is set true, 
jest-runner-cucumber will perform a scan of all \_\_mock__ folders and files and manually unmock them for you.
    

## Cucumber

### Feature

```path/to/your/features/button.feature```

```feature
Feature: Button

Given I go to home
When I click the login button
Then the login button is not visible
```

### Hooks

```path/to/your/hooks.tsx```

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils'
import { AfterAll, BeforeAll } from 'cucumber';

import SignUp from './path/to/your/app';

BeforeAll(async function () {
    await act(async () => {
        ReactDOM.render(
            <SignUp/>,
            document.body
        )
    });
});

AfterAll(async function () {
    await act(async () => {
        ReactDOM.unmountComponentAtNode(
            document.body
        )
    });
});
```

You can choose to use the hooks to render/unmount your component before/after each feature file like above,
or you can add a path to your application entry point to your jest configuration's [setupFiles](https://jestjs.io/docs/en/configuration#setupfiles-array) property. 
The latter is more performant.
    
### Steps

```path/to/your/steps.ts```

```typescript
import { Given, When, Then } from 'cucumber';
import { act } from 'react-dom/test-utils';

Given(/I go to (.*)$/, function(link) {
    window.location.hash = `#/${link}`;
});

When(/I click the (\S+) button$/, async function(name) {
    await act(async () => {
        document.querySelector(`[data-test-id="${name}"]`).click();
    });
});

Then(/the (\S+) button is (visible|not visible)$/, function(name, state) {
    expect(!!document.querySelector(`[data-test-id="${name}"]`))
        .toEqual(state === 'visible')
});
```

### World

[setWorldConstuctor](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/world.md) allows you to set the context of "this" for your steps/hooks definitions. 
This can be helpful when you want to maintain state between steps/hooks or want your steps/hooks to have access 
to some predefined data. The values are accessible within all Hooks, and Steps by using *this*

```path/to/your/world.ts```

```typescript
import { setWorldConstructor } from 'cucumber';

setWorldConstructor(
    class MyWorld {
        pages = [];
    }
);
```

## Example Output

Below is an example output from running tests against the [example](https://github.com/mentierd/pekel/tree/master/examples/basic/test)
```text
 PASS  test/features/scenarioOutline.feature (97 MB heap size)
  Feature: Sign Up - Submitting With Extra Emails
    ✓ Given the firstName text input value is Dayne (37 ms)
    ✓ And the lastName text input value is Mentier (11 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (13 ms)
    ✓ And the password text input value is itsASecretShh... (9 ms)
    ✓ And the extraEmails checkbox input is not checked (2 ms)
    ✓ When the submit button is clicked (89 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body: (3 ms)
    ✓ And the successAlert is visible (2 ms)
    ✓ And the showExtraEmailsAlert is not visible (2 ms)
  Feature: Sign Up - Submitting Without Extra Emails
    ✓ Given the firstName text input value is Dayne (12 ms)
    ✓ And the lastName text input value is Mentier (11 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (8 ms)
    ✓ And the password text input value is itsASecretShh... (10 ms)
    ✓ And the extraEmails checkbox input is checked (9 ms)
    ✓ When the submit button is clicked (45 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body: (1 ms)
    ✓ And the successAlert is visible (1 ms)
    ✓ And the showExtraEmailsAlert is visible (1 ms)

 PASS  test/features/scenario.feature (93 MB heap size)
  Feature: Sign Up - Without Extra Emails
    ✓ Given the firstName text input value is Dayne (11 ms)
    ✓ And the lastName text input value is Mentier (12 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (11 ms)
    ✓ And the password text input value is itsASecretShh... (14 ms)
    ✓ When the submit button is clicked (66 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body: (5 ms)
    ✓ And the successAlert is visible (2 ms)
    ✓ And the showExtraEmailsAlert is not visible (2 ms)
  Feature: Sign Up - With Extra Emails
    ✓ Given the firstName text input value is Dayne (14 ms)
    ✓ And the lastName text input value is Mentier (12 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (12 ms)
    ✓ And the password text input value is itsASecretShh... (9 ms)
    ✓ And the extraEmails checkbox input is checked (9 ms)
    ✓ When the submit button is clicked (49 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body: (1 ms)
    ✓ And the successAlert is visible (2 ms)
    ✓ And the showExtraEmailsAlert is visible (1 ms)

 PASS  test/features/scenarioBackground.feature (85 MB heap size)
  Feature: Sign Up - Without Extra Emails
    ✓ Given the firstName text input value is Dayne (14 ms)
    ✓ And the lastName text input value is Mentier (13 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (15 ms)
    ✓ And the password text input value is itsASecretShh... (22 ms)
    ✓ When the submit button is clicked (66 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body: (3 ms)
    ✓ And the successAlert is visible (4 ms)
    ✓ And the showExtraEmailsAlert is not visible (2 ms)
  Feature: Sign Up - With Extra Emails
    ✓ Given the firstName text input value is Dayne (10 ms)
    ✓ And the lastName text input value is Mentier (8 ms)
    ✓ And the email text input value is dayne.mentier@gmail.com (10 ms)
    ✓ And the password text input value is itsASecretShh... (8 ms)
    ✓ And the extraEmails checkbox input is checked (7 ms)
    ✓ When the submit button is clicked (46 ms)
    ✓ Then POST http://127.0.0.1:8080/api/sign-up is called with the request body:
    ✓ And the successAlert is visible (2 ms)
    ✓ And the showExtraEmailsAlert is visible (1 ms)

Test Suites: 3 passed, 3 total
Tests:       52 passed, 52 total
Snapshots:   0 total
Time:        7.603 s
Ran all test suites.
```

## Gherkin Variables

This provides the ability to define variables in your feature files, and hold the values in a [separate file](https://github.com/mentierd/pekel/tree/master/examples/basic/features/scenarioOutline.vars.ts).
A few things to note for this functionality is:

1. the file must contain the same name as the feature file you're looking to populate
2. all variables start with a "$"; eg, in the feature file, the variable would be defined as *$email*, while the vars file would contain *email*
3. you can further split up your vars files by using the *CUCUMBER_ENV* variable. Using that, your files would look like this:
```featureFileName.CUCUMBER_ENV.vars.{js,ts,json}```
   
For an example, see the example [scenarioOutline](https://github.com/mentierd/pekel/tree/master/examples/basic/features/scenarioOutline.feature) feature file, 
and the accompanying [variable file](https://github.com/mentierd/pekel/tree/master/examples/basic/features/scenarioOutline.vars.ts)

## MockXHR

One of the hardest thing I've found when using jest as a runner for integration tests is figuring out how to properly spy on
api calls and mock the requests. I've included a helper class called [MockXHR](https://github.com/mentierd/pekel/tree/master/src/mocks/MockXHR.ts) 
that simplifies the process. Below is an example of how to set it up in your *World* and *Hooks*

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { After, AfterAll, BeforeAll,setWorldConstructor } from 'cucumber';
import { MockXHR } from 'jest-runner-cucumber/dist/mocks/MockXHR';

import TestApp from 'my/root/path';

setWorldConstructor(
    class TestWorld {
        $mockXhr = new MockXHR([
            {
                url: '/api/sign-up',
                method: 'post',
                status: 200,
                response: {
                    message: 'thanks for signing up!'
                }
            },
            {
                url: '/api/sign-up',
                method: 'get',
                status: 200,
                response: {
                    registered: false
                }
            }
        ])
    }
)

After(function () {
    this.$mockServer.spy.mockClear();
});

AfterAll(async function () {
    ReactDOM.unmountComponentAtNode(document.body);
    this.$mockServer.destroy();
});

BeforeAll(async function () {
    ReactDOM.render(
        <TestApp/>,
        document.body
    );
});
```

*MockXHR* provides a spy that is called whenever a request goes out, this can be use your steps like this:

```typescript
import { Then } from 'cucumber'

Then(/^(GET|PUT|POST|DELETE) (.*) is called with the (request body|params):$/,
    function (method, url, type, value) {

        const hasBody = type === 'request body';

        expect(this.$mockServer.spy).toHaveBeenCalledWith({
            ...hasBody ? {data: value} : {params: value},
            method,
            url
        });
    }
);
```

```gherkin
Scenario: Without Extra Emails
  When the submit button is clicked
  Then POST /api/sign-up is called with the request body:
  """
   {
       "firstName": "Dayne",
       "lastName": "Mentier",
       "email": "dayne.mentier@gmail.com",
       "password": "itsASecretShh...",
       "extraEmails": false
   }
  """
```

Internally, it uses [xhr-mock](https://github.com/jameslnewell/xhr-mock). Unlike [nock](https://github.com/nock/nock) 
which causes memory leak issues because it is mutating native node modules, xhr-mock does not.  I've also found that
if your http lib is [axios](https://github.com/axios/axios), you can also run into memory leak issues if you do not 
mock the [follow-redirects](https://github.com/follow-redirects/follow-redirects). That lib has the same issue as *nock*;
it mutates the native http and https modules, which causes leaking. If you are using *axios* make sure you add the 
following mock to one of your entry files:

```typescript
jest.mock('follow-redirects', () => ({
    http: function () {
    },
    https: function () {
    }
}));
```
