Skip to content

Commit

Permalink
feat: step definitions related to the feature file (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgandecki authored Dec 7, 2018
1 parent e0b4892 commit e0d09ca
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 32 deletions.
84 changes: 72 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![CircleCI](https://circleci.com/gh/TheBrainFamily/cypress-cucumber-preprocessor.svg?style=shield)](https://circleci.com/gh/TheBrainFamily/cypress-cucumber-preprocessor)
# Run cucumber/gherkin-syntaxed specs with cypress.io

Follow the Setup steps, or if you prefer to hack on a working example, take a look at [https://github.com/TheBrainFamily/cypress-cucumber-example](https://github.com/TheBrainFamily/cypress-cucumber-example
Follow the Setup steps, or if you prefer to hack on a working example, take a look [https://github.com/TheBrainFamily/cypress-cucumber-example](https://github.com/TheBrainFamily/cypress-cucumber-example
)

## Setup
Expand All @@ -25,15 +25,6 @@ module.exports = (on, config) => {
}
```

Step definition files are by default in: cypress/support/step_definitions. If you want to put them somewhere please use [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) format. For example, add to your package.json :

```javascript
"cypress-cucumber-preprocessor": {
"step_definitions": "cypress/support/step_definitions/"
}
```

## Usage
### Feature files

Put your feature files in cypress/integration/
Expand All @@ -51,7 +42,73 @@ Feature: The Facebook

### Step definitions

Put your step definitions in cypress/support/step_definitions
#### Cypress Cucumber Preprocessor Style (recommended!)

##### Step definitions unique for the feature

###### Configuration
First please use [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) to create a configuration for the plugin, for example put this section:

```json
"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true
}
```
inside your package.json. (this will become the default option in a future version)

###### Step definitions creation
Then put your step definitions in cypress/integration with the folder name matching the .feature filename.
Easier to show than to explain, so, assuming the feature file is in cypress/integration/Google.feature , as proposed above, the preprocessor will read all the files inside cypress/integration/Google/, so:

cypress/integration/Google/google.js (or any other .js file in the same path)
```javascript
import { Given } from "cypress-cucumber-preprocessor/steps";

const url = 'https://google.com'
Given('I open Google page', () => {
cy.visit(url)
})
```

This is a good place to put before/beforeEach/after/afterEach hooks related to THAT PARTICULAR FEATURE. This is incredibly hard to get right with pure cucumber.

##### Reusable step definitions

We also have a way to create reusable step definitions. Put them in cypress/integration/common/

Example:
cypress/integration/common/i_see_string_in_the_title.js
```javascript
import { Then } from "cypress-cucumber-preprocessor/steps";

Then(`I see {string} in the title`, (title) => {
cy.title().should('include', title)
})
```

This is a good place to put global before/beforeEach/after/afterEach hooks.

##### Why a new pattern?
The problem with the legacy structure is that everything is global. This is problematic for multiple reasons.
- It makes it harder to create .feature files that read nicely - you have to make sure you are not stepping on toes of already existing step definitions. You should be able to write your tests without worrying about reusability, complex regexp matches, or anything like that. Just write a story. Explain what you want to see without getting into the details. Reuse in the .js files, not in something you should consider an always up-to-date, human-readable documentation.
- The startup times get much worse - because we have to analyze all the different step definitions so we can match the .feature files to the test.
- Hooks are problematic. If you put before() in a step definition file, you might think that it will run only for the .feature file related to that step definition. You try the feature you work on, everything seems fine and you push the code. Here comes a surprise - it will run for ALL .feature files in your whole project. Very unintuitive. And good luck debugging problems caused by that! This problem was not unique to this plugin, bo to the way cucumberjs operates.
Let's look how this differs with the proposed structure. Assuming you want to have a hook before ./Google.feature file, just create a ./Google/before.js and put the hook there. This should take care of long requested feature - (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/25)[#25]

If you have a few tests the "oldschool" style is completely fine. But for a large enterprise-grade application, with hundreds or sometimes thousands of .feature files, the fact that everything is global becomes a maintainability nightmare.

#### Oldschool/Legacy Cucumber style (please let us know if you decide to use it!)

##### Step Definition location configuration
Step definition files are by default in: cypress/support/step_definitions. If you want to put them somewhere please use [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) format. For example, add to your package.json :

```javascript
"cypress-cucumber-preprocessor": {
"step_definitions": "cypress/support/step_definitions/"
}
```

Follow your configuration or use the defaults and put your step definitions in cypress/support/step_definitions

Examples:
cypress/support/step_definitions/google.js
Expand All @@ -73,13 +130,16 @@ Then(`I see {string} in the title`, (title) => {
})
```


#### Given/When/Then functions

Since Given/When/Then are on global scope please use
```javascript
/* global Given, When, Then */
```
to make IDE/linter happy

or import them directly
or import them directly as shown in the above examples

### Running

Expand Down
6 changes: 6 additions & 0 deletions lib/getStepDefinitionPathsFrom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const path = require("path");

module.exports = {
getStepDefinitionPathsFrom: filePath =>
filePath.replace(path.extname(filePath), "")
};
7 changes: 7 additions & 0 deletions lib/getStepDefinitionPathsFrom.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { getStepDefinitionPathsFrom } = require("./getStepDefinitionPathsFrom");

test("getStepDefinitionPathsFrom", () => {
expect(
getStepDefinitionPathsFrom("/home/lgandecki/someComplex_.feature")
).equal("/home/lgandecki/someComplex_");
});
24 changes: 24 additions & 0 deletions lib/getStepDefinitionsPaths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const glob = require("glob");
const cosmiconfig = require("cosmiconfig");
const stepDefinitionPath = require("./stepDefinitionPath.js");
const { getStepDefinitionPathsFrom } = require("./getStepDefinitionPathsFrom");

const getStepDefinitionsPaths = filePath => {
let paths = [];
const explorer = cosmiconfig("cypress-cucumber-preprocessor", { sync: true });
const loaded = explorer.load();
if (loaded && loaded.config && loaded.config.nonGlobalStepDefinitions) {
const nonGlobalPattern = `${getStepDefinitionPathsFrom(
filePath
)}/**/*.+(js|ts)`;
const commonDefinitionsPattern = `${stepDefinitionPath()}/common/**/*.+(js|ts)`;
paths = paths.concat(glob.sync(nonGlobalPattern));
paths = paths.concat(glob.sync(commonDefinitionsPattern));
} else {
const pattern = `${stepDefinitionPath()}/**/*.+(js|ts)`;
paths = paths.concat(glob.sync(pattern));
}
return paths;
};

module.exports = { getStepDefinitionsPaths };
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const transform = file => {
function end() {
if (file.match(".feature$")) {
log("compiling feature ", file);
this.queue(compile(data));
this.queue(compile(data, file));
} else {
this.queue(data);
}
Expand Down
13 changes: 3 additions & 10 deletions lib/loader.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable no-eval */
const log = require("debug")("cypress:cucumber");
const glob = require("glob");
const stepDefinitionPath = require("./stepDefinitionPath.js");
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");

// This is the template for the file that we will send back to cypress instead of the text of a
// feature file
Expand All @@ -21,15 +20,9 @@ const createCucumber = (spec, toRequire) =>
createTestsFromFeature(gherkinAst);
`;

const createPattern = () => `${stepDefinitionPath()}/**/*.+(js|ts)`;

const pattern = createPattern();

const getStepDefinitionsPaths = () => [].concat(glob.sync(pattern));

module.exports = spec => {
module.exports = function(spec, filePath = this.resourcePath) {
log("compiling", spec);
const stepDefinitionsToRequire = getStepDefinitionsPaths().map(
const stepDefinitionsToRequire = getStepDefinitionsPaths(filePath).map(
sdPath => `require('${sdPath}')`
);
return createCucumber(spec, stepDefinitionsToRequire);
Expand Down
26 changes: 21 additions & 5 deletions lib/stepDefinitionPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@ const cosmiconfig = require("cosmiconfig");
module.exports = () => {
const appRoot = process.cwd();

const cypressOptions = JSON.parse(
fs.readFileSync(`${appRoot}/cypress.json`, "utf-8")
);

const explorer = cosmiconfig("cypress-cucumber-preprocessor", { sync: true });
const loaded = explorer.load();
if (loaded && loaded.config && loaded.config.step_definitions) {
return path.resolve(appRoot, loaded.config.step_definitions);
if (loaded && loaded.config) {
const { config } = loaded;
if (config.nonGlobalStepDefinitions && config.step_definitions) {
throw new Error(
"Error! You can't have both step_definitions folder and nonGlobalStepDefinitions setup in cypress-cucumber-preprocessor configuration"
);
}
if (config.nonGlobalStepDefinitions) {
return path.resolve(
appRoot,
cypressOptions.integrationFolder || "cypress/integration"
);
}
if (config.step_definitions) {
return path.resolve(appRoot, config.step_definitions);
}
}

// XXX Deprecated, left here for backward compability
const cypressOptions = JSON.parse(
fs.readFileSync(`${appRoot}/cypress.json`, "utf-8")
);

if (cypressOptions && cypressOptions.fileServerFolder) {
return `${cypressOptions.fileServerFolder}/support/step_definitions`;
}
Expand Down
3 changes: 3 additions & 0 deletions loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const loader = require("./lib/loader");

module.exports = loader;
8 changes: 4 additions & 4 deletions wallaby.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
module.exports = function (wallaby) {
return {
files: [
{pattern: './*.+(js|jsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', load: false},
{pattern: '!./*.test.js?(x)', load: false},
'./*.snap',
{pattern: './lib/*.+(js|jsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', load: false},
{pattern: '!./lib/*.test.js?(x)', load: false},
'./lib/*.snap',
{pattern: '.eslintrc', load: false},
'./cypress/support/step_definitions/*.js',
'./cypress/integration/*.feature',
],

tests: [
{pattern: './*.test.js?(x)', load: true}
{pattern: './lib/*.test.js?(x)', load: true}
],


Expand Down

0 comments on commit e0d09ca

Please sign in to comment.