Repository: TheBrainFamily/cypress-cucumber-preprocessor
Pull request #121 (2018-12-07 15:14:13)

feat: step definitions related to the feature file


Patch:
diff --git a/README.md b/README.md
index 4d2ba9d..425d21c 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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/
@@ -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
@@ -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
 
diff --git a/lib/getStepDefinitionPathsFrom.js b/lib/getStepDefinitionPathsFrom.js
new file mode 100644
index 0000000..b637637
--- /dev/null
+++ b/lib/getStepDefinitionPathsFrom.js
@@ -0,0 +1,6 @@
+const path = require("path");
+
+module.exports = {
+  getStepDefinitionPathsFrom: filePath =>
+    filePath.replace(path.extname(filePath), "")
+};
diff --git a/lib/getStepDefinitionsPaths.js b/lib/getStepDefinitionsPaths.js
new file mode 100644
index 0000000..eb440f4
--- /dev/null
+++ b/lib/getStepDefinitionsPaths.js
@@ -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 };
diff --git a/lib/index.js b/lib/index.js
index 41c4911..f7a80d5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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);
     }
diff --git a/lib/loader.js b/lib/loader.js
index 17d0d98..653eee2 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -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
@@ -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);
diff --git a/lib/stepDefinitionPath.js b/lib/stepDefinitionPath.js
index 96c2fe9..b6e37da 100644
--- a/lib/stepDefinitionPath.js
+++ b/lib/stepDefinitionPath.js
@@ -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`;
   }
diff --git a/loader.js b/loader.js
new file mode 100644
index 0000000..225a95f
--- /dev/null
+++ b/loader.js
@@ -0,0 +1,3 @@
+const loader = require("./lib/loader");
+
+module.exports = loader;
diff --git a/wallaby.js b/wallaby.js
index 5e64672..e2b8ee3 100644
--- a/wallaby.js
+++ b/wallaby.js
@@ -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}
     ],
 
 


Test patch:
diff --git a/lib/getStepDefinitionPathsFrom.test.js b/lib/getStepDefinitionPathsFrom.test.js
new file mode 100644
index 0000000..2676fd0
--- /dev/null
+++ b/lib/getStepDefinitionPathsFrom.test.js
@@ -0,0 +1,7 @@
+const { getStepDefinitionPathsFrom } = require("./getStepDefinitionPathsFrom");
+
+test("getStepDefinitionPathsFrom", () => {
+  expect(
+    getStepDefinitionPathsFrom("/home/lgandecki/someComplex_.feature")
+  ).equal("/home/lgandecki/someComplex_");
+});