Repository: TheBrainFamily/cypress-cucumber-preprocessor
Pull request #191 (2019-07-23 10:15:35)

Cucumber JSON Reporter


Patch:
diff --git a/.gitignore b/.gitignore
index 92e59a4..41ee580 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,6 +61,8 @@ typings/
 
 cypress/videos
 cypress/screenshots
+cypress/cucumber-json
+target
 
 #optional .vscode settings directory
 .vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index 3325b88..b11d4bc 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ module.exports = (on, config) => {
 }
 ```
 
+
 ### Feature files
 
 Put your feature files in cypress/integration/
@@ -279,6 +280,42 @@ Here is an [example](cypress/support/step_definitions/customParameterTypes.js) w
 
 We use https://docs.cucumber.io/cucumber/cucumber-expressions/ to parse your .feature file, please use that document as your reference 
 
+## cucumber.json file generation
+
+The cypress-cucumber-preprocessor can generate cucumber.json file output as it runs the features files. This is separate from, and in addition to, any mocha reporter configured in cypress.
+
+These files are intended to be used with one of the many available cucumber report generator packages. 
+Seems to work fine with both https://github.com/jenkinsci/cucumber-reports-plugin and https://github.com/wswebcreation/multiple-cucumber-html-reporter
+
+
+Output, by default, is written to the folder cypress/cucumber-json/, and one file is generated per feature.
+
+ 
+This behaviour is configurable. Use cosmiconfig to create a configuration for the plugin, see step definition discussion above, 
+and add the following to the cypress-cucumber-preprocessor section in package.json to turn it off or change the defaults:
+
+```
+  "cypress-cucumber-preprocessor": {
+    "cucumberJson": {
+      "generate": true,
+      "outputFolder": "cypress/cucumber-json",
+      "filePrefix": "",
+      "fileSuffix": ".cucumber"
+    }
+  }
+```
+
+Here:
+
+outputFolder: The folder to write the files to, defaults to ```cypress/cucumber-json```
+
+filePrefix: A separate json file is generated for each feature based on the name of the feature file. All generated file names will be prefixed with this option if specified. Empty string (no prefix) by default.
+
+fileSuffix: A suffix to add to each generated filename, defaults to '.cucumber'
+
+generate: Flag, output cucumber.json or not, defaults to false.
+
+
 ## Development
 
 Install all dependencies:
diff --git a/lib/cukejson/cucumberDataCollector.js b/lib/cukejson/cucumberDataCollector.js
new file mode 100644
index 0000000..4810398
--- /dev/null
+++ b/lib/cukejson/cucumberDataCollector.js
@@ -0,0 +1,150 @@
+const { Parser } = require("gherkin");
+const statuses = require("cucumber").Status;
+
+class CucumberDataCollector {
+  constructor(uri, spec) {
+    this.feature = new Parser().parse(spec.toString()).feature;
+    this.scenarioSteps = {};
+    this.runScenarios = {};
+    this.runTests = {};
+    this.stepResults = {};
+    this.testError = null;
+    this.uri = uri;
+    this.spec = spec;
+
+    this.currentScenario = null;
+    this.currentStep = 0;
+
+    this.timer = Date.now();
+
+    this.logStep = step => {
+      Cypress.log({
+        name: "step",
+        displayName: step.keyword,
+        message: `${step.text}`,
+        consoleProps: () => ({ feature: this.uri, step })
+      });
+    };
+
+    this.onStartTest = () => {};
+
+    this.onFinishTest = () => {
+      if (this.testError) {
+        this.attachErrorToFailingStep();
+      }
+    };
+
+    this.onStartScenario = (scenario, stepsToRun) => {
+      this.currentScenario = scenario;
+      this.currentStep = 0;
+      this.stepResults = {};
+      this.scenarioSteps[scenario.name] = stepsToRun;
+      this.testError = null;
+
+      stepsToRun.forEach(step => {
+        this.stepResults[step.index] = { status: statuses.PENDING };
+      });
+      this.runScenarios[scenario.name] = scenario;
+    };
+
+    this.onFinishScenario = scenario => {
+      this.markStillPendingStepsAsSkipped(scenario);
+      this.recordScenarioResult(scenario);
+    };
+
+    this.onStartStep = step => {
+      this.currentStep = step.index;
+      this.setStepToPending(step);
+      this.logStep(step);
+    };
+
+    this.onFinishStep = (step, result) => {
+      this.recordStepResult(step, result);
+    };
+
+    this.onFail = err => {
+      this.testError = err;
+      if (err.message.indexOf("Step implementation missing for") > -1) {
+        this.stepResults[this.currentStep] = {
+          status: statuses.UNDEFINED,
+          duration: this.timeTaken()
+        };
+      } else {
+        const attachment = {
+          index: this.currentStep,
+          testCase: this.formatTestCase(this.currentScenario),
+          data: btoa(err.message.toString()),
+          media: { type: "text/plain" }
+        };
+        this.stepResults[this.currentStep] = {
+          status: statuses.FAILED,
+          duration: this.timeTaken(),
+          exception: this.testError,
+          attachment
+        };
+      }
+      this.onFinishScenario(this.currentScenario);
+    };
+
+    this.timeTaken = () => {
+      const now = Date.now();
+      const duration = now - this.timer;
+      this.timer = now;
+      return duration;
+    };
+
+    this.formatTestCase = scenario => {
+      const line = scenario.example
+        ? scenario.example.line
+        : scenario.location.line;
+      return {
+        sourceLocation: { uri, line }
+      };
+    };
+
+    this.attachErrorToFailingStep = () => {
+      Object.keys(this.runTests).forEach(test => {
+        const stepResults = this.runTests[test];
+        Object.keys(stepResults).forEach(stepIdx => {
+          const stepResult = stepResults[stepIdx];
+          if (stepResult.result === statuses.FAILED) {
+            stepResult.exception = this.testError;
+          }
+        });
+      });
+    };
+
+    this.markStillPendingStepsAsSkipped = scenario => {
+      this.runTests[scenario.name] = Object.keys(this.stepResults).map(key => {
+        const result = this.stepResults[key];
+        return Object.assign({}, result, {
+          status:
+            result.status === statuses.PENDING
+              ? statuses.SKIPPED
+              : result.status
+        });
+      });
+    };
+    this.recordScenarioResult = scenario => {
+      this.runTests[scenario.name].result = this.anyStepsHaveFailed(scenario)
+        ? statuses.FAILED
+        : statuses.PASSED;
+    };
+
+    this.setStepToPending = step => {
+      this.stepResults[step.index] = { status: statuses.PENDING };
+    };
+
+    this.recordStepResult = (step, result) => {
+      this.stepResults[step.index] = {
+        status: result,
+        duration: this.timeTaken()
+      };
+    };
+
+    this.anyStepsHaveFailed = () =>
+      Object.values(this.stepResults).find(e => e.status !== statuses.PASSED);
+  }
+}
+
+module.exports = { CucumberDataCollector };
diff --git a/lib/cukejson/generateCucumberJson.js b/lib/cukejson/generateCucumberJson.js
new file mode 100644
index 0000000..f77de96
--- /dev/null
+++ b/lib/cukejson/generateCucumberJson.js
@@ -0,0 +1,71 @@
+const { EventEmitter } = require("events");
+const { generateEvents } = require("gherkin");
+const { JsonFormatter, formatterHelpers } = require("cucumber");
+
+function generateCucumberJson(state) {
+  let output = "";
+  const logFn = data => {
+    output += data;
+  };
+
+  const eventBroadcaster = new EventEmitter();
+
+  function storePickle({ pickle, uri }) {
+    eventBroadcaster.emit("pickle-accepted", { pickle, uri });
+  }
+
+  eventBroadcaster.on("pickle", storePickle);
+
+  // eslint-disable-next-line no-new
+  new JsonFormatter({
+    eventBroadcaster,
+    eventDataCollector: new formatterHelpers.EventDataCollector(
+      eventBroadcaster
+    ),
+    log: logFn
+  });
+
+  // Start feeding the recorded test run into the JsonFormatter
+
+  // Feed in the static test structure
+  generateEvents(state.spec.toString(), state.uri).forEach(event => {
+    eventBroadcaster.emit(event.type, event);
+  });
+
+  // Feed in the results from the recorded scenarios and steps
+  Object.keys(state.runTests).forEach(test => {
+    const scenario = state.runScenarios[test];
+    const stepResults = state.runTests[test];
+    const stepsToRun = state.scenarioSteps[test];
+    const steps = stepsToRun.map(step => ({
+      sourceLocation: { uri: state.uri, line: step.location.line }
+    }));
+    eventBroadcaster.emit("test-case-prepared", {
+      sourceLocation: state.formatTestCase(scenario).sourceLocation,
+      steps
+    });
+    stepResults.forEach((stepResult, stepIdx) => {
+      eventBroadcaster.emit("test-step-prepared", {
+        index: stepIdx,
+        testCase: state.formatTestCase(scenario)
+      });
+      eventBroadcaster.emit("test-step-finished", {
+        index: stepIdx,
+        testCase: state.formatTestCase(scenario),
+        result: stepResult
+      });
+      if (stepResult.attachment) {
+        eventBroadcaster.emit("test-step-attachment", stepResult.attachment);
+      }
+    });
+    eventBroadcaster.emit("test-case-finished", {
+      sourceLocation: state.formatTestCase(scenario).sourceLocation,
+      result: state.runTests[scenario.name].result
+    });
+  });
+  eventBroadcaster.emit("test-run-finished", {});
+
+  return JSON.parse(output);
+}
+
+module.exports = { generateCucumberJson };
diff --git a/lib/loader.js b/lib/loader.js
index 7e10465..e4906b3 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -1,10 +1,12 @@
 /* eslint-disable no-eval */
 const log = require("debug")("cypress:cucumber");
+const path = require("path");
+const cosmiconfig = require("cosmiconfig");
 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
-const createCucumber = (spec, toRequire) =>
+const createCucumber = (filePath, cucumberJson, spec, toRequire) =>
   `
   const {resolveAndRunStepDefinition, defineParameterType, given, when, then, and, but, defineStep} = require('cypress-cucumber-preprocessor/lib/resolveStepDefinition');
   const Given = window.Given = window.given = given;
@@ -16,17 +18,30 @@ const createCucumber = (spec, toRequire) =>
   window.defineStep = defineStep;
   const { createTestsFromFeature } = require('cypress-cucumber-preprocessor/lib/createTestsFromFeature');
   ${eval(toRequire).join("\n")}
-  const {Parser, Compiler} = require('gherkin');
-  const spec = \`${spec}\`
-  const gherkinAst = new Parser().parse(spec);
-
-  createTestsFromFeature(gherkinAst);
+  const spec = \`${spec}\`;
+  const filePath = '${filePath}';
+  window.cucumberJson = ${JSON.stringify(cucumberJson)};
+     
+  createTestsFromFeature(filePath, spec);
   `;
 
-module.exports = function(spec, filePath = this.resourcePath) {
+module.exports = (spec, filePath = this.resourcePath) => {
+  const explorer = cosmiconfig("cypress-cucumber-preprocessor", { sync: true });
+  const loaded = explorer.load();
+  const cucumberJson =
+    loaded && loaded.config && loaded.config.cucumberJson
+      ? loaded.config.cucumberJson
+      : { generate: false };
+
   log("compiling", spec);
+  log("cucumber.json", JSON.stringify(cucumberJson));
   const stepDefinitionsToRequire = getStepDefinitionsPaths(filePath).map(
     sdPath => `require('${sdPath}')`
   );
-  return createCucumber(spec, stepDefinitionsToRequire);
+  return createCucumber(
+    path.basename(filePath),
+    cucumberJson,
+    spec,
+    stepDefinitionsToRequire
+  );
 };
diff --git a/lib/setup.js b/lib/setup.js
index d5aa4b5..29894cc 100644
--- a/lib/setup.js
+++ b/lib/setup.js
@@ -1,7 +1,8 @@
 /* eslint-disable global-require */
 /* global jest */
 const fs = require("fs");
-const { Parser } = require("gherkin");
+const { createTestsFromFeature } = require("./createTestsFromFeature");
+
 const {
   defineParameterType,
   defineStep,
@@ -12,6 +13,8 @@ const {
   but
 } = require("./resolveStepDefinition");
 
+const mockedPromise = jest.fn().mockImplementation(() => Promise.resolve(true));
+
 window.defineParameterType = defineParameterType;
 window.when = when;
 window.then = then;
@@ -20,18 +23,31 @@ window.and = and;
 window.but = but;
 window.defineStep = defineStep;
 window.cy = {
-  log: jest.fn()
+  log: jest.fn(),
+  logStep: mockedPromise,
+  startScenario: mockedPromise,
+  finishScenario: mockedPromise,
+  startStep: mockedPromise,
+  finishStep: mockedPromise,
+  finishTest: mockedPromise,
+  then: mockedPromise,
+  end: mockedPromise
 };
 
+window.before = jest.fn();
+window.after = jest.fn();
+
 window.Cypress = {
-  env: jest.fn()
+  env: jest.fn(),
+  on: jest.fn(),
+  off: jest.fn()
 };
 
-const readAndParseFeatureFile = featureFilePath => {
-  const spec = fs.readFileSync(featureFilePath);
-  return new Parser().parse(spec.toString());
+const resolveFeatureFromFile = featureFile => {
+  const spec = fs.readFileSync(featureFile);
+  createTestsFromFeature(featureFile, spec);
 };
 
 module.exports = {
-  readAndParseFeatureFile
+  resolveFeatureFromFile
 };


Test patch:
diff --git a/lib/createTestFromScenario.js b/lib/createTestFromScenario.js
index 2fe13bc..838304e 100644
--- a/lib/createTestFromScenario.js
+++ b/lib/createTestFromScenario.js
@@ -1,5 +1,7 @@
 /* eslint-disable prefer-template */
+const statuses = require("cucumber").Status;
 const { resolveAndRunStepDefinition } = require("./resolveStepDefinition");
+const { generateCucumberJson } = require("./cukejson/generateCucumberJson");
 
 const replaceParameterTags = (rowData, text) =>
   Object.keys(rowData).reduce(
@@ -7,58 +9,137 @@ const replaceParameterTags = (rowData, text) =>
     text
   );
 
-const stepTest = function(stepDetails, exampleRowData) {
-  cy.log(`${stepDetails.keyword} ${stepDetails.text}`);
-  resolveAndRunStepDefinition.call(
-    this,
-    stepDetails,
-    replaceParameterTags,
-    exampleRowData
+// eslint-disable-next-line func-names
+const stepTest = function(state, stepDetails, exampleRowData) {
+  cy.then(() => state.onStartStep(stepDetails))
+    .then(() =>
+      resolveAndRunStepDefinition.call(
+        this,
+        stepDetails,
+        replaceParameterTags,
+        exampleRowData
+      )
+    )
+    .then(() => state.onFinishStep(stepDetails, statuses.PASSED));
+};
+
+const runTest = (scenario, stepsToRun, rowData) => {
+  const indexedSteps = stepsToRun.map((step, index) =>
+    Object.assign({}, step, { index })
   );
+
+  // eslint-disable-next-line func-names
+  it(scenario.name, function() {
+    const state = window.testState;
+    return cy
+      .then(() => state.onStartScenario(scenario, indexedSteps))
+      .then(() =>
+        indexedSteps.forEach(step => stepTest.call(this, state, step, rowData))
+      )
+      .then(() => state.onFinishScenario(scenario));
+  });
+};
+
+const cleanupFilename = s => s.split(".")[0];
+
+const writeCucumberJsonFile = json => {
+  const outputFolder =
+    window.cucumberJson.outputFolder || "cypress/cucumber-json";
+  const outputPrefix = window.cucumberJson.filePrefix || "";
+  const outputSuffix = window.cucumberJson.fileSuffix || ".cucumber";
+  const fileName = json[0] ? cleanupFilename(json[0].uri) : "empty";
+  const outFile = `${outputFolder}/${outputPrefix}${fileName}${outputSuffix}.json`;
+  cy.writeFile(outFile, json, { log: false });
 };
 
-const createTestFromScenario = (scenario, backgroundSection) => {
-  if (scenario.examples) {
-    scenario.examples.forEach(example => {
-      const exampleValues = [];
+const createTestFromScenarios = (
+  scenariosToRun,
+  backgroundSection,
+  testState
+) => {
+  // eslint-disable-next-line func-names
+  describe(testState.feature.name, function() {
+    before(() => {
+      cy.then(() => testState.onStartTest());
+    });
+
+    // ctx is cleared between each 'it'
+    // eslint-disable-next-line func-names, prefer-arrow-callback
+    beforeEach(function() {
+      window.testState = testState;
+
+      const failHandler = err => {
+        Cypress.off("fail", failHandler);
+        testState.onFail(err);
+        throw err;
+      };
+
+      Cypress.on("fail", failHandler);
+    });
+
+    scenariosToRun.forEach(section => {
+      if (section.examples) {
+        section.examples.forEach(example => {
+          const exampleValues = [];
+          const exampleLocations = [];
 
-      example.tableBody.forEach((row, rowIndex) => {
-        example.tableHeader.cells.forEach((header, headerIndex) => {
-          exampleValues[rowIndex] = Object.assign({}, exampleValues[rowIndex], {
-            [header.value]: row.cells[headerIndex].value
+          example.tableBody.forEach((row, rowIndex) => {
+            exampleLocations[rowIndex] = row.location;
+            example.tableHeader.cells.forEach((header, headerIndex) => {
+              exampleValues[rowIndex] = Object.assign(
+                {},
+                exampleValues[rowIndex],
+                {
+                  [header.value]: row.cells[headerIndex].value
+                }
+              );
+            });
           });
-        });
-      });
 
-      exampleValues.forEach((rowData, index) => {
-        // eslint-disable-next-line prefer-arrow-callback
-        const scenarioName = replaceParameterTags(rowData, scenario.name);
-        it(`${scenarioName} (example #${index + 1})`, function() {
-          if (backgroundSection) {
-            backgroundSection.steps.forEach(step => {
-              stepTest.call(this, step);
+          exampleValues.forEach((rowData, index) => {
+            // eslint-disable-next-line prefer-arrow-callback
+            const scenarioName = replaceParameterTags(rowData, section.name);
+            const uniqueScenarioName = `${scenarioName} (example #${index +
+              1})`;
+            const exampleSteps = section.steps.map(step => {
+              const newStep = Object.assign({}, step);
+              newStep.text = replaceParameterTags(rowData, newStep.text);
+              return newStep;
             });
-          }
 
-          scenario.steps.forEach(step => {
-            const newStep = Object.assign({}, step);
-            newStep.text = replaceParameterTags(rowData, newStep.text);
+            const stepsToRun = backgroundSection
+              ? backgroundSection.steps.concat(exampleSteps)
+              : exampleSteps;
 
-            stepTest.call(this, newStep, rowData);
+            const scenarioExample = Object.assign({}, section, {
+              name: uniqueScenarioName,
+              example: exampleLocations[index]
+            });
+
+            runTest.call(this, scenarioExample, stepsToRun, rowData);
           });
         });
-      });
-    });
-  } else {
-    it(scenario.name, function() {
-      if (backgroundSection) {
-        backgroundSection.steps.forEach(step => stepTest.call(this, step));
+      } else {
+        const stepsToRun = backgroundSection
+          ? backgroundSection.steps.concat(section.steps)
+          : section.steps;
+
+        runTest.call(this, section, stepsToRun);
       }
-      scenario.steps.forEach(step => stepTest.call(this, step));
     });
-  }
+
+    // eslint-disable-next-line func-names, prefer-arrow-callback
+    after(function() {
+      cy.then(() => testState.onFinishTest()).then(() => {
+        if (window.cucumberJson.generate) {
+          const json = generateCucumberJson(testState);
+          writeCucumberJsonFile(json);
+        }
+      });
+    });
+  });
 };
 
 module.exports = {
-  createTestFromScenario
+  createTestFromScenarios
 };
diff --git a/lib/createTestsFromFeature.js b/lib/createTestsFromFeature.js
index 914b3a0..a5bc69a 100644
--- a/lib/createTestsFromFeature.js
+++ b/lib/createTestsFromFeature.js
@@ -1,10 +1,12 @@
-const { createTestFromScenario } = require("./createTestFromScenario");
+const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector");
+const { createTestFromScenarios } = require("./createTestFromScenario");
 const { shouldProceedCurrentStep, getEnvTags } = require("./tagsHelper");
 
-const createTestsFromFeature = parsedFeature => {
-  const featureTags = parsedFeature.feature.tags;
+const createTestsFromFeature = (filePath, spec) => {
+  const testState = new CucumberDataCollector(filePath, spec);
+  const featureTags = testState.feature.tags;
   const hasEnvTags = !!getEnvTags();
-  const sectionsWithTags = parsedFeature.feature.children.filter(
+  const sectionsWithTags = testState.feature.children.filter(
     section => section.tags && section.tags.length
   );
 
@@ -16,7 +18,7 @@ const createTestsFromFeature = parsedFeature => {
   let anyFocused = false;
   if (hasEnvTags) {
     featureShouldRun = shouldProceedCurrentStep(featureTags);
-    taggedScenarioShouldRun = parsedFeature.feature.children.some(
+    taggedScenarioShouldRun = testState.feature.children.some(
       section =>
         section.tags &&
         section.tags.length &&
@@ -36,29 +38,26 @@ const createTestsFromFeature = parsedFeature => {
   }
 
   // eslint-disable-next-line prefer-arrow-callback
-  describe(parsedFeature.feature.name, function() {
-    if (everythingShouldRun || featureShouldRun || taggedScenarioShouldRun) {
-      const backgroundSection = parsedFeature.feature.children.find(
-        section => section.type === "Background"
-      );
-      const otherSections = parsedFeature.feature.children.filter(
-        section => section.type !== "Background"
-      );
-      otherSections.forEach(section => {
-        let shouldRun;
-        if (anyFocused) {
-          shouldRun = section.tags.find(t => t.name === "@focus");
-        } else {
-          shouldRun =
-            everythingShouldRun ||
-            shouldProceedCurrentStep(section.tags.concat(featureTags)); // concat handles inheritance of tags from feature
-        }
-        if (shouldRun) {
-          createTestFromScenario(section, backgroundSection);
-        }
-      });
-    }
-  });
+  if (everythingShouldRun || featureShouldRun || taggedScenarioShouldRun) {
+    const backgroundSection = testState.feature.children.find(
+      section => section.type === "Background"
+    );
+    const otherSections = testState.feature.children.filter(
+      section => section.type !== "Background"
+    );
+    const scenariosToRun = otherSections.filter(section => {
+      let shouldRun;
+      if (anyFocused) {
+        shouldRun = section.tags.find(t => t.name === "@focus");
+      } else {
+        shouldRun =
+          everythingShouldRun ||
+          shouldProceedCurrentStep(section.tags.concat(featureTags)); // Concat handles inheritance of tags from feature
+      }
+      return shouldRun;
+    });
+    createTestFromScenarios(scenariosToRun, backgroundSection, testState);
+  }
 };
 
 module.exports = {
diff --git a/lib/cukejson/cucumberDataCollector.test.js b/lib/cukejson/cucumberDataCollector.test.js
new file mode 100644
index 0000000..a59441f
--- /dev/null
+++ b/lib/cukejson/cucumberDataCollector.test.js
@@ -0,0 +1,164 @@
+const fs = require("fs");
+const statuses = require("cucumber").Status;
+const { CucumberDataCollector } = require("./cucumberDataCollector");
+const { generateCucumberJson } = require("./generateCucumberJson");
+
+window.cucumberJson = { generate: true };
+
+window.Cypress = {
+  log: jest.fn()
+};
+
+const assertCucumberJson = (json, expectedResults) => {
+  expect(json).to.have.length(1);
+  expect(json[0].keyword).to.eql("Feature");
+  expect(json[0].name).to.eql("Being a plugin");
+  expect(json[0].elements).to.have.length(1);
+  expect(json[0].elements[0].keyword).to.eql("Scenario");
+  expect(json[0].elements[0].name).to.eql("Basic example");
+  expect(json[0].elements[0].steps).to.have.length(3);
+  expect(json[0].elements[0].steps[0].keyword).to.equal("Given ");
+  expect(json[0].elements[0].steps[0].name).to.equal(
+    "a feature and a matching step definition file"
+  );
+  // eslint-disable-next-line no-unused-expressions
+  expect(json[0].elements[0].steps[0].result).to.be.not.null;
+  expect(json[0].elements[0].steps[0].result.status).to.eql(expectedResults[0]);
+  expect(json[0].elements[0].steps[1].keyword).to.equal("When ");
+  expect(json[0].elements[0].steps[1].name).to.equal("I run cypress tests");
+  // eslint-disable-next-line no-unused-expressions
+  expect(json[0].elements[0].steps[1].result).to.be.not.null;
+  expect(json[0].elements[0].steps[1].result.status).to.eql(expectedResults[1]);
+  expect(json[0].elements[0].steps[2].keyword).to.equal("Then ");
+  expect(json[0].elements[0].steps[2].name).to.equal("they run properly");
+  // eslint-disable-next-line no-unused-expressions
+  expect(json[0].elements[0].steps[2].result).to.be.not.null;
+  expect(json[0].elements[0].steps[2].result.status).to.eql(expectedResults[2]);
+};
+describe("Cucumber Data Collector", () => {
+  const scenario = {
+    type: "Scenario",
+    tags: [],
+    location: { line: 7, column: 3 },
+    keyword: "Scenario",
+    name: "Basic example",
+    steps: [
+      {
+        type: "Step",
+        location: { line: 8, column: 5 },
+        keyword: "Given ",
+        text: "a feature and a matching step definition file"
+      },
+      {
+        type: "Step",
+        location: { line: 9, column: 5 },
+        keyword: "When ",
+        text: "I run cypress tests"
+      },
+      {
+        type: "Step",
+        location: { line: 10, column: 5 },
+        keyword: "Then ",
+        text: "they run properly"
+      }
+    ]
+  };
+
+  const stepsToRun = [
+    {
+      type: "Step",
+      location: { line: 8, column: 5 },
+      keyword: "Given ",
+      text: "a feature and a matching step definition file",
+      index: 0
+    },
+    {
+      type: "Step",
+      location: { line: 9, column: 5 },
+      keyword: "When ",
+      text: "I run cypress tests",
+      index: 1
+    },
+    {
+      type: "Step",
+      location: { line: 10, column: 5 },
+      keyword: "Then ",
+      text: "they run properly",
+      index: 2
+    }
+  ];
+
+  beforeEach(() => {
+    const filePath = "./cypress/integration/Plugin.feature";
+    const spec = fs.readFileSync(filePath);
+    this.testState = new CucumberDataCollector(filePath, spec);
+    this.testState.onStartTest();
+  });
+
+  it("runs", () => {
+    this.testState.onFinishTest();
+    const json = generateCucumberJson(this.testState);
+    expect(json).to.have.length(0);
+  });
+
+  it("records pending scenarios", () => {
+    this.testState.onStartScenario(scenario, stepsToRun);
+    this.testState.onFinishScenario(scenario);
+    this.testState.onFinishTest();
+    const json = generateCucumberJson(this.testState);
+    assertCucumberJson(json, [
+      statuses.SKIPPED,
+      statuses.SKIPPED,
+      statuses.SKIPPED
+    ]);
+  });
+  it("records passed scenarios", () => {
+    this.testState.onStartScenario(scenario, stepsToRun);
+    this.testState.onStartStep(stepsToRun[0]);
+    this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
+    this.testState.onStartStep(stepsToRun[1]);
+    this.testState.onFinishStep(stepsToRun[1], statuses.PASSED);
+    this.testState.onStartStep(stepsToRun[2]);
+    this.testState.onFinishStep(stepsToRun[2], statuses.PASSED);
+    this.testState.onFinishScenario(scenario);
+    this.testState.onFinishTest();
+    const json = generateCucumberJson(this.testState);
+    assertCucumberJson(json, [
+      statuses.PASSED,
+      statuses.PASSED,
+      statuses.PASSED
+    ]);
+  });
+
+  it("records failed scenarios", () => {
+    this.testState.onStartScenario(scenario, stepsToRun);
+    this.testState.onStartStep(stepsToRun[0]);
+    this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
+    this.testState.onStartStep(stepsToRun[1]);
+    this.testState.onFinishStep(stepsToRun[1], statuses.FAILED);
+    this.testState.onFinishScenario(scenario);
+    this.testState.onFinishTest();
+    const json = generateCucumberJson(this.testState);
+    assertCucumberJson(json, [
+      statuses.PASSED,
+      statuses.FAILED,
+      statuses.SKIPPED
+    ]);
+  });
+
+  it("handles missing steps", () => {
+    this.testState.onStartScenario(scenario, stepsToRun);
+    this.testState.onStartStep(stepsToRun[0]);
+    this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
+    this.testState.onStartStep(stepsToRun[1]);
+    this.testState.onFinishStep(stepsToRun[1], statuses.UNDEFINED);
+    this.testState.onFinishScenario(scenario);
+    this.testState.onFinishTest();
+    const json = generateCucumberJson(this.testState);
+    assertCucumberJson(json, [
+      statuses.PASSED,
+      statuses.UNDEFINED,
+      statuses.SKIPPED
+    ]);
+  });
+});
diff --git a/lib/resolveStepDefinition.test.js b/lib/resolveStepDefinition.test.js
index 06e454a..30be458 100644
--- a/lib/resolveStepDefinition.test.js
+++ b/lib/resolveStepDefinition.test.js
@@ -1,113 +1,81 @@
 /* eslint-disable global-require */
-/* global jest */
-const { readAndParseFeatureFile } = require("./setup");
-const { createTestsFromFeature } = require("./createTestsFromFeature");
+
+const { resolveFeatureFromFile } = require("./setup.js");
 
 describe("Scenario Outline", () => {
   require("../cypress/support/step_definitions/scenario_outline_integer");
   require("../cypress/support/step_definitions/scenario_outline_string");
   require("../cypress/support/step_definitions/scenario_outline_data_table");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/ScenarioOutline.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/ScenarioOutline.feature");
 });
 
 describe("DocString", () => {
   require("../cypress/support/step_definitions/docString");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/DocString.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/DocString.feature");
 });
 
 describe("Data table", () => {
   require("../cypress/support/step_definitions/dataTable");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/DataTable.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/DataTable.feature");
 });
 
 describe("Basic example", () => {
   require("../cypress/support/step_definitions/basic");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/Plugin.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/Plugin.feature");
 });
 
 describe("Background section", () => {
   require("../cypress/support/step_definitions/backgroundSection");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/BackgroundSection.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/BackgroundSection.feature");
 });
 
 describe("Regexp", () => {
   require("../cypress/support/step_definitions/regexp");
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/RegularExpressions.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/RegularExpressions.feature");
 });
 
 describe("Custom Parameter Types", () => {
   require("../cypress/support/step_definitions/customParameterTypes");
-  createTestsFromFeature(
-    readAndParseFeatureFile(
-      "./cypress/integration/CustomParameterTypes.feature"
-    )
-  );
+  resolveFeatureFromFile("./cypress/integration/CustomParameterTypes.feature");
 });
 
 describe("Tags implementation", () => {
   require("../cypress/support/step_definitions/tags_implementation");
+  resolveFeatureFromFile("./cypress/integration/TagsImplementation.feature");
+});
+
+describe("Tags with env TAGS set", () => {
+  window.Cypress = {
+    env: () => "@test-tag and not @ignore-tag",
+    on: jest.fn(),
+    off: jest.fn()
+  };
+  require("../cypress/support/step_definitions/tags_implementation_with_env_set");
+  resolveFeatureFromFile(
+    "./cypress/integration/TagsImplementationWithEnvSet.feature"
+  );
 
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/TagsImplementation.feature")
+  resolveFeatureFromFile(
+    "./cypress/integration/TagsImplementationWithEnvSetScenarioLevel.feature"
   );
 });
 
 describe("Smart tagging", () => {
+  window.Cypress = {
+    env: () => "",
+    on: jest.fn(),
+    off: jest.fn()
+  };
   require("../cypress/support/step_definitions/smart_tagging");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/SmartTagging.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/SmartTagging.feature");
 });
 
 describe("And and But", () => {
   require("../cypress/support/step_definitions/and_and_but_steps");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/AndAndButSteps.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/AndAndButSteps.feature");
 });
 
 describe("defineStep", () => {
   require("../cypress/support/step_definitions/usingDefineSteps");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/DefineStep.feature")
-  );
-});
-
-describe("Tags with env TAGS set", () => {
-  window.Cypress = {
-    env: () => "@test-tag and not @ignore-tag"
-  };
-  require("../cypress/support/step_definitions/tags_implementation_with_env_set");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile(
-      "./cypress/integration/TagsImplementationWithEnvSet.feature"
-    )
-  );
-
-  createTestsFromFeature(
-    readAndParseFeatureFile(
-      "./cypress/integration/TagsImplementationWithEnvSetScenarioLevel.feature"
-    )
-  );
+  resolveFeatureFromFile("./cypress/integration/DefineStep.feature");
 });
diff --git a/lib/tagsInheritance.test.js b/lib/tagsInheritance.test.js
index eba051c..3e5ed24 100644
--- a/lib/tagsInheritance.test.js
+++ b/lib/tagsInheritance.test.js
@@ -1,16 +1,14 @@
 /* eslint-disable global-require */
 /* global jest */
-const { readAndParseFeatureFile } = require("./setup");
-const { createTestsFromFeature } = require("./createTestsFromFeature");
+const { resolveFeatureFromFile } = require("./setup");
 
 describe("Tags inheritance", () => {
   window.Cypress = {
-    env: () => "@inherited-tag and @own-tag"
+    env: () => "@inherited-tag and @own-tag",
+    on: jest.fn(),
+    off: jest.fn()
   };
 
   require("../cypress/support/step_definitions/tags_implementation_with_env_set");
-
-  createTestsFromFeature(
-    readAndParseFeatureFile("./cypress/integration/TagsInheritance.feature")
-  );
+  resolveFeatureFromFile("./cypress/integration/TagsInheritance.feature");
 });