Repository: nightwatchjs/nightwatch
Pull request #3817 (2023-07-18 15:29:45)

Lifecycle event hooks support for v2


Patch:
diff --git a/lib/api/client-commands/saveScreenshot.js b/lib/api/client-commands/saveScreenshot.js
index ec8b8df2..d827acc7 100644
--- a/lib/api/client-commands/saveScreenshot.js
+++ b/lib/api/client-commands/saveScreenshot.js
@@ -1,5 +1,6 @@
 const ClientCommand = require('./_base-command.js');
 const {Logger, writeToFile} = require('../../utils');
+const {COMMON_EVENTS: {ScreenshotCreated}, NightwatchEventHub} = require('../../runner/eventHub.js');
 
 /**
  * Take a screenshot of the current page and saves it as the given filename.
@@ -24,6 +25,10 @@ class SaveScreenshot extends ClientCommand {
       if (client.transport.isResultSuccess(result) && result.value) {
         writeToFile(fileName, result.value, 'base64').then(() => {
           callback.call(this, result);
+
+          NightwatchEventHub.emit(ScreenshotCreated, {
+            path: fileName
+          });
         }).catch(err => {
           Logger.error(`Couldn't save screenshot to "${fileName}":`);
           Logger.error(err);
diff --git a/lib/http/request.js b/lib/http/request.js
index c0bd74a8..038ad2fd 100644
--- a/lib/http/request.js
+++ b/lib/http/request.js
@@ -11,6 +11,7 @@ const Formatter = require('./formatter.js');
 const HttpResponse = require('./response.js');
 const Utils = require('./../utils');
 const {Logger, isString} = Utils;
+const {DEFAULT_RUNNER_EVENTS: {LogCreated}, NightwatchEventHub} = require('../runner/eventHub.js');
 
 // To handle Node v17 issue. Refer https://github.com/nodejs/node/issues/40702 for details.
 if (dns.setDefaultResultOrder && (typeof dns.setDefaultResultOrder === 'function')) {
@@ -271,6 +272,12 @@ class HttpRequest extends EventEmitter {
       return;
     }
 
+    if (NightwatchEventHub.runner !== 'cucumber') {
+      NightwatchEventHub.emit(LogCreated, {
+        httpOutput: Logger.collectCommandOutput()
+      });
+    }
+
     this.emit('complete', result);
   }
 
diff --git a/lib/index.js b/lib/index.js
index 0f452e54..fc7ecc7e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -4,6 +4,7 @@ const Utils = require('./utils');
 const Settings = require('./settings/settings.js');
 const ElementGlobal = require('./api/_loaders/element-global.js');
 const NightwatchClient = require('./core/client.js');
+const {NightwatchEventHub} = require('./runner/eventHub');
 
 const {Logger} = Utils;
 
@@ -129,8 +130,25 @@ module.exports.createClient = function({
         .then(() => {
           return client.createSession({argv});
         })
-        .then(_ => {
+        .then((data) => {
+          if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) {
+            const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format');
+    
+            NightwatchFormatter.setCapabilities(data);
+          }
+
           return client.api;
+        })
+        .catch((error) => {
+          if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) {
+            const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format');
+    
+            NightwatchFormatter.setCapabilities({
+              error: error
+            });
+          }
+      
+          throw error;
         });
     }
   };
diff --git a/lib/reporter/index.js b/lib/reporter/index.js
index 041a2681..a456f01b 100644
--- a/lib/reporter/index.js
+++ b/lib/reporter/index.js
@@ -72,6 +72,14 @@ class Reporter extends SimplifiedReporter {
     this.testResults.setCurrentSection(testcase);
   }
 
+  markHookRun(hookName) {
+    this.testResults.markHookRun(hookName);
+  }
+
+  unmarkHookRun(hookName) {
+    this.testResults.unmarkHookRun(hookName);
+  }
+
   setAxeResults(results) {
     this.currentTest.results.a11y = results;
   }
diff --git a/lib/reporter/results.js b/lib/reporter/results.js
index c88ea48f..ed1772bf 100644
--- a/lib/reporter/results.js
+++ b/lib/reporter/results.js
@@ -21,6 +21,8 @@ module.exports = class Results {
     this.skippedAtRuntime = tests.slice(0);
     this.testcases = {};
     this.testSections = {};
+    this.testSectionHook = {};
+    this.isHookRunning = false;
     this.suiteName = opts.suiteName;
     this.moduleKey = opts.moduleKey;
     this.modulePath = opts.modulePath;
@@ -57,6 +59,41 @@ module.exports = class Results {
     this.initCount(allScreenedTests);
   }
 
+  markHookRun(hookName) {
+    this.isHookRunning = true;
+    this.currentHookName = hookName;
+    this.testSectionHook = this.createTestCaseResults({testName: `${this.currentSectionName}__${hookName}`});
+
+    // reset test hooks output
+    Logger.collectTestHooksOutput();
+  }
+
+  unmarkHookRun() {
+    this.isHookRunning = false;
+    if (!this.currentHookName) {
+      throw new Error('Hook run not started yet');
+    }
+    
+    const currentSection = this.getTestSection(this.currentSectionName);
+    const hookdata = this.testSectionHook;
+
+    const startTime = hookdata.startTimestamp;
+    const endTime = new Date().getTime();
+    const elapsedTime = endTime - startTime;
+    hookdata.endTimestamp = endTime;
+
+    hookdata.time = (elapsedTime/1000).toPrecision(4);
+    hookdata.timeMs = elapsedTime;
+
+    hookdata.httpOutput = Logger.collectTestHooksOutput();
+
+    if (hookdata.errors > 0 || hookdata.failed > 0) {
+      hookdata.status = Results.TEST_FAIL;
+    }
+
+    currentSection[this.currentHookName] = hookdata;
+  }
+
   get initialResult() {
     return this.__initialResult;
   }
@@ -408,6 +445,10 @@ module.exports = class Results {
     const result = this.getTestSection(this.currentSectionName);
     result.commands.push(command);
 
+    if (this.isHookRunning) {
+      this.testSectionHook.commands.push(command);
+    }
+
     return this;
   }
   
@@ -599,4 +640,15 @@ module.exports = class Results {
       return prev;
     }, initialReport);
   }
+
+  get eventDataToEmit() {
+    const {testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host} = this; 
+    
+    return {
+      envelope: this.testSections,
+      metadata: {
+        testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host
+      }
+    };
+  }
 };
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index b15baf8c..8570d1c9 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -10,6 +10,9 @@ const Runner = require('../runner.js');
 const ProcessListener = require('../process-listener.js');
 const analyticsCollector = require('../../utils/analytics.js');
 const {Logger, singleSourceFile, isSafari, isLocalhost} = Utils;
+const NightwatchEvent = require('../eventHub.js');
+const {NightwatchEventHub, DEFAULT_RUNNER_EVENTS} = NightwatchEvent;
+const {GlobalHook} = DEFAULT_RUNNER_EVENTS;
 const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator} = require('../../utils/mobile.js');
 
 class CliRunner {
@@ -255,6 +258,38 @@ class CliRunner {
       isBstack: this.test_settings.desiredCapabilities['bstack:options'] !== undefined,
       test_runner: this.test_settings.test_runner ? this.test_settings.test_runner.type : null
     });
+
+    this.setupEventHub();
+  }
+
+  isRegisterEventHandlersCallbackExistsInGlobal() {
+    const {globals} = this.test_settings;
+    const {plugins = []} = this.globals;
+  
+    return Utils.isFunction(globals.registerEventHandlers) || plugins.some(plugin => plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers));
+  }
+
+  setupEventHub() {
+    if (this.isRegisterEventHandlersCallbackExistsInGlobal() && !NightwatchEventHub.isAvailable) {
+      NightwatchEventHub.runner = this.testRunner.type;
+      NightwatchEventHub.isAvailable = true;
+      
+      const {globals, output_folder} = this.test_settings;
+      NightwatchEventHub.output_folder = output_folder;
+      const {plugins} = this.globals;
+  
+      if (Utils.isFunction(globals.registerEventHandlers)) {
+        globals.registerEventHandlers(NightwatchEventHub);
+      }
+  
+      if (plugins.length > 0) {
+        plugins.forEach((plugin) => {
+          if (plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers)){
+            plugin.globals.registerEventHandlers(NightwatchEventHub);
+          }
+        });
+      }
+    }
   }
 
   loadTypescriptTranspiler() {
@@ -354,10 +389,30 @@ class CliRunner {
   }
 
   runGlobalHook(key, args = [], isParallelHook = false) {
+    let promise;
+    const {globals} = this.test_settings;
+
     if (isParallelHook && Concurrency.isChildProcess() || !isParallelHook && !Concurrency.isChildProcess()) {
-      return this.globals.runGlobalHook(key, args);
-    }
+      const start_time = new Date();
+
+      if (Utils.isFunction(globals[key])) {
+        NightwatchEventHub.emit(GlobalHook[key].started, {
+          start_time: start_time
+        });
+      }
+      
+      promise = this.globals.runGlobalHook(key, args);
 
+      return promise.finally(() => {
+        if (Utils.isFunction(globals[key])) {
+          NightwatchEventHub.emit(GlobalHook[key].finished, {
+            start_time: start_time,
+            end_time: new Date()
+          });
+        }
+      });
+    }
+ 
     return Promise.resolve();
   }
 
diff --git a/lib/runner/eventHub.js b/lib/runner/eventHub.js
new file mode 100644
index 00000000..b883b859
--- /dev/null
+++ b/lib/runner/eventHub.js
@@ -0,0 +1,140 @@
+const fs = require('fs');
+const path = require('path');
+const EventEmitter = require('events');
+const {Logger, createFolder} = require('../utils');
+
+class NightwatchEventHub extends EventEmitter {
+  emit(eventName, data) {
+    if (this.isAvailable) {
+      try {
+        super.emit(eventName, data);
+      } catch (err) {
+        Logger.error(err);
+      }
+    }
+
+    if (eventName === 'TestFinished' && this.output_folder && this.runner === 'cucumber') {
+      let {output_folder} = this;
+      output_folder = path.join(output_folder, 'cucumber');
+      const filename = path.join(output_folder, 'cucumber-report.json');
+
+      this.writeReportFile(filename, JSON.stringify(data.report, null, 2), true, output_folder)
+        .then(_ => {
+          Logger.info(Logger.colors.stack_trace(`Wrote JSON report file to: ${path.resolve(filename)}`));
+        });
+    }
+  }
+
+  get isAvailable() {
+    return this.eventFnExist;
+  }
+
+  set isAvailable(eventFnExist) {
+    this.eventFnExist = eventFnExist;
+  }
+
+  set runner(type) {
+    this.runnerType = type;
+  }
+
+  get runner() {
+    return this.runnerType;
+  }
+
+  writeReportFile(filename, rendered, shouldCreateFolder, output_folder) {
+    return (shouldCreateFolder ? createFolder(output_folder) : Promise.resolve())
+      .then(() => {
+        return new Promise((resolve, reject) => {
+          fs.writeFile(filename, rendered, function(err) {
+            if (err) {
+              return reject(err);
+            }
+
+            resolve();
+          });
+        });
+      });
+  }
+}
+
+const instance = new NightwatchEventHub();
+
+module.exports = {
+  NightwatchEventHub: instance,
+  COMMON_EVENTS: {
+    ScreenshotCreated: 'ScreenshotCreated'
+  },
+  DEFAULT_RUNNER_EVENTS: {
+    GlobalHook: {
+      before: {
+        started: 'GlobalBeforeStarted',
+        finished: 'GlobalBeforeFinished'
+      },
+  
+      beforeChildProcess: {
+        started: 'GlobalBeforeChildProcessStarted',
+        finished: 'GlobalBeforeChildProcessFinished'
+      },
+  
+      beforeEach: {
+        started: 'GlobalBeforeEachStarted',
+        finished: 'GlobalBeforeEachFinished'
+      },
+  
+      afterEach: {
+        started: 'GlobalAfterEachStarted',
+        finished: 'GlobalAfterEachFinished'
+      },
+  
+      afterChildProcess: {
+        started: 'GlobalAfterChildProcessStarted',
+        finished: 'GlobalAfterChildProcessFinished'
+      },
+  
+      after: {
+        started: 'GlobalAfterStarted',
+        finished: 'GlobalAfterFinished'
+      }
+    },
+
+    TestSuiteHook: {
+      started: 'TestSuiteStarted',
+      finished: 'TestSuiteFinished',
+      
+      before: {
+        started: 'BeforeStarted',
+        finished: 'BeforeFinished'
+      },
+
+      beforeEach: {
+        started: 'BeforeEachStarted',
+        finished: 'BeforeEachFinished'
+      },
+
+      test: {
+        started: 'TestRunStarted',
+        finished: 'TestRunFinished'
+      },
+  
+      afterEach: {
+        started: 'AfterEachStarted',
+        finished: 'AfterEachFinished'
+      },
+
+      after: {
+        started: 'AfterStarted',
+        finished: 'AfterFinished'
+      }
+    },
+
+    LogCreated: 'LogCreated'
+  },
+  CUCUMBER_RUNNER_EVENTS: {
+    TestStarted: 'TestStarted',
+    TestFinished: 'TestFinished',
+    TestCaseStarted: 'TestCaseStarted',
+    TestCaseFinished: 'TestCaseFinished',
+    TestStepStarted: 'TestStepStarted',
+    TestStepFinished: 'TestStepFinished'
+  }
+};
diff --git a/lib/runner/folder-walk.js b/lib/runner/folder-walk.js
index c3d5a83a..b0a0ea9e 100644
--- a/lib/runner/folder-walk.js
+++ b/lib/runner/folder-walk.js
@@ -39,6 +39,7 @@ class Walker {
     this.settings = lodashClone(settings, true);
     this.argv = argv;
     this.usingMocha = this.settings.test_runner && this.settings.test_runner.type === 'mocha';
+    this.usingCucumber = this.settings.test_runner?.type === 'cucumber';
 
     this.registerMatchers();
 
diff --git a/lib/runner/rerunUtil.js b/lib/runner/rerunUtil.js
new file mode 100644
index 00000000..e8f7ba81
--- /dev/null
+++ b/lib/runner/rerunUtil.js
@@ -0,0 +1,62 @@
+const {fileExistsSync, Logger} = require('../utils');
+const path = require('path');
+
+function getRerunFailedFile(minimal_report_file_path) {
+  const jsonFile = path.resolve(process.env.NIGHTWATCH_RERUN_REPORT_FILE || minimal_report_file_path || '');
+
+  if (!fileExistsSync(jsonFile)) {
+    const err = new Error('Unable to find the Json reporter file to rerun failed tests');
+
+    err.showTrace = false;
+    err.detailedErr = 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path';
+    err.help = [
+      `Try setting ${Logger.colors.cyan('minimal_report_file_path: "JSON-REPORTER-PATH"')} in nightwatch configuration`,
+      `Or, try running: ${Logger.colors.cyan('export NIGHTWATCH_RERUN_REPORT_FILE="JSON-REPORTER-PATH"')}`
+    ];
+
+    throw err;
+  }
+
+  return jsonFile;
+}
+
+function getTestSourceForRerunFailed(settings) {
+  const {reporter_options: {minimal_report_file_path}} = settings;
+  const minimalJsonFile = getRerunFailedFile(minimal_report_file_path);
+
+  try {
+    const {modules = {}} = require(minimalJsonFile);
+    const testsource = [];
+  
+    Object.keys(modules).forEach(moduleKey => {
+      if (modules[moduleKey] && modules[moduleKey].status === 'fail') {
+        testsource.push(modules[moduleKey].modulePath);
+      }
+    });
+
+    if (testsource.length === 0) {
+      const err = new Error('Rerun Failed Tests: No failed tests found to rerun.');
+      err.noFailedTestFound = true;
+      err.showTrace = false;
+      err.detailedErr = 'Run nightwatch with --help to display usage info.';
+
+      throw err;
+    }
+
+    return testsource;
+  } catch (err) {
+    if (err.noFailedTestFound) {
+      err.message = 'Rerun Failed Tests: Invalid Json reporter.';
+
+      err.showTrace = false;
+      err.detailedErr = 'Please set env variable NIGHTWATCH_RERUN_REPORT_FILE with valid Json reporter path.';
+    }
+
+    throw err;
+  }
+}
+
+module.exports = {
+  getRerunFailedFile,
+  getTestSourceForRerunFailed
+};
diff --git a/cucumber-js/README.md b/lib/runner/test-runners/cucumber/README.md
similarity index 100%
rename from cucumber-js/README.md
rename to lib/runner/test-runners/cucumber/README.md
diff --git a/cucumber-js/_setup_cucumber_runner.js b/lib/runner/test-runners/cucumber/_setup_cucumber_runner.js
similarity index 84%
rename from cucumber-js/_setup_cucumber_runner.js
rename to lib/runner/test-runners/cucumber/_setup_cucumber_runner.js
index 0ed0c506..a860a4c3 100644
--- a/cucumber-js/_setup_cucumber_runner.js
+++ b/lib/runner/test-runners/cucumber/_setup_cucumber_runner.js
@@ -1,11 +1,16 @@
-const Nightwatch = require('../lib');
+const Nightwatch = require('../../../index.js');
 const {Before, After, setDefaultTimeout} = require('@cucumber/cucumber');
+const NightwatchFormatter = require('./nightwatch-format.js');
 
 setDefaultTimeout(-1);
 
-Before(function({pickle}) {
+Before(function({pickle, testCaseStartedId}) {
   const webdriver = {};
 
+  process.env.CUCUMBER_TEST_CASE_STARTED_ID = testCaseStartedId;
+  // eslint-disable-next-line no-console
+  console.log('setup_cucumber_before', 'PID:', process.pid, 'testCaseStartedId:', testCaseStartedId);
+
   if (this.parameters['webdriver-host']) {
     webdriver.host = this.parameters['webdriver-host'];
   }
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index 4a8824c1..fb0c693c 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -11,7 +11,6 @@ const CDP = require('./cdp.js');
 const {colors} = Logger;
 const {isErrorResponse, checkLegacyResponse, throwDecodedError, WebDriverError} = error;
 
-
 let _driverService = null;
 let _driver = null;
 
@@ -295,10 +294,12 @@ class Transport extends BaseTransport {
   
       this.__elementKey = elementKey;
       await this.showConnectInfo({startTime, host, port, start_process, sessionInfo});
-  
+
       return {
         sessionId,
-        capabilities
+        capabilities,
+        host, 
+        port
       };
     } catch (err) {
       const error = this.handleConnectError(err);
diff --git a/lib/utils/logger/index.js b/lib/utils/logger/index.js
index 4ac84db2..a9a0bc5a 100644
--- a/lib/utils/logger/index.js
+++ b/lib/utils/logger/index.js
@@ -35,7 +35,7 @@ function timestamp(d = new Date(), format) {
     return d.toISOString();
   }
 
-  let time = [
+  const time = [
     pad(d.getHours()),
     pad(d.getMinutes()),
     pad(d.getSeconds())
@@ -75,8 +75,8 @@ function logMessage(type, message, args, alwaysShow) {
   let messageStr = '';
   let logMethod = 'log';
   let prefix;
-  let d = new Date();
-  let timeIso = d.toISOString();
+  const d = new Date();
+  const timeIso = d.toISOString();
   let timestamp = logTimestamp(d);
 
   switch (type) {
@@ -152,7 +152,9 @@ function logRequest(message, params) {
   const instance = Logger.getInstance();
 
   instance.output.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
+  instance.commandOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
   instance.testSectionOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
+  instance.testHooksOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
 }
 
 function logError(severity, errOrMessage, args) {
@@ -168,7 +170,9 @@ class Logger {
   constructor() {
     this.colors = colors;
     this.output = [];
+    this.commandOutput = [];
     this.testSectionOutput = [];
+    this.testHooksOutput = [];
   }
 
   logMessage(...args) {
@@ -462,3 +466,29 @@ module.exports.collectTestSectionOutput = function() {
 
   return testSectionOutput;
 };
+
+module.exports.collectCommandOutput = function() {
+  const instance = Logger.getInstance();
+
+  if (!instance) {
+    return [];
+  }
+
+  const {commandOutput} = instance;
+  instance.commandOutput = [];
+
+  return commandOutput;
+};
+
+module.exports.collectTestHooksOutput = function() {
+  const instance = Logger.getInstance();
+
+  if (!instance) {
+    return [];
+  }
+
+  const {testHooksOutput} = instance;
+  instance.testHooksOutput = [];
+
+  return testHooksOutput;
+};
diff --git a/package.json b/package.json
index 5048e1e0..80df92c6 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
   },
   "man": "",
   "scripts": {
-    "eslint": "eslint index.js lib bin api examples cucumber-js test --quiet",
+    "eslint": "eslint index.js lib bin api examples test --quiet",
     "mocha-coverage": "nyc --reporter=html mocha test/src/ --recursive",
     "test": "mocha test/src/ --recursive --timeout 20000",
     "component-tests": "mocha test/component-tests/src",


Test patch:
diff --git a/lib/runner/test-runners/cucumber.js b/lib/runner/test-runners/cucumber.js
index 8fb18a69..4881b1fd 100644
--- a/lib/runner/test-runners/cucumber.js
+++ b/lib/runner/test-runners/cucumber.js
@@ -2,6 +2,8 @@ const path = require('path');
 const Runner = require('./default');
 const TestSuite =  require('../../testsuite');
 const {Logger, isString, isDefined} = require('../../utils');
+const {NightwatchEventHub} = require('../eventHub');
+const {getTestSourceForRerunFailed} = require('../rerunUtil');
 
 class CucumberSuite extends TestSuite {
   static isSessionCreateError(err) {
@@ -35,7 +37,7 @@ class CucumberSuite extends TestSuite {
   }
 
   static get cucumberSetupFile() {
-    return path.join(__dirname, '../../../cucumber-js/_setup_cucumber_runner.js');
+    return path.join(__dirname, './cucumber/_setup_cucumber_runner.js');
   }
 
   constructor({modulePath, modules, settings, argv}) {
@@ -88,13 +90,29 @@ class CucumberSuite extends TestSuite {
     });
   }
 
+  shouldUseNightwatchFormatter(options) {
+    return (NightwatchEventHub.isAvailable || 
+      options.format === 'nightwatch-format' || 
+      this.argv.format === 'nightwatch-format'
+    );
+  }
+
   getCliArgvForCucumber() {
     const specs = this.createInitialRequires();
     const {options} = this.settings.test_runner;
 
+    if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed'])) {
+      options.feature_path = getTestSourceForRerunFailed(this.settings);
+    }
+
     if (options.feature_path && !Array.isArray(options.feature_path)) {
       options.feature_path = [options.feature_path];
     }
+
+    if (this.shouldUseNightwatchFormatter(options)) {
+      options.format = [...(options.format || []), path.join(__dirname, './cucumber/nightwatch-format.js')];
+    }
+
     const {feature_path = ''} = options;
     const parallelArgs = this.usingCucumberWorkers ? ['--parallel', this.usingCucumberWorkers]: [];
     const additionalOptions = this.buildArgvValue(['tags', 'retry-tag-filter', 'profile', 'format', 'format-options', 'dry-run', 'fail-fast', ['retry', 'retries'], 'no-strict', 'name']);
@@ -120,6 +138,15 @@ class CucumberSuite extends TestSuite {
     }, initialRequires);
   }
 
+  mergeCliConfigValues(key) {
+    const {options = {}} = this.settings.test_runner;
+
+    return Array.from(new Set([
+      ...(Array.isArray(this.argv[key]) ? this.argv[key] : (isString(this.argv[key]) ? [this.argv[key]] : [])),
+      ...(Array.isArray(options[key]) ? options[key] : (isString(options[key]) ? [options[key]] : []))
+    ]));
+  }
+
   buildArgvValue(argNames) {
     if (isString(argNames)) {
       argNames = [argNames];
@@ -137,7 +164,12 @@ class CucumberSuite extends TestSuite {
 
       const allArgv = {
         ...options,
-        ...this.argv
+        ...this.argv,
+        require: this.mergeCliConfigValues('require'),
+        requireModule: this.mergeCliConfigValues('requireModule'),
+        format: this.mergeCliConfigValues('format'),
+        paths: this.mergeCliConfigValues('paths'),
+        import: this.mergeCliConfigValues('import')
       };
 
       if ((isDefined(allArgv[argName]) && allArgv[argName] !== '') || 
@@ -206,7 +238,7 @@ class CucumberSuite extends TestSuite {
 
 class CucumberRunnner extends Runner {
   get supportsConcurrency() {
-    return true;
+    return false;
   }
 
   get type() {
diff --git a/lib/runner/test-runners/cucumber/nightwatch-format.js b/lib/runner/test-runners/cucumber/nightwatch-format.js
new file mode 100644
index 00000000..ebc64890
--- /dev/null
+++ b/lib/runner/test-runners/cucumber/nightwatch-format.js
@@ -0,0 +1,173 @@
+const Utils = require('../../../utils');
+const {Logger, SafeJSON, isFunction} = Utils;
+const {Formatter} = require('@cucumber/cucumber');
+
+const {NightwatchEventHub, CUCUMBER_RUNNER_EVENTS: {
+  TestStarted, 
+  TestFinished,
+  TestCaseStarted,
+  TestCaseFinished,
+  TestStepStarted,
+  TestStepFinished
+}} = require('../../eventHub.js');
+
+module.exports = class NightwatchFormatter extends Formatter {
+  constructor(options) {
+    super(options);
+
+    this.report = {};
+
+    NightwatchFormatter.eventBroadcaster = options.eventBroadcaster;
+
+    options.eventBroadcaster.on('envelope', (envelope) => {
+      this.reportHandler(envelope);
+    });
+  }
+
+  static setCapabilities(data) {
+    data = {...data, testCaseStartedId: process.env.CUCUMBER_TEST_CASE_STARTED_ID};
+
+    if (isFunction(process.send)) {
+      process.send({'jsonEnvelope': SafeJSON.stringify({
+        session: {
+          ...data,
+          workerId: process.env.CUCUMBER_WORKER_ID
+        }})});
+    } else {
+      NightwatchFormatter.eventBroadcaster?.emit('envelope', {session: data});
+    }
+  }
+
+  onSessionCapabilities(envelope) {
+    this.report.session = this.report.session || {};
+
+    this.report.session[envelope.testCaseStartedId] = envelope;
+  }
+
+  onMeta(meta) {
+    this.report.metadata = meta;
+  }
+
+  onGherkinDocument(gherkinDocument) {
+    this.report.gherkinDocument = [...(this.report.gherkinDocument || []), gherkinDocument];
+  }
+
+  onParseError(parseError) {
+    this.report.error = [...(this.report.error || []), parseError];
+  }
+
+  onPickle(pickle) {
+    this.report.pickle = [...(this.report.pickle || []), pickle];
+  }
+
+  onHook(hook) {
+    this.report.hooks = [...(this.report.hooks || []), hook];
+  }
+
+  onSource(source) {
+    this.report.source = source;
+  }
+
+  onStepDefinition(stepDefinition) {
+    this.report.stepDefinition = [...(this.report.stepDefinition || []), stepDefinition];
+  }
+
+  onTestCase(testCase) {
+    this.report.testCases = [...(this.report.testCases || []), testCase];
+  }
+
+  onTestCaseFinished(result) {
+    result.httpOutput = Logger.collectTestSectionOutput();
+
+    this.report.testCaseFinished = this.report.testCaseFinished || {};
+    this.report.testCaseFinished[result.testCaseStartedId] = result;
+
+    NightwatchEventHub.emit(TestCaseFinished, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  onTestCaseStarted(result) {
+    this.report.testCaseStarted = this.report.testCaseStarted || {};
+    this.report.testCaseStarted[result.id] = result;
+
+    NightwatchEventHub.emit(TestCaseStarted, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  onTestRunFinished(result) {
+    result.httpOutput = Logger.collectOutput();
+    this.report.testRunFinished = result;
+
+    NightwatchEventHub.emit(TestFinished, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  onTestRunStarted(result) {
+    this.report.testRunStarted = result;
+
+    NightwatchEventHub.emit(TestStarted, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  onTestStepFinished(result) {
+    result.httpOutput = Logger.collectCommandOutput();
+    this.report.testStepFinished = this.report.testStepFinished || {};
+    this.report.testStepFinished[result.testCaseStartedId] = result;
+
+    NightwatchEventHub.emit(TestStepFinished, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  onTestStepStarted(result) {
+    this.report.testStepStarted = this.report.testStepStarted || {};
+    this.report.testStepStarted[result.testCaseStartedId] = result;
+
+    NightwatchEventHub.emit(TestStepStarted, {
+      envelope: result,
+      report: this.report
+    });
+  }
+
+  reportHandler(envelope) {
+    try {
+      if (!NightwatchEventHub.isAvailable) {
+        return;
+      }
+  
+      const handlers = {
+        meta: this.onMeta,
+        gherkinDocument: this.onGherkinDocument,
+        parseError: this.onParseError,
+        pickle: this.onPickle,
+        source: this.onSource,
+        stepDefinition: this.onStepDefinition,
+        testCase: this.onTestCase,
+        hook: this.onHook,
+        testCaseFinished: this.onTestCaseFinished,
+        testCaseStarted: this.onTestCaseStarted,
+        testRunFinished: this.onTestRunFinished,
+        testRunStarted: this.onTestRunStarted,
+        testStepFinished: this.onTestStepFinished,
+        testStepStarted: this.onTestStepStarted,
+        session: this.onSessionCapabilities
+      };
+    
+      const cucumberEvent = Object.keys(envelope)[0];
+      if (cucumberEvent && handlers[cucumberEvent]) {
+        handlers[cucumberEvent].call(this, envelope[cucumberEvent]);
+      }
+    } catch (err) {
+      Logger.error(err);
+    }
+  }
+};
diff --git a/lib/runner/test-source.js b/lib/runner/test-source.js
index b764cd48..642afeaa 100644
--- a/lib/runner/test-source.js
+++ b/lib/runner/test-source.js
@@ -3,8 +3,8 @@ const fs = require('fs');
 const minimatch = require('minimatch');
 const Utils = require('../utils');
 const Concurrency = require('./concurrency');
-const {Logger, validExtensions, tsFileExt, jsFileExt, singleSourceFile} = Utils;
-const {MinimalJsonReporter} = require('../reporter/reporters/minimalJson');
+const {Logger, validExtensions, jsFileExt, singleSourceFile} = Utils;
+const {getTestSourceForRerunFailed} = require('./rerunUtil');
 
 class TestSource {
   static get GroupNameDelimiter() {
@@ -48,68 +48,19 @@ class TestSource {
     return testsource;
   }
 
-  getRerunFailedFile(minimal_report_file_path) {
-    const jsonFile = path.resolve(process.env.NIGHTWATCH_RERUN_REPORT_FILE || minimal_report_file_path || '');
-
-    if (!Utils.fileExistsSync(jsonFile)) {
-      const err = new Error('Unable to find the Json reporter file to rerun failed tests');
-
-      err.showTrace = false;
-      err.detailedErr = 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path';
-      err.help = [
-        `Try setting ${Logger.colors.cyan('minimal_report_file_path: "JSON-REPORTER-PATH"')} in nightwatch configuration`,
-        `Or, try running: ${Logger.colors.cyan('export NIGHTWATCH_RERUN_REPORT_FILE="JSON-REPORTER-PATH"')}`
-      ];
-
-      throw err;
-    }
-
-    return jsonFile;
-  }
-  
-  getTestSourceForRerunFailed() {
-    const {reporter_options: {minimal_report_file_path}} = this.settings;
-    const minimalJsonFile = this.getRerunFailedFile(minimal_report_file_path);
-  
-    try {
-      const {modules = {}} = require(minimalJsonFile);
-      const testsource = [];
-    
-      Object.keys(modules).forEach(moduleKey => {
-        if (modules[moduleKey] && modules[moduleKey].status === 'fail') {
-          testsource.push(modules[moduleKey].modulePath);
-        }
-      });
-
-      if (testsource.length === 0) {
-        const err = new Error('Rerun Failed Tests: No failed tests found to rerun.');
-        err.noFailedTestFound = true;
-        err.showTrace = false;
-        err.detailedErr = 'Run nightwatch with --help to display usage info.';
-
-        throw err;
-      }
-
-      return testsource;
-    } catch (err) {
-      if (err.noFailedTestFound) {
-        err.message = 'Rerun Failed Tests: Invalid Json reporter.';
-
-        err.showTrace = false;
-        err.detailedErr = 'Please set env variable NIGHTWATCH_RERUN_REPORT_FILE with valid Json reporter path.';
-      }
-
-      throw err;
-    }
-  }
-
   /**
    * Returns the path where the tests are located
    * @returns {*}
    */
   getSource() {
-    if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed']) && Concurrency.isMasterProcess()) {
-      return this.getTestSourceForRerunFailed(this.argv);
+    const {test_runner} = this.settings;
+
+    if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed']) && Concurrency.isMasterProcess() && test_runner?.type !== 'cucumber') {
+      return getTestSourceForRerunFailed(this.settings);
+    }
+
+    if (this.src_folders.length === 0 && test_runner?.src_folders && test_runner?.type === 'cucumber') {
+      this.src_folders = test_runner?.src_folders;
     }
     
     if (this.argv['test-worker'] || singleSourceFile(this.argv)) {
@@ -140,10 +91,10 @@ class TestSource {
         return groupTestsource;
       }
       // only when all groups fail to match will there be a run error
-      let testsource = groupTestsource.filter(Utils.dirExistsSync);
+      const testsource = groupTestsource.filter(Utils.dirExistsSync);
 
       if (!this.settings.silent) {
-        let ignoredSource = groupTestsource.filter(entry => testsource.indexOf(entry) === -1);
+        const ignoredSource = groupTestsource.filter(entry => testsource.indexOf(entry) === -1);
 
         if (ignoredSource.length) {
           Logger.warn('The following group paths were not found and will be excluded from the run:\n - ' +
@@ -168,11 +119,11 @@ class TestSource {
     }
 
     if (this.settings.exclude) {
-      let arrExclude = Array.isArray(this.settings.exclude) ? this.settings.exclude : [this.settings.exclude];
-      let resolvedExclude = arrExclude.map(item => path.resolve(item));
+      const arrExclude = Array.isArray(this.settings.exclude) ? this.settings.exclude : [this.settings.exclude];
+      const resolvedExclude = arrExclude.map(item => path.resolve(item));
 
       this.src_folders = this.src_folders.filter(item => {
-        let resolvedPath = path.resolve(item);
+        const resolvedPath = path.resolve(item);
         let match = true;
 
         resolvedExclude.forEach(function(pattern) {
@@ -191,13 +142,13 @@ class TestSource {
    * @return {Array}
    */
   findGroupPath(groupName) {
-    let fullGroupPath = path.resolve(groupName);
+    const fullGroupPath = path.resolve(groupName);
 
     // for each src folder, append the group to the path
     // to resolve the full test path
 
     return this.src_folders.map(function(srcFolder) {
-      let fullSrcFolder = path.resolve(srcFolder);
+      const fullSrcFolder = path.resolve(srcFolder);
       if (fullGroupPath.indexOf(fullSrcFolder) === 0) {
         return groupName;
       }
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 58ec2076..255440d0 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -14,6 +14,9 @@ const NightwatchClient = require('../core/client.js');
 const Concurrency = require('../runner/concurrency');
 const ElementGlobal = require('../api/_loaders/element-global.js');
 const {Logger, Screenshots, Snapshots, alwaysDisplayError, isString, isFunction, SafeJSON} = require('../utils');
+const {DEFAULT_RUNNER_EVENTS, NightwatchEventHub} = require('../runner/eventHub');
+const {GlobalHook, TestSuiteHook} = DEFAULT_RUNNER_EVENTS;
+
 
 class TestSuite {
   constructor({modulePath, modules, settings, argv, usingMocha = false, addtOpts = {}}) {
@@ -57,13 +60,13 @@ class TestSuite {
   }
 
   get skipTestcasesOnFail() {
-    let localDefinedValue = this.context.getSkipTestcasesOnFail();
+    const localDefinedValue = this.context.getSkipTestcasesOnFail();
 
     if (localDefinedValue !== undefined) {
       return localDefinedValue;
     }
 
-    let settingsValueUndefined = this.settings.skip_testcases_on_fail === undefined;
+    const settingsValueUndefined = this.settings.skip_testcases_on_fail === undefined;
     if (settingsValueUndefined && this.context.unitTestingMode) {
       // false by default when running unit tests
       return false;
@@ -74,7 +77,7 @@ class TestSuite {
   }
 
   get endSessionOnFail() {
-    let definedValue = this.context.getEndSessionOnFail();
+    const definedValue = this.context.getEndSessionOnFail();
 
     return definedValue === undefined ? this.settings.end_session_on_fail : definedValue;
   }
@@ -385,10 +388,10 @@ class TestSuite {
       return this;
     }
 
-    let capabilities = data.capabilities || {};
-    let browserName = (capabilities.browserName && capabilities.browserName.toUpperCase()) || '';
-    let browserVersion = capabilities.version || capabilities.browserVersion || '';
-    let platformVersion = capabilities.platform || capabilities.platformVersion || '';
+    const capabilities = data.capabilities || {};
+    const browserName = (capabilities.browserName && capabilities.browserName.toUpperCase()) || '';
+    const browserVersion = capabilities.version || capabilities.browserVersion || '';
+    const platformVersion = capabilities.platform || capabilities.platformVersion || '';
 
     if (!this.context.unitTestingMode) {
       this.settings.report_prefix = this.__reportPrefix = `${browserName}_${browserVersion}_${platformVersion}_`.replace(/ /g, '_');
@@ -437,7 +440,15 @@ class TestSuite {
 
   runHook(hookName) {
     Logger.log(`${Logger.colors.green('→')} Running [${hookName}]:`);
+
+    if (this.context.hasHook(hookName)) {
+      NightwatchEventHub.emit(TestSuiteHook[hookName].started, this.reporter.testResults.eventDataToEmit);
+    }
     
+    if (hookName === 'beforeEach' || hookName === 'afterEach') {
+      this.client.reporter.markHookRun(hookName);
+    }
+
     if (hookName === 'before' || hookName === 'after') {
       this.client.reporter.setCurrentSection({testName: `__${hookName}_hook`});
     }
@@ -445,6 +456,14 @@ class TestSuite {
     return this.handleRunnable(hookName, () => {
       return this.hooks[hookName].run(this.client);
     }).then(result => {
+      if (this.context.hasHook(hookName)) {
+        NightwatchEventHub.emit(TestSuiteHook[hookName].finished, this.reporter.testResults.eventDataToEmit);
+      }
+
+      if (hookName === 'beforeEach' || hookName === 'afterEach') {
+        this.client.reporter.unmarkHookRun(hookName);
+      }
+
       Logger.log(`${Logger.colors.green('→')} Completed [${hookName}].`);
 
       return result;
@@ -452,6 +471,12 @@ class TestSuite {
   }
 
   runGlobalHook(hookName) {
+    const {globals} = this.settings;
+
+    if (globals[hookName]) {
+      NightwatchEventHub.emit(GlobalHook[hookName].started, this.reporter.testResults.eventDataToEmit);
+    }
+
     if (!this.globalHooks[hookName]) {
       return Promise.resolve();
     }
@@ -464,6 +489,10 @@ class TestSuite {
       }
 
       const result = await this.globalHooks[hookName].run(this.client);
+
+      if (globals[hookName]) {
+        NightwatchEventHub.emit(GlobalHook[hookName].finished, this.reporter.testResults.eventDataToEmit);
+      }
       
       this.onTestSectionFinished();
 
@@ -489,6 +518,8 @@ class TestSuite {
   }
 
   async startTestSuite() {
+    NightwatchEventHub.emit(TestSuiteHook.started, this.reporter.testResults.eventDataToEmit);
+
     if (!this.client) {
       await this.createClient();
       this.setupHooks();
@@ -623,7 +654,7 @@ class TestSuite {
           return this.retrySuite();
         }
 
-        let failures = errorOrFailures || failedResult || !this.reporter.allTestsPassed;
+        const failures = errorOrFailures || failedResult || !this.reporter.allTestsPassed;
 
         return this.stopSession(failures);
       });
@@ -667,6 +698,8 @@ class TestSuite {
     } catch (err) {
       Logger.error(`Unable to stop the test suite gracefully because of: [${err.name}]: ${err.message}`);
     }
+    
+    NightwatchEventHub.emit(TestSuiteHook.finished, this.reporter.testResults.eventDataToEmit);
 
     return failures;
   }
@@ -770,12 +803,26 @@ class TestSuite {
     return this.createSession()
       .then(() => this.runHook('beforeEach'))
       .then(_ => {
+        this.client.reporter.markHookRun('testcase');
+
+        NightwatchEventHub.emit(TestSuiteHook.test.started, {
+          ...this.reporter.testResults.eventDataToEmit,
+          testcase: this.reporter.testResults.currentTestName
+        });
+
         return this.handleRunnable(this.testcase.testName, () => this.testcase.run(this.client));
       })
       .catch(err => {
         return err;
       })
       .then(possibleError => {
+        this.client.reporter.unmarkHookRun();
+
+        NightwatchEventHub.emit(TestSuiteHook.test.finished, {
+          ...this.reporter.testResults.eventDataToEmit,
+          testcase: this.reporter.testResults.currentTestName
+        });
+
         if (this.transport.driverService && this.reporter.testResults) {
           this.reporter.testResults.setSeleniumLogFile(this.transport.driverService.getSeleniumOutputFilePath());
         }
diff --git a/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testExpect.js b/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testExpect.js
new file mode 100644
index 00000000..fb51a98f
--- /dev/null
+++ b/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testExpect.js
@@ -0,0 +1,8 @@
+const {Then} = require('@cucumber/cucumber');
+
+
+Then('I check if webdriver is present and contains text', async function() {
+  browser.globals.test_calls++;
+
+  await browser.expect.element('#webdriver').text.to.contain('xx');
+});
diff --git a/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testSample.js b/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testSample.js
new file mode 100644
index 00000000..c1c33291
--- /dev/null
+++ b/test/cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testSample.js
@@ -0,0 +1,12 @@
+const {Given, Then} = require('@cucumber/cucumber');
+
+Given('I navigate to localhost', function() {
+
+  return browser.navigateTo('http://localhost');
+});
+
+Then('I check if webdriver is present', function() {
+
+  return browser.assert.elementPresent('#webdriver');
+});
+
diff --git a/test/cucumber-integration-tests/testCucumberTestsInParallel.js b/test/cucumber-integration-tests/testCucumberTestsInParallel.js
index 1461ed9e..ca6a7ae3 100644
--- a/test/cucumber-integration-tests/testCucumberTestsInParallel.js
+++ b/test/cucumber-integration-tests/testCucumberTestsInParallel.js
@@ -1,6 +1,7 @@
 const path = require('path');
 const assert = require('assert');
 const Globals = require('../lib/globals/commands.js');
+const commandMocks = require('../lib/command-mocks.js');
 const common = require('../common.js');
 const MockServer = require('../lib/mockserver.js');
 
@@ -39,4 +40,35 @@ describe('Cucumber integration - parallel running single formatter', function()
         assert.strictEqual(errorOrFailed, true);
       });
   });
+
+  it('testCucumberSampleTests in parallel with multiple step definition files', function() {
+
+    commandMocks.elementText('5cc459b8-36a8-3042-8b4a-258883ea642b', 'xx');
+
+    const source = [
+      path.join(__dirname, './sample_cucumber_tests/parallelWithMultipleDefinition')
+    ];
+
+    const {runTests} = common.require('index.js');
+
+    return runTests({
+      source,
+      parallel: true,
+      verbose: true,
+      timeout: 10,
+      silent: false,
+      tags: ['not @fail'],
+      format: 'progress',
+      config: path.join(__dirname, '../extra/cucumber-config-parallel.js'),
+      'format-options': '',
+      ['retry-interval']: 5,
+      ['persist-globals']: true,
+      ['webdriver-host']: 'localhost',
+      ['start-process']: false,
+      ['webdriver-port']: 10190
+    }, {})
+      .then(errorOrFailed => {
+        assert.strictEqual(errorOrFailed, false);
+      });
+  });
 });
\ No newline at end of file
diff --git a/test/cucumber-integration-tests/testCucumberTestsWithExpect.js b/test/cucumber-integration-tests/testCucumberTestsWithExpect.js
index b560019a..be7c247e 100644
--- a/test/cucumber-integration-tests/testCucumberTestsWithExpect.js
+++ b/test/cucumber-integration-tests/testCucumberTestsWithExpect.js
@@ -46,4 +46,32 @@ describe('Cucumber integration with .expect APIs', function() {
     });
   });
 
-});
\ No newline at end of file
+  it('testCucumberSampleTests -- nightwatch formatter live events', function() {
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/element/5cc459b8-36a8-3042-8b4a-258883ea642b/text',
+      statusCode: 200,
+      method: 'GET',
+      response: {
+        value: 'jean sibelius'
+      },
+      times: 3
+    });
+
+    const source = [path.join(__dirname, './sample_cucumber_tests/withexpect/testSample.js')];
+
+    return runTests({
+      source,
+      verbose: false,
+      config: path.join(__dirname, '../extra/cucumber-config-events.js')
+    }, settings({
+      silent: false,
+      output: false,
+      globals: {
+        waitForConditionTimeout: 10,
+        waitForConditionPollInterval: 50
+      }
+    })).then(failures => {
+      assert.strictEqual(failures, true, 'Cucumber should have test failures');
+    });
+  });
+});
diff --git a/test/extra/cucumber-config-events.js b/test/extra/cucumber-config-events.js
new file mode 100644
index 00000000..7586377e
--- /dev/null
+++ b/test/extra/cucumber-config-events.js
@@ -0,0 +1,57 @@
+const assert = require('assert');
+const path = require('path');
+
+module.exports = {
+  selenium: {
+    port: 10192,
+    host: 'localhost'
+  },
+
+  persist_globals: true,
+  output_folder: false,
+
+  webdriver: {
+    start_process: false
+  },
+
+  globals: {
+    test_calls: 0,
+    waitForConditionTimeout: 20,
+    retryAssertionTimeout: 20,
+    waitForConditionPollInterval: 10,
+    registerEventHandlers: function(eventBroadcaster) {
+      eventBroadcaster.on('TestCaseStarted', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+
+      eventBroadcaster.on('TestCaseFinished', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+
+      eventBroadcaster.on('TestStepStarted', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+
+      eventBroadcaster.on('TestStepFinished', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+
+      eventBroadcaster.on('TestStarted', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+
+      eventBroadcaster.on('TestFinished', (args) => {
+        assert.deepStrictEqual(Object.keys(args), ['envelope', 'report']);
+      });
+    }
+  },
+
+  test_runner: {
+    type: 'cucumber',
+    options: {
+      feature_path: path.join(__dirname, '../cucumber-integration-tests/sample_cucumber_tests/withexpect/sample.feature')
+    }
+  },
+  output: false,
+  silent: false
+};
diff --git a/test/src/cli/testCliRunner.js b/test/src/cli/testCliRunner.js
index 197b0ed0..f64a70c8 100644
--- a/test/src/cli/testCliRunner.js
+++ b/test/src/cli/testCliRunner.js
@@ -290,6 +290,9 @@ describe('Test CLI Runner', function() {
 
   function registerNoSettingsJsonMock(){
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './settings.json') {
           throw new Error('Does not exist');
@@ -389,6 +392,9 @@ describe('Test CLI Runner', function() {
 
   it('testSetOutputFolder', function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './settings.json' || module === './nightwatch.conf.js') {
           throw new Error('Does not exist');
@@ -415,6 +421,9 @@ describe('Test CLI Runner', function() {
 
   it('testSetOutputFolder using reporterOptions', function(done) {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './settings.json' || module === './nightwatch.conf.js') {
           throw new Error('Does not exist');
@@ -444,6 +453,9 @@ describe('Test CLI Runner', function() {
 
   it('testReadSettingsDeprecated', function(done) {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './settings.json') {
           return {
@@ -483,6 +495,9 @@ describe('Test CLI Runner', function() {
 
   it('testCustomSettingsFileAndEnvironment', function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './custom.json') {
           return {
@@ -779,6 +794,9 @@ describe('Test CLI Runner', function() {
 
   it('testGetTestSourceMultipleGroups', function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         switch (module) {
           case './custom.json':
@@ -838,6 +856,9 @@ describe('Test CLI Runner', function() {
 
   it('testParseTestSettingsNull', function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './null.json') {
           return {
@@ -866,6 +887,9 @@ describe('Test CLI Runner', function() {
 
   it('testParseTestSettingsIncorrect', async function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './incorrect.json') {
           return {
@@ -896,6 +920,9 @@ describe('Test CLI Runner', function() {
 
   it('testReadExternalGlobals', function() {
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function(module) {
         if (module === './custom.json' || module === './globals.json') {
           return {
@@ -943,7 +970,9 @@ describe('Test CLI Runner', function() {
 
   it('testReadExternalGlobalsError', function() {
     mockery.registerMock('fs', {
-      statSync: function(module) {
+      existsSync() {
+        return false;
+      }, statSync: function(module) {
         if (module === './custom.json') {
           return {
             isFile: function() {
@@ -1008,6 +1037,9 @@ describe('Test CLI Runner', function() {
       }
     });
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function (module) {
         if (module === './nightwatch.conf.ts') {
           return {
diff --git a/test/src/cli/testCliRunnerGenerate.js b/test/src/cli/testCliRunnerGenerate.js
index b2176c80..343fa14d 100644
--- a/test/src/cli/testCliRunnerGenerate.js
+++ b/test/src/cli/testCliRunnerGenerate.js
@@ -41,6 +41,9 @@ describe('Test CLI Runner Generate', function() {
     });
 
     mockery.registerMock('fs', {
+      existsSync() {
+        return false;
+      },
       statSync: function (fileName) {
         if (fileName.endsWith('/nightwatch.conf.js')) {
           return {
diff --git a/test/src/runner/cucumber-integration/testCucumberSampleTests.js b/test/src/runner/cucumber-integration/testCucumberSampleTests.js
index 08574c15..ade6c3db 100644
--- a/test/src/runner/cucumber-integration/testCucumberSampleTests.js
+++ b/test/src/runner/cucumber-integration/testCucumberSampleTests.js
@@ -16,7 +16,7 @@ describe('Cucumber integration', function() {
   });
 
   it('testCucumberSampleTests', function() {
-    const source = [path.join(__dirname, '../../../cucumber-integration-tests/sample_cucumber_tests/integration/testSample.js')];
+    const source = [path.join(__dirname, '../../../cucumber-integration-tests/sample_cucumber_tests/parallelWithMultipleDefinition/testExpect.js'), path.join(__dirname, '../../../cucumber-integration-tests/sample_cucumber_tests/integration/testSample.js')];
 
     return runTests({
       source,
diff --git a/test/src/runner/testEventReporter.js b/test/src/runner/testEventReporter.js
new file mode 100644
index 00000000..271de4e1
--- /dev/null
+++ b/test/src/runner/testEventReporter.js
@@ -0,0 +1,142 @@
+const assert = require('assert');
+const path = require('path');
+const mockery = require('mockery');
+const rimraf = require('rimraf');
+
+const common = require('../../common.js');
+const {settings} = common;
+const {runTests} = common.require('index.js');
+const {mkpath} = common.require('utils');
+
+const MockServer = require('../../lib/mockserver.js');
+
+describe('testNightwatchEventReporter', function() {
+
+  before(function(done) {
+    this.server = MockServer.init();
+    this.server.on('listening', () => done());
+  });
+
+  beforeEach(function(done) {
+    mkpath('output', function(err) {
+      if (err) {
+        return done(err);
+      }
+      mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+      done();
+    });
+  });
+
+
+  afterEach(function(done) {
+    mockery.deregisterAll();
+    mockery.resetCache();
+    mockery.disable();
+    rimraf('output', done);
+  });
+
+
+  after(function(done) {
+    this.server.close(function() {
+      done();
+    });
+  });
+
+  it('Check reporter output on real time event', async function () {
+    let possibleError = null;
+    const testsPath = [path.join(__dirname, '../../sampletests/before-after/sampleWithBeforeAndAfterNoCallback.js')];
+
+    const eventExecuted = {
+      TestSuiteStarted: false,
+      TestSuiteFinished: false,
+      BeforeStarted: false,
+      BeforeFinished: false,
+      TestRunStarted: false,
+      TestRunFinished: false,
+      AfterStarted: false,
+      AfterFinished: false,
+      LogCreated: false,
+      AfterEachFinished: false,
+      AfterEachStarted: false
+    };
+
+    try {
+      await runTests({
+        source: testsPath
+      },
+      settings({
+        output_folder: 'output',
+        globals: {
+          waitForConditionPollInterval: 20,
+          waitForConditionTimeout: 50,
+          retryAssertionTimeout: 50,
+          registerEventHandlers: function(eventBroadcaster) {
+            eventBroadcaster.on('TestSuiteStarted', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              eventExecuted.TestSuiteStarted = true;
+            });
+            
+            eventBroadcaster.on('TestSuiteFinished', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              eventExecuted.TestSuiteFinished = true;
+            });
+            
+            eventBroadcaster.on('BeforeStarted', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              eventExecuted.BeforeStarted = true;
+            });
+            
+            eventBroadcaster.on('BeforeFinished', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              assert.ok(Object.prototype.hasOwnProperty.call(args['envelope'], '__before_hook'));
+              eventExecuted.BeforeFinished = true;
+            });
+            
+            eventBroadcaster.on('TestRunStarted', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata', 'testcase']);
+              eventExecuted.TestRunStarted = true;
+            });
+            
+            eventBroadcaster.on('TestRunFinished', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata', 'testcase']);
+              eventExecuted.TestRunFinished = true;
+            });
+            
+            eventBroadcaster.on('AfterStarted', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              eventExecuted.AfterStarted = true;
+            });
+            
+            eventBroadcaster.on('AfterFinished', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['envelope', 'metadata']);
+              eventExecuted.AfterFinished = true;
+            });
+            
+            eventBroadcaster.on('LogCreated', (args) => {
+              assert.deepStrictEqual(Object.keys(args), ['httpOutput']);
+              eventExecuted.LogCreated = true;
+            });            
+          }
+        },
+        silent: true,
+        output: false
+      }));
+
+      assert.strictEqual(eventExecuted.AfterFinished, true);
+      assert.strictEqual(eventExecuted.AfterStarted, true);
+      assert.strictEqual(eventExecuted.BeforeFinished, true);
+      assert.strictEqual(eventExecuted.BeforeStarted, true);
+      assert.strictEqual(eventExecuted.LogCreated, true);
+      assert.strictEqual(eventExecuted.TestRunFinished, true);
+      assert.strictEqual(eventExecuted.TestRunStarted, true);
+      assert.strictEqual(eventExecuted.AfterEachFinished, false);
+      assert.strictEqual(eventExecuted.AfterEachStarted, false);
+      assert.strictEqual(eventExecuted.TestSuiteFinished, true);
+      assert.strictEqual(eventExecuted.TestSuiteStarted, true);
+    } catch (error) {
+      possibleError = error;
+    }
+    
+    assert.strictEqual(possibleError, null);
+  });
+});
diff --git a/test/src/runner/testRunnerScreenshotsOutput.js b/test/src/runner/testRunnerScreenshotsOutput.js
index bd1120a9..93ca4f34 100644
--- a/test/src/runner/testRunnerScreenshotsOutput.js
+++ b/test/src/runner/testRunnerScreenshotsOutput.js
@@ -37,7 +37,7 @@ describe('testRunnerScreenshotsOutput', function () {
   });
 
   it('takes screenshot on each test failure', function () {
-    let testsPath = [
+    const testsPath = [
       path.join(__dirname, '../../sampletests/withfailures')
     ];
 
@@ -79,7 +79,7 @@ describe('testRunnerScreenshotsOutput', function () {
 
   it('takes screenshot for failed test and exits if skip_testcases_on_fail is set to true', function () {
 
-    let testsPath = [
+    const testsPath = [
       path.join(__dirname, '../../sampletests/withfailures')
     ];
 
@@ -124,7 +124,7 @@ describe('testRunnerScreenshotsOutput', function () {
 
   it('doesnt save file if screenshot call is failed', function () {
 
-    let testsPath = [
+    const testsPath = [
       path.join(__dirname, '../../sampletests/withfailures')
     ];
 
@@ -171,7 +171,7 @@ describe('testRunnerScreenshotsOutput', function () {
 
   it('does not take screenshot if screenshot is disabled', function () {
 
-    let testsPath = [
+    const testsPath = [
       path.join(__dirname, '../../sampletests/withfailures')
     ];
 
@@ -203,7 +203,7 @@ describe('testRunnerScreenshotsOutput', function () {
 
   it('does not take screenshot if screenshot is enabled but on_failure is set to false', function () {
 
-    let testsPath = [
+    const testsPath = [
       path.join(__dirname, '../../sampletests/withfailures')
     ];
 
@@ -233,7 +233,56 @@ describe('testRunnerScreenshotsOutput', function () {
       });
   });
 
+  it('emit screenshot event on test failure', function () {
+    const testsPath = [
+      path.join(__dirname, '../../sampletests/withfailures')
+    ];
+
+    let screenshotEventExecuted = false;
+
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/screenshot',
+      method: 'GET',
+      response: JSON.stringify({
+        sessionId: '1352110219202',
+        status: 0,
+        value: 'c2NyZWVuZGF0YQ=='
+      })
+    }, true);
+
+    return runTests(testsPath, settings({
+      skip_testcases_on_fail: false,
+      globals: {
+        waitForConditionPollInterval: 5,
+        waitForConditionTimeout: 5,
+        retryAssertionTimeout: 1,
+        reporter: function () {
+        },
+        registerEventHandlers: function(eventBroadcaster) {
+          eventBroadcaster.on('ScreenshotCreated', (args) => {
+            assert.deepStrictEqual(Object.keys(args), ['path']);
+            screenshotEventExecuted = true;
+          });
+        }
+      },
+      screenshots: {
+        enabled: true,
+        on_failure: true,
+        on_error: true,
+        path: screenshotFilePath
+      }
+    }))
+      .then(_ => {
+        assert.strictEqual(screenshotEventExecuted, true);
 
+        return readDirPromise(`${screenshotFilePath}/${moduleName}`);
+      })
+      .then(files => {
+        assert.ok(Array.isArray(files));
+        assert.strictEqual(files.length, 1);
+        files.forEach((val) => assert.ok(val.endsWith('.png')));
+      });
+  });
 });
 
 function readDirPromise(dirName) {
diff --git a/test/src/service-builders/testSafariDriver.js b/test/src/service-builders/testSafariDriver.js
index 2589fab1..ef0c36bb 100644
--- a/test/src/service-builders/testSafariDriver.js
+++ b/test/src/service-builders/testSafariDriver.js
@@ -77,9 +77,9 @@ describe('SafariDriver Transport Tests', function () {
 
           },
           async start() {
-            return 'http://localhost'
+            return 'http://localhost';
           }
-        }
+        };
       }
     }
 
@@ -93,12 +93,12 @@ describe('SafariDriver Transport Tests', function () {
     mockExecutor({
       sessionId: '111',
       getId() {
-        return '1111'
+        return '1111';
       },
       getCapabilities() {
         return {
           getPlatform() {
-            return 'MAC'
+            return 'MAC';
           },
           getBrowserName() {
             return 'safari';
@@ -112,7 +112,7 @@ describe('SafariDriver Transport Tests', function () {
           keys() {
             return new Map();
           }
-        }
+        };
       }
     });
 
@@ -181,7 +181,7 @@ describe('SafariDriver Transport Tests', function () {
       session,
       serverPath,
       serverPort
-    }
+    };
   }
 
   it('test create session with safari driver', async function() {