Repository: nightwatchjs/nightwatch
Pull request #3056 (2022-02-18 08:58:06)

Add prefer-const to eslint rule set 


Patch:
diff --git a/.eslintrc b/.eslintrc
index 6c6ea50b..343eb6e3 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -100,6 +100,9 @@
       {
         "SwitchCase": 1
       }
+    ],
+    "prefer-const": [
+      "warn"
     ]
   },
   "globals": {
diff --git a/examples/pages/google/search.js b/examples/pages/google/search.js
index 9d92bab5..778244c7 100644
--- a/examples/pages/google/search.js
+++ b/examples/pages/google/search.js
@@ -9,6 +9,8 @@ const searchCommands = {
   }
 };
 
+const consentModal = '[aria-modal="true"]';
+
 module.exports = {
   url: 'https://google.no',
   commands: [
@@ -17,7 +19,7 @@ module.exports = {
 
   sections: {
     consentModal: {
-      selector: '[aria-modal="true"][title="Before you continue to Google Search"]',
+      selector: consentModal,
       elements: {
         customizeButton: 'div.VDity button:nth-child(1)'
       }
@@ -25,6 +27,8 @@ module.exports = {
   },
 
   elements: {
+    consentModal,
+
     searchBar: {
       selector: 'input[name=q]'
     },
diff --git a/lib/api/_loaders/_command-loader.js b/lib/api/_loaders/_command-loader.js
index 61c47579..4d3d99ef 100644
--- a/lib/api/_loaders/_command-loader.js
+++ b/lib/api/_loaders/_command-loader.js
@@ -45,7 +45,8 @@ class BaseCommandLoader extends BaseLoader {
 
     if (this.nightwatchInstance.isApiMethodDefined(this.commandName, namespace)) {
       const err = new TypeError(`Error while loading the API commands: the ${this.type} ${this.namespace || ''}.${this.commandName}() is already defined.`);
-      err.displayed = true;
+      err.displayed = false;
+      err.detailedErr = `Source: ${this.fileName}`;
       err.showTrace = false;
 
       throw err;
diff --git a/lib/api/_loaders/assertion-scheduler.js b/lib/api/_loaders/assertion-scheduler.js
index ccc21943..fccd331f 100644
--- a/lib/api/_loaders/assertion-scheduler.js
+++ b/lib/api/_loaders/assertion-scheduler.js
@@ -85,7 +85,7 @@ class AssertionScheduler {
       const elapsedTime = new Date().getTime() - startTime;
 
       if (commandInstance) {
-        this.__opts.abortOnFailure = commandInstance.abortOnFailure;
+        //this.__opts.abortOnFailure = commandInstance.abortOnFailure;
       }
 
       let passed;
diff --git a/lib/api/assertions/elementsCount.js b/lib/api/assertions/elementsCount.js
index 189ef4b6..10efb5e6 100644
--- a/lib/api/assertions/elementsCount.js
+++ b/lib/api/assertions/elementsCount.js
@@ -48,17 +48,10 @@ exports.assertion = function (definition, count, msg) {
 
   };
 
-  this.createElement = function(selector, using = this.client.locateStrategy) {
-    this.element = Element.createFromSelector(selector, using);
 
-    return this;
-  };
 
   this.command = async function(callback) {
-    this.createElement(definition);
-
-    const {element} = this;
-
-    await this.api.findElements(element, callback);
+  
+    await this.api.findElements(definition, callback);
   };
 };
diff --git a/lib/api/expect/assertions/_baseAssertion.js b/lib/api/expect/assertions/_baseAssertion.js
index be1ebc0e..756b1dd7 100644
--- a/lib/api/expect/assertions/_baseAssertion.js
+++ b/lib/api/expect/assertions/_baseAssertion.js
@@ -319,7 +319,7 @@ class BaseAssertion {
         if (isError) {
           result.isExpect = true;
 
-          if (this.resultErrorStatus) {
+          if (this.resultErrorStatus && this.reporter) {
             this.reporter.registerTestError(this.resultErrorStatus);
           }
 
diff --git a/lib/core/client.js b/lib/core/client.js
index ff74345d..2732a510 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -126,7 +126,13 @@ class NightwatchClient extends EventEmitter {
     this.createCommandQueue();
 
     this.__elementLocator = new Locator(this);
-    this.__reporter = null;
+    this.__reporter = {
+      logFailedAssertion(err) {
+      },
+      registerTestError(err) {
+        Logger.error(err);
+      }
+    };
 
     this.__api = new NightwatchAPI(this.sessionId, this.settings);
 
@@ -615,6 +621,7 @@ class NightwatchClient extends EventEmitter {
     this.__commandQueue = new CommandQueue({
       compatMode: this.settings.backwards_compatibility_mode,
       foreignRunner: runner !== 'default',
+      mochaRunner: runner === 'mocha',
       cucumberRunner: runner === 'cucumber'
     });
   }
diff --git a/lib/http/request.js b/lib/http/request.js
index a36e92b9..f3321b6c 100644
--- a/lib/http/request.js
+++ b/lib/http/request.js
@@ -278,7 +278,7 @@ class HttpRequest extends EventEmitter {
       setTimeout(() => {
         this.send();
         this.retryAttempts = this.retryAttempts - 1;
-      }, 15);
+      }, 100);
 
       return;
     }
diff --git a/lib/page-object/command-wrapper.js b/lib/page-object/command-wrapper.js
index e4a91891..a85a6927 100644
--- a/lib/page-object/command-wrapper.js
+++ b/lib/page-object/command-wrapper.js
@@ -303,7 +303,7 @@ class CommandLoader {
   static addCommand({target, commandFn, commandName, parent, isChaiAssertion, overwrite = false}) {
     if (target[commandName] && !overwrite) {
       const err = new TypeError(`Error while loading the page object commands: the command "${commandName}" is already defined.`);
-      err.displayed = true;
+      err.displayed = false;
       err.showTrace = false;
 
       throw err;
diff --git a/lib/page-object/index.js b/lib/page-object/index.js
index 8090d8d2..008d7fc6 100644
--- a/lib/page-object/index.js
+++ b/lib/page-object/index.js
@@ -108,27 +108,46 @@ class Page extends BaseObject {
    * @returns {*}
    */
   get navigate() {
-    return (url, callback) => {
-      let goToUrl = this.getUrl(url || this.url);
+    return function(url = this.url, callback) {
+      const promise = new Promise((resolve, reject) => {
+        (async () => {
+          if (typeof url == 'function' && !callback) {
+            callback = url;
+            url = this.url;
+          }
 
-      if (Utils.relativeUrl(goToUrl)) {
-        if (!this.client.settings.launchUrl) {
-          throw new Error(`Invalid URL ${goToUrl} in "${this.name}" page object. When using relative uris, you must ` +
-            'define a "baseUrl" or "launchUrl" in your nightwatch config.');
-        }
+          let goToUrl = this.getUrl(url);
 
-        goToUrl = Utils.uriJoin(this.client.settings.launchUrl, goToUrl);
-      }
+          if (Utils.isString(goToUrl) && Utils.relativeUrl(goToUrl)) {
+            if (!this.client.settings.launchUrl) {
+              const err = new Error(`Invalid URL ${goToUrl} in "${this.name}" page object. When using relative uris, you must ` +
+                'define a "baseUrl" or "launchUrl" in your nightwatch config.');
 
-      if (goToUrl === null) {
-        throw new Error('Invalid URL: You must either add a url property to "' +
-          this.name + '" or provide a url as an argument');
-      }
+              return reject(err);
+            }
+
+            goToUrl = Utils.uriJoin(this.client.settings.launchUrl, goToUrl);
+          }
+
+          if (goToUrl === null) {
+            const err = new Error('Invalid URL: You must either add a url property to "' +
+              this.name + '" or provide a url as an argument');
+
+            return reject(err);
+          }
 
-      this.api.url(goToUrl, callback);
+          await this.api.url(goToUrl, callback);
 
-      return this;
+          resolve();
+        })();
+      });
+
+      Object.assign(promise, this);
+
+      return promise;
     };
+
+
   }
 }
 
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index 7e611463..971e90ff 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -51,7 +51,7 @@ class CliRunner {
     });
 
     let rendered = ejs.render(tplData, {
-      plugins: (availablePlugins.length > 0) ? JSON.stringify(availablePlugins) : false,
+      plugins: (availablePlugins.length > 0) ? JSON.stringify(availablePlugins) : '[]',
       launch_url,
       isMacOS: os.platform() === 'darwin'
     });
@@ -183,7 +183,14 @@ class CliRunner {
   }
 
   getLocalConfigFileName() {
-    const packageInfo = require(path.resolve('package.json'));
+    let packageInfo;
+    try {
+      packageInfo = require(path.resolve('package.json'));
+    } catch (err) {
+      packageInfo = null;
+    }
+
+    packageInfo = packageInfo || {};
     const usingESM = packageInfo.type === 'module';
 
     return path.resolve(usingESM ? CliRunner.CONFIG_FILE_CJS : CliRunner.CONFIG_FILE_JS);
diff --git a/lib/runner/cli/nightwatch.conf.ejs b/lib/runner/cli/nightwatch.conf.ejs
index e32790fe..cb7853ce 100644
--- a/lib/runner/cli/nightwatch.conf.ejs
+++ b/lib/runner/cli/nightwatch.conf.ejs
@@ -1,7 +1,7 @@
-// Autogenerated by Nightwatch
-// Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/
-const Services = {}; loadServices();
-
+//
+// Refer to the online docs for more details:
+// https://nightwatchjs.org/gettingstarted/configuration/
+//
 //  _   _  _         _      _                     _          _
 // | \ | |(_)       | |    | |                   | |        | |
 // |  \| | _   __ _ | |__  | |_ __      __  __ _ | |_   ___ | |__
@@ -10,23 +10,24 @@ const Services = {}; loadServices();
 // \_| \_/|_| \__, ||_| |_| \__|  \_/\_/   \__,_| \__| \___||_| |_|
 //             __/ |
 //            |___/
+//
 
 module.exports = {
   // An array of folders (excluding subfolders) where your tests are located;
   // if this is not specified, the test source must be passed as the second argument to the test runner.
   src_folders: [],
 
-  // See https://nightwatchjs.org/guide/working-with-page-objects/
+  // See https://nightwatchjs.org/guide/working-with-page-objects/using-page-objects.html
   page_objects_path: ['node_modules/nightwatch/examples/pages/'],
 
-  // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands
+  // See https://nightwatchjs.org/guide/extending-nightwatch/custom-commands.html
   custom_commands_path: ['node_modules/nightwatch/examples/custom-commands/'],
 
-  // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions
+  // See https://nightwatchjs.org/guide/extending-nightwatch/custom-assertions.html
   custom_assertions_path: '',
 
+  // See https://nightwatchjs.org/guide/extending-nightwatch/plugin-api.html
   <% if (plugins) { %>plugins: <%- plugins %>,
-
   <% } %>
   // See https://nightwatchjs.org/guide/#external-globals
   globals_path : '',
@@ -257,14 +258,20 @@ module.exports = {
     //////////////////////////////////////////////////////////////////////////////////
     selenium_server: {
       // Selenium Server is running locally and is managed by Nightwatch
+      // Install the NPM package @nightwatch/selenium-server or download the selenium server jar file from https://github.com/SeleniumHQ/selenium/releases/, e.g.: selenium-server-4.1.1.jar
       selenium: {
         start_process: true,
         port: 4444,
-        server_path: (Services.seleniumServer ? Services.seleniumServer.path : ''),
+        server_path: '', // Leave empty if @nightwatch/selenium-server is installed
+        command: 'standalone', // Selenium 4 only
         cli_args: {
-          'webdriver.gecko.driver': (Services.geckodriver ? Services.geckodriver.path : ''),
-          'webdriver.chrome.driver': (Services.chromedriver ? Services.chromedriver.path : '')
+          //'webdriver.gecko.driver': '',
+          //'webdriver.chrome.driver': ''
         }
+      },
+      webdriver: {
+        start_process: false,
+        default_path_prefix: '/wd/hub'
       }
     },
 
@@ -292,17 +299,3 @@ module.exports = {
     }
   }
 };
-
-function loadServices() {
-  try {
-    Services.seleniumServer = require('selenium-server');
-  } catch (err) {}
-
-  try {
-    Services.chromedriver = require('chromedriver');
-  } catch (err) {}
-
-  try {
-    Services.geckodriver = require('geckodriver');
-  } catch (err) {}
-}
diff --git a/lib/runner/concurrency/child-process.js b/lib/runner/concurrency/child-process.js
index 1ab059e9..3abcc8bc 100644
--- a/lib/runner/concurrency/child-process.js
+++ b/lib/runner/concurrency/child-process.js
@@ -1,6 +1,6 @@
 const child_process = require('child_process');
 const EventEmitter = require('events');
-const {Logger, isObject} = require('../../utils');
+const {Logger, isObject, symbols} = require('../../utils');
 const ProcessListener = require('../../runner/process-listener.js');
 
 let prevIndex = 0;
@@ -41,6 +41,13 @@ class ChildProcess extends EventEmitter {
     return this;
   }
 
+  printLog(msg) {
+    if (this.settings.output) {
+      // eslint-disable-next-line no-console
+      console.info(msg);
+    }
+  }
+
   getChildProcessEnvKey(env) {
     return `${env}_${this.index + 1}`;
   }
@@ -88,7 +95,8 @@ class ChildProcess extends EventEmitter {
       if (this.settings.disable_colors) {
         childProcessLabel = ' ' + this.environment + ' ';
       } else {
-        childProcessLabel = Logger.colors[color_pair[1]](` ${this.environment} `, Logger.colors.background[color_pair[0]]);
+        childProcessLabel = ' │ ';
+        this.env_label = Logger.colors[color_pair[1]](` ${this.environment} `, Logger.colors.background[color_pair[0]]);
       }
 
       let lines = data.split('\n').map(line => {
@@ -132,7 +140,9 @@ class ChildProcess extends EventEmitter {
           stdio: [null, null, null, 'ipc']
         });
 
-        Logger.info('Started child process for:' + this.env_label);
+        let color_pair = this.availColors[this.index%4];
+
+        this.printLog('Started child process for: ' + Logger.colors[color_pair[1]](` ${this.env_itemKey} `, Logger.colors.background[color_pair[0]]));
 
         this.child.stdout.on('data', data => {
           this.writeToStdout(data);
@@ -149,6 +159,19 @@ class ChildProcess extends EventEmitter {
         this.processListener = new ProcessListener(this.child);
 
         this.child.on('exit', code => {
+          this.printLog('');
+
+          let status = code > 0 ? symbols.fail : symbols.ok;
+          status = Logger.colors[color_pair[1]](` ${status} `, Logger.colors.background[color_pair[0]]);
+
+          let header = ` ┌────────────── ${status}${this.env_label} `;
+          let footer = ' └──' + new Array(70).join('─');
+
+          header += new Array(Math.max(0, footer.length - header.length + 30)).join('─');
+
+          this.printLog(header);
+          this.printLog(this.env_output.join('\n'));
+          this.printLog(footer);
           code = code || this.processListener.exitCode;
           resolve(code);
         });
diff --git a/lib/runner/concurrency/index.js b/lib/runner/concurrency/index.js
index 7c05b3ed..05070422 100644
--- a/lib/runner/concurrency/index.js
+++ b/lib/runner/concurrency/index.js
@@ -86,7 +86,12 @@ class Concurrency extends EventEmitter {
   runMultipleTestWorkers(modules, args, availColors) {
     let maxWorkerCount = this.getTestWorkersCount();
     let remaining = modules.length;
-    Logger.log(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`);
+
+    if (this.settings.output) {
+      // eslint-disable-next-line no-console
+      console.log(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`);
+    }
+
 
     return new Promise((resolve, reject) => {
       Concurrency.buildProcessQueue(maxWorkerCount, modules, (modulePath, index, next) => {
diff --git a/lib/settings/settings.js b/lib/settings/settings.js
index 4168cc2f..1cfc10c0 100644
--- a/lib/settings/settings.js
+++ b/lib/settings/settings.js
@@ -6,7 +6,7 @@ const lodashMerge = require('lodash.merge');
 const CI_Info = require('ci-info');
 const Defaults = require('./defaults.js');
 const Utils = require('../utils');
-const {isObject, isUndefined, isDefined} = Utils;
+const {isObject, isUndefined, isDefined, isNumber} = Utils;
 
 class Settings {
 
@@ -296,6 +296,14 @@ class Settings {
 
     if (this.argv.parallel === true && !this.settings.test_workers) {
       this.settings.test_workers = true;
+    } else if (isNumber(this.argv.parallel)) {
+      if (!isObject(this.settings.test_workers)) {
+        this.settings.test_workers = {
+          enabled: true
+        };
+      }
+
+      this.settings.test_workers.workers = this.argv.parallel;
     }
 
     this.settings.testWorkersEnabled = this.testWorkersEnabled;
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index 49af2da3..d418c125 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -82,6 +82,10 @@ class Transport extends BaseTransport {
     return this.settings.output;
   }
 
+  get usingSeleniumServer() {
+    return this.settings.selenium && this.settings.selenium.start_process;
+  }
+
   get shouldStartDriverService() {
     return this.settings.webdriver.start_process;
   }
@@ -104,7 +108,6 @@ class Transport extends BaseTransport {
     this.nightwatchInstance = nightwatchInstance;
     this.browserName = browserName;
 
-
     this.seleniumCapabilities = new SeleniumCapabilities({
       settings: this.settings,
       browserName
@@ -209,7 +212,14 @@ class Transport extends BaseTransport {
     const options = this.createSessionOptions(argv);
 
     if (start_process) {
-      this.showConnectSpinner(`Starting ${this.serviceName} on ${portStr}...\n`);
+      if (this.usingSeleniumServer) {
+        options.showSpinner = (msg) => {
+          this.showConnectSpinner(msg);
+        };
+      } else {
+        this.showConnectSpinner(`Starting ${this.serviceName} on ${portStr}...\n`);
+      }
+
       await this.createDriverService({options, moduleKey});
     } else {
       this.showConnectSpinner(`Connecting to ${host} on ${portStr}...\n`);
diff --git a/lib/transport/selenium-webdriver/options.js b/lib/transport/selenium-webdriver/options.js
index af4863dd..86330e02 100644
--- a/lib/transport/selenium-webdriver/options.js
+++ b/lib/transport/selenium-webdriver/options.js
@@ -202,16 +202,57 @@ module.exports = class SeleniumCapabilities {
     return options;
   }
 
+  usingSeleniumServer() {
+    return this.settings.selenium && this.settings.selenium.start_process;
+  }
+
+  shouldSetupWebdriver() {
+    return this.settings.webdriver.start_process && !this.settings.webdriver.server_path;
+  }
+
+  getChromedriverPath() {
+    try {
+      return require('chromedriver').path;
+    } catch (err) {
+      return '';
+    }
+  }
+
+  getGeckodriverPath() {
+    try {
+      return require('geckodriver').path;
+    } catch (err) {
+      return '';
+    }
+  }
+
   updateWebdriverPath() {
-    if (this.settings.webdriver.start_process && !this.settings.webdriver.server_path) {
+    if (this.shouldSetupWebdriver()) {
       try {
+        if (this.usingSeleniumServer()) {
+          this.settings.selenium.server_path = this.settings.webdriver.server_path = require('@nightwatch/selenium-server').path;
+          this.settings.selenium.cli_args = this.settings.selenium.cli_args || {};
+
+          const chromeDriver = this.getChromedriverPath();
+          if (chromeDriver) {
+            this.settings.selenium.cli_args['webdriver.chrome.driver'] = chromeDriver;
+          }
+
+          const geckoDriver = this.getGeckodriverPath();
+          if (geckoDriver) {
+            this.settings.selenium.cli_args['webdriver.gecko.driver'] = geckoDriver;
+          }
+
+          return this;
+        }
+
         switch (this.initialCapabilities.getBrowserName()) {
           case Browser.CHROME:
-            this.settings.webdriver.server_path = require('chromedriver').path;
+            this.settings.webdriver.server_path = this.getChromedriverPath();
             break;
 
           case Browser.FIREFOX:
-            this.settings.webdriver.server_path = require('geckodriver').path;
+            this.settings.webdriver.server_path = this.getGeckodriverPath();
             break;
 
           case Browser.SAFARI:
diff --git a/lib/transport/selenium-webdriver/service-builders/base-service.js b/lib/transport/selenium-webdriver/service-builders/base-service.js
index 15503a33..d9185d29 100644
--- a/lib/transport/selenium-webdriver/service-builders/base-service.js
+++ b/lib/transport/selenium-webdriver/service-builders/base-service.js
@@ -29,17 +29,21 @@ class BaseService {
     return null;
   }
 
+  get downloadMessage() {
+    return `download it from ${this.serviceDownloadUrl}, \nextract the archive and set ` +
+      '"webdriver.server_path" config option to point to the binary file.\n';
+  }
+
   get errorMessages() {
     let binaryMissing = `${this.serviceName} cannot be found in the current project.`;
 
     if (this.npmPackageName) {
-      binaryMissing += '\n\n ' + Logger.colors.yellow(`You can either install ${this.npmPackageName} from NPM: \n    npm install ${this.npmPackageName} --save-dev\n\n`) + ' or ';
+      binaryMissing += '\n\n ' + Logger.colors.yellow(`You can either install ${this.npmPackageName} from NPM with: \n\n    npm install ${this.npmPackageName} --save-dev\n\n`) + ' or ';
     } else {
       binaryMissing += '\n\n Please ';
     }
 
-    binaryMissing += `download it from ${this.serviceDownloadUrl}, \nextract the archive and set ` +
-      '"webdriver.server_path" config option to point to the binary file.\n';
+    binaryMissing += this.downloadMessage;
 
     return {
       binaryMissing
@@ -265,8 +269,8 @@ class BaseService {
       await this.createService(options);
     } catch (err) {
       const {message} = err;
-      err.message = `Unable to create the ${this.serviceName} process:`;
-      err.detailedErr = message + '; verify if webdriver is configured correctly; using:\n  ' + this.getSettingsFormatted() + '\n';
+      err.message = `Unable to create the ${this.serviceName} process: ${message}`;
+      err.detailedErr = '; verify if webdriver is configured correctly; using:\n  ' + this.getSettingsFormatted() + '\n';
       err.showTrace = false;
       err.sessionCreate = true;
 
diff --git a/lib/transport/selenium-webdriver/service-builders/safari.js b/lib/transport/selenium-webdriver/service-builders/safari.js
index 35d1f3cb..94c01dee 100644
--- a/lib/transport/selenium-webdriver/service-builders/safari.js
+++ b/lib/transport/selenium-webdriver/service-builders/safari.js
@@ -51,10 +51,10 @@ class SafariServiceBuilder extends BaseService {
    */
   async createService(options) {
     if (useTechnologyPreview(options.get(OPTIONS_CAPABILITY_KEY))) {
-      this.settings.server_path = SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE;
+      this.settings.webdriver.server_path = SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE;
     }
 
-    this.service = new safari.ServiceBuilder(this.settings.server_path);
+    this.service = new safari.ServiceBuilder(this.settings.webdriver.server_path);
 
     if (Array.isArray(this.cliArgs)) {
       this.service.addArguments(...this.cliArgs);
diff --git a/lib/transport/selenium-webdriver/service-builders/selenium.js b/lib/transport/selenium-webdriver/service-builders/selenium.js
index 0272f539..9d752866 100644
--- a/lib/transport/selenium-webdriver/service-builders/selenium.js
+++ b/lib/transport/selenium-webdriver/service-builders/selenium.js
@@ -1,7 +1,37 @@
-const {SeleniumServer} = require('selenium-webdriver/remote');
-const {Logger, isObject, isUndefined} = require('../../../utils');
+const path = require('path');
+const {SeleniumServer, DriverService} = require('selenium-webdriver/remote');
+const {isObject, isUndefined, isDefined, getFreePort} = require('../../../utils');
 const BaseService = require('./base-service.js');
 
+class SeleniumServer4 extends DriverService {
+  constructor(jar, opt_options) {
+    const options = opt_options || {};
+    const {command = 'standalone', jvmArgs, args} = options;
+    let combinedArgs = jvmArgs.concat('-jar', jar, command);
+
+    const port = options.port;
+    if (!args.includes('--port')) {
+      args.unshift('--port', port);
+    }
+
+    combinedArgs = combinedArgs.concat(args);
+
+    let java = 'java';
+    if (process.env['JAVA_HOME']) {
+      java = path.join(process.env['JAVA_HOME'], 'bin/java');
+    }
+
+    super(java, {
+      loopback: options.loopback,
+      port,
+      args: combinedArgs,
+      path: '/wd/hub',
+      env: options.env,
+      stdio: options.stdio
+    });
+  }
+}
+
 class SeleniumServiceBuilder extends BaseService {
   static get serviceName() {
     return 'Selenium Server';
@@ -12,7 +42,7 @@ class SeleniumServiceBuilder extends BaseService {
   }
 
   get npmPackageName() {
-    return 'chromedriver';
+    return '@nightwatch/selenium-server';
   }
 
   get outputFile() {
@@ -31,6 +61,11 @@ class SeleniumServiceBuilder extends BaseService {
     return 'https://selenium.dev/download/';
   }
 
+  get downloadMessage() {
+    return `download the selenium server jar from ${this.serviceDownloadUrl}, \n and set ` +
+      '"selenium.server_path" config option to point to the jar file.';
+  }
+
   setCliArgs() {
     const {cli_args, jvmArgs} = this.settings.selenium;
     let cliArgs = jvmArgs || cli_args;
@@ -70,18 +105,31 @@ class SeleniumServiceBuilder extends BaseService {
     }
   }
 
+  usingSelenium4() {
+    const {command, server_path} = this.settings.webdriver;
+
+    return isDefined(command) && !server_path.includes('selenium-server-standalone-3.');
+  }
+
   /**
    * @param {Capabilities} opts
    * @returns {Promise<void>}
    */
   async createService(opts) {
-    const {default_path_prefix, server_path} = this.settings.webdriver;
     const {port} = this;
-    Logger.info(`Starting ${this.serviceName} on port ${port}...`);
-
     const options = new SeleniumServer.Options();
-    if (port) {
-      options.port = port;
+    options.port = port || await getFreePort();
+    let {server_path, command} = this.settings.webdriver;
+
+    if (opts.showSpinner) {
+      let commandStr = '';
+      if (command) {
+        commandStr = ` in ${command} mode `;
+      }
+
+      let serverPathStr = server_path.split(path.sep).pop();
+
+      opts.showSpinner(`Starting Selenium Server [${serverPathStr}] on port ${options.port}${commandStr}...\n\n`);
     }
 
     // TODO: read the log_path and add it to cliArgs
@@ -93,7 +141,13 @@ class SeleniumServiceBuilder extends BaseService {
       options.stdio = ['pipe', this.process.stdin, this.process.stdin];
     }
 
-    this.service = new SeleniumServer(server_path, options);
+    let Constructor = SeleniumServer;
+    if (this.usingSelenium4()) {
+      options.command = command;
+      Constructor = SeleniumServer4;
+    }
+
+    this.service = new Constructor(server_path, options);
 
     return this.service.start();
   }


Test patch:
diff --git a/examples/tests/.gitignore b/examples/tests/.gitignore
deleted file mode 100644
index dbb61070..00000000
--- a/examples/tests/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-fbcredentials.json
diff --git a/examples/mocha/README.md b/examples/tests/README.md
similarity index 100%
rename from examples/mocha/README.md
rename to examples/tests/README.md
diff --git a/examples/tests/angularTest.js b/examples/tests/angularTest.js
deleted file mode 100644
index 97f5588c..00000000
--- a/examples/tests/angularTest.js
+++ /dev/null
@@ -1,68 +0,0 @@
-describe('angularjs homepage todo list', function() {
-
-  // using the new element() global utility in Nightwatch 2 to init elements
-  // before tests and use them later
-  const todoElement = element('[ng-model="todoList.todoText"]');
-  const addButtonEl = element('[value="add"]');
-
-  it('should add a todo using global element()', function() {
-    ///////////////////////////////////////////////////
-    // browser can now also be accessed as a global   |
-    ///////////////////////////////////////////////////
-
-    // adding a new task to the list
-    browser
-      .navigateTo('https://angularjs.org')
-      .sendKeys(todoElement, 'what is nightwatch?')
-      .click(addButtonEl);
-
-    ///////////////////////////////////////////////////
-    // global expect is equivalent to browser.expect  |
-    ///////////////////////////////////////////////////
-
-    // verifying if there are 3 tasks in the list
-    expect.elements('[ng-repeat="todo in todoList.todos"]').count.to.equal(3);
-
-    // verifying if the third task if the one we have just added
-    const lastElementTask = element({
-      selector: '[ng-repeat="todo in todoList.todos"]',
-      index: 2
-    });
-
-    expect(lastElementTask).text.to.equal('what is nightwatch?');
-
-    // find our task in the list and mark it as done
-    lastElementTask.findElement('input', function(inputResult) {
-      if (inputResult.error) {
-        throw inputResult.error;
-      }
-
-      const inputElement = element(inputResult.value);
-      browser.click(inputElement);
-    });
-
-    // verify if there are 2 tasks which are marked as done in the list
-    expect.elements('*[module=todoApp] li .done-true').count.to.equal(2);
-  });
-
-  it('should add a todo using custom commands', async function(browser) {
-    // adding a new task to the list
-    await browser
-      .navigateTo('https://angularjs.org')
-      .sendKeys('[ng-model="todoList.todoText"]', 'what is nightwatch?')
-      .click('[value="add"]');
-
-    const elements = await browser.angular.getElementsInList('todoList.todos');
-
-    // verifying if the third task if the one we have just added
-    await expect.element(elements[2]).text.toEqual('what is nightwatch?');
-
-    // find our task in the list and mark it as done
-    const inputElement = await element(elements[2]).findElement('input');
-    await browser.click(inputElement);
-
-    // verify if there are 2 tasks which are marked as done in the list
-    await expect.elements('*[module=todoApp] li .done-true').count.to.equal(2);
-  });
-
-});
diff --git a/examples/tests/angularTodoTest.js b/examples/tests/angularTodoTest.js
new file mode 100644
index 00000000..58b9fcfb
--- /dev/null
+++ b/examples/tests/angularTodoTest.js
@@ -0,0 +1,26 @@
+describe('angularjs homepage todo list', function() {
+
+  it('should add a todo using custom commands', async function(browser) {
+    // adding a new task to the list
+    const elements = await browser
+      .navigateTo('https://angularjs.org')
+      .sendKeys('[ng-model="todoList.todoText"]', 'what is nightwatch?')
+      .click('[value="add"]')
+      .angular.getElementsInList('todoList.todos');
+
+    const taskEl = element(elements[2]);
+
+    // verifying if the third task is the one we have just added
+    // browser.assert.textEquals(taskEl, 'what is nightwatch?');
+
+    await expect(taskEl).text.toEqual('what is nightwatch?');
+
+    // find our task in the list and mark it as done
+    const inputElement = await element(elements[2]).findElement('input');
+    await browser.click(inputElement);
+
+    // verify if there are 2 tasks which are marked as done in the list
+    await expect.elements('*[module=todoApp] li .done-true').count.to.equal(2);
+  });
+
+});
diff --git a/examples/tests/duckDuckGo.js b/examples/tests/duckDuckGo.js
index 3aa067a4..468be5f7 100644
--- a/examples/tests/duckDuckGo.js
+++ b/examples/tests/duckDuckGo.js
@@ -1,4 +1,5 @@
 describe('duckduckgo example', function() {
+  this.timeout(999999);
 
   it('Search Nightwatch.js and check results', function(browser) {
     browser
diff --git a/examples/tests/ecosia.js b/examples/tests/ecosia.js
index 7591488c..41be0ff0 100644
--- a/examples/tests/ecosia.js
+++ b/examples/tests/ecosia.js
@@ -2,7 +2,7 @@ describe('Ecosia.org Demo', function() {
 
   before(browser => browser.navigateTo('https://www.ecosia.org/'));
 
-  test('Demo test ecosia.org', function(browser) {
+  it('Demo test ecosia.org', function(browser) {
     browser
       .waitForElementVisible('body')
       .assert.titleContains('Ecosia')
diff --git a/examples/tests/github/github.js b/examples/tests/github/github.js
deleted file mode 100644
index 99e24b82..00000000
--- a/examples/tests/github/github.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
-  tags: ['git'],
-  'Demo test GitHub': function (client) {
-    client
-      .url('https://github.com/nightwatchjs/nightwatch')
-      .waitForElementVisible('body')
-      .assert.visible('#repository-container-header h1')
-      .assert.containsText('#repository-container-header h1', 'nightwatch', 'Checking if project title is set to nightwatch');
-  },
-
-  after(client) {
-    client.end();
-  }
-};
diff --git a/examples/tests/google.js b/examples/tests/google.js
index dfccd449..fbfed00c 100644
--- a/examples/tests/google.js
+++ b/examples/tests/google.js
@@ -2,7 +2,7 @@ describe('sample google search', function() {
   this.tags = ['google'];
 
   it('demo test using expect apis', async function(browser) {
-    browser.navigateTo('http://google.no');
+    await browser.navigateTo('http://google.no');
 
     const consentPresent = await browser.isPresent('[aria-modal="true"][title="Before you continue to Google Search"]');
 
diff --git a/examples/tests/googlePageObject.js b/examples/tests/googlePageObject.js
index 679abe85..c857d627 100644
--- a/examples/tests/googlePageObject.js
+++ b/examples/tests/googlePageObject.js
@@ -1,34 +1,41 @@
-module.exports = {
-  'Demo Google search test using page objects': async function (browser) {
-    const homePage = browser.page.google.search();
-    const consentPage = browser.page.google.consent();
+describe('google search with consent form - page objects', function() {
 
-    homePage.navigate();
+  this.disabled = this.argv.env !== 'chrome';
+
+  const homePage = browser.page.google.search();
+  const consentPage = browser.page.google.consent();
+
+  before(async () => homePage.navigate());
+
+  after(async (browser) => browser.quit());
+
+  it('should complete the consent form', async function (browser) {
+
+    const consentPresent = await homePage.isPresent('@consentModal');
 
-    const consentPresent = await homePage.isPresent('[aria-modal="true"][title="Before you continue to Google Search"]');
     if (consentPresent) {
-      homePage.expect.section('@consentModal').to.be.visible;
+      await homePage.expect.section('@consentModal').to.be.visible;
 
       const {consentModal} = homePage.section;
-      consentModal.click('@customizeButton');
+      await consentModal.click('@customizeButton');
 
-      browser.expect.url().toContain('https://consent.google.');
-      consentPage.turnOffEverything();
+      await browser.expect.url().toContain('https://consent.google.');
+      await consentPage.turnOffEverything();
     }
+  });
 
+  it('should find nightwatch.js in results', function (browser) {
     homePage.setValue('@searchBar', 'Nightwatch.js');
     homePage.submit();
 
     const resultsPage = browser.page.google.searchResults();
     resultsPage.expect.element('@results').to.be.present;
 
-    await resultsPage.expect.element('@results').text.to.contain('Nightwatch.js');
+    resultsPage.expect.element('@results').text.to.contain('Nightwatch.js');
 
     resultsPage.expect.section('@menu').to.be.visible;
 
     const menuSection = resultsPage.section.menu;
     menuSection.expect.element('@all').to.be.visible;
-
-    browser.quit();
-  }
-};
+  });
+});
diff --git a/examples/tests/nightwatchByIndex.js b/examples/tests/nightwatchByIndex.js
deleted file mode 100644
index 733b04f6..00000000
--- a/examples/tests/nightwatchByIndex.js
+++ /dev/null
@@ -1,27 +0,0 @@
-module.exports = {
-  '@disabled': true,
-
-  'Show getting Nightwatch features using index': function (client) {
-
-    client.url('http://nightwatchjs.org');
-
-    var featuresPage = client.page.nightwatchFeatures();
-
-    featuresPage
-      .assert.textContains('@featuresHeading', 'Main Features')
-      .getFeatureCount(function (result) {
-
-        client.assert.equal(result.status, 0, 'status counting features');
-        var totalFeatures = result.value;
-        var lastFeatureIndex = totalFeatures - 1;
-
-        featuresPage.getText({selector: '@features', index: lastFeatureIndex}, function (result) {
-          client.assert.equal(result.status, 0, 'status finding last feature text');
-          client.assert.equal(result.value, 'Continous integration support', 'text of last feature');
-        });
-
-      });
-      
-    client.end();
-  }
-};
diff --git a/examples/tests/shadowRootExample.js b/examples/tests/shadowRootExample.js
index f307ea21..f632159b 100644
--- a/examples/tests/shadowRootExample.js
+++ b/examples/tests/shadowRootExample.js
@@ -1,5 +1,8 @@
 describe('Shadow Root example test', function() {
 
+  // Doesn't work in Firefox due to a serialization issue in Geckodriver
+  this.disabled = this.settings.desiredCapabilities.browserName === 'firefox';
+
   it('retrieve the shadowRoot', async function(browser) {
     await browser
       .navigateTo('https://mdn.github.io/web-components-examples/popup-info-box-web-component/')
diff --git a/examples/tests/vueTodoList.js b/examples/tests/vueTodoList.js
new file mode 100644
index 00000000..16638c8e
--- /dev/null
+++ b/examples/tests/vueTodoList.js
@@ -0,0 +1,46 @@
+/**
+ * End-to-end test for the sample Vue3+Vite todo app located at
+ * https://github.com/nightwatchjs-community/todo-vue
+ */
+describe('To-Do List End-to-End Test', function() {
+
+  // using the new element() global utility in Nightwatch 2 to init elements
+  // before tests and use them later
+  const todoElement = element('#new-todo-input');
+  const addButtonEl = element('form button[type="submit"]');
+
+  it('should add a todo using global element()', async function() {
+    ///////////////////////////////////////////////////
+    // browser can now also be accessed as a global   |
+    ///////////////////////////////////////////////////
+
+    // adding a new task to the list
+    await browser
+      .navigateTo('https://todo-vue3-vite.netlify.app/')
+      .sendKeys(todoElement, 'what is nightwatch?')
+      .click(addButtonEl);
+
+    ///////////////////////////////////////////////////
+    // global expect is equivalent to browser.expect  |
+    ///////////////////////////////////////////////////
+
+    // verifying if there are 5 tasks in the list
+    await expect.elements('#todo-list ul li').count.toEqual(5);
+
+    // verifying if the 4th task if the one we have just added
+    const lastElementTask = element({
+      selector: '#todo-list ul li',
+      index: 4
+    });
+
+    await expect(lastElementTask).text.toContain('what is nightwatch?');
+
+    // find our task in the list and mark it as done
+    const inputElement = await lastElementTask.findElement('input[type="checkbox"]');
+    await browser.click(inputElement);
+
+    // verify if there are 3 tasks which are marked as done in the list
+    await expect.elements('#todo-list ul li input:checked').count.toEqual(3);
+  });
+
+});
diff --git a/lib/runner/test-runners/mocha.js b/lib/runner/test-runners/mocha.js
index c67d2b31..6b461735 100644
--- a/lib/runner/test-runners/mocha.js
+++ b/lib/runner/test-runners/mocha.js
@@ -17,8 +17,8 @@ class MochaRunner {
 
         const runner = this;
 
-        this.suite.on('suite', suite => {
-          suite.on('suite', childSuite => {
+        this.suite.on('suite', function (suite) {
+          suite.on('suite', function (childSuite) {
             augmentTestSuite({suite: childSuite, runner, argv, settings, addtOpts});
           });
 
@@ -33,7 +33,7 @@ class MochaRunner {
   }
 
   get combinedReporterList() {
-    return ['mochawesome'];
+    return ['mochawesome', 'html'];
   }
 
   get type() {
@@ -61,6 +61,10 @@ class MochaRunner {
       this.mochaOpts.reporter = argv.reporter;
     }
 
+    if (argv.reporter === 'html') {
+      this.mochaOpts.reporter = 'mochawesome';
+    }
+
     if (argv.grep) {
       this.mochaOpts.grep = argv.grep;
     }
@@ -92,22 +96,22 @@ class MochaRunner {
       const reportFilename = filePath.substring(filePath.lastIndexOf('/')+1).split('.')[0];
       this.mochaOpts.isWorker = true;
 
-      switch (argv.reporter) {
-        case 'mocha-junit-reporter':
-        case 'mochawesome':
-          this.mochaOpts.reporterOptions.mochaFile = path.join(settings.output_folder, `${reportFilename}.xml`);
-          break;
-      }
-
-      if (argv.reporter === 'mochawesome') {
+      if (argv.reporter === 'mocha-junit-reporter') {
+        this.mochaOpts.reporterOptions.mochaFile = path.join(settings.output_folder, `${reportFilename}.xml`);
+      } else if (argv.reporter === 'mochawesome') {
         this.mochaOpts.reporterOptions = Object.assign(this.mochaOpts.reporterOptions, {
-          //overwrite: false,
           html: false,
-          json: true
+          json: true,
+          quiet: true,
+          reportFilename
         });
       }
     }
 
+    if (this.mochaOpts.reporter === 'mochawesome' && !this.mochaOpts.reporterOptions.reportDir) {
+      this.mochaOpts.reporterOptions.reportDir = 'html-report';
+    }
+
     const MochaNightwatch = MochaRunner.MochaNightwatch({settings, argv, addtOpts});
     this.mocha = new MochaNightwatch(this.mochaOpts, settings);
   }
@@ -124,7 +128,7 @@ class MochaRunner {
   }
 
   checkReporterDependencies() {
-    if (this.argv.reporter !== 'mochawesome') {
+    if (!['mochawesome', 'html'].includes(this.argv.reporter)) {
       return;
     }
 
@@ -147,6 +151,7 @@ class MochaRunner {
     const {reportDir} = this.mochaOpts.reporterOptions;
 
     switch (this.argv.reporter) {
+      case 'html':
       case 'mochawesome': {
         const marge = require('mochawesome-report-generator');
         const {merge} = require('mochawesome-merge');
diff --git a/lib/runner/test-runners/mocha/createNightwatchSuite.js b/lib/runner/test-runners/mocha/createNightwatchSuite.js
index 68e19d1c..be81eba8 100644
--- a/lib/runner/test-runners/mocha/createNightwatchSuite.js
+++ b/lib/runner/test-runners/mocha/createNightwatchSuite.js
@@ -1,18 +1,115 @@
 const TestSuite = require('../../../testsuite');
 
-module.exports = ({suite, modules = [], settings, argv, addtOpts}) => {
+module.exports = function ({suite, settings, argv, modulePath, addtOpts, runner}) {
+  const modules = [];
+  const timeoutFn = suite.timeout;
   const nightwatchSuite = new TestSuite({
     modules,
     settings,
     argv,
     usingMocha: true,
     addtOpts,
-    modulePath: null
+    modulePath
   });
 
   nightwatchSuite.initCommon({
     suiteTitle: suite.title
   });
 
-  return nightwatchSuite;
+  Object.defineProperties(suite, {
+    nightwatchSuite: {
+      configurable: true,
+      get: function() {
+        return nightwatchSuite;
+      }
+    },
+
+    client: {
+      configurable: true,
+      get: function() {
+        return this.nightwatchSuite.client;
+      }
+    },
+
+    isWorker: {
+      configurable: true,
+      get: function() {
+        return runner.isWorker;
+      }
+    },
+
+    files: {
+      configurable: true,
+      get: function() {
+        return runner.files;
+      }
+    },
+
+    mochaOptions: {
+      configurable: true,
+      get: function() {
+        return runner.options;
+      }
+    }
+  });
+
+  Object.defineProperties(suite, {
+    globals: {
+      get: function() {
+        return this.nightwatchSuite.settings.globals;
+      }
+    },
+
+    settings: {
+      get: function() {
+        return this.nightwatchSuite.settings;
+      }
+    },
+
+    argv: {
+      get: function() {
+        return this.nightwatchSuite.argv;
+      }
+    },
+
+    suiteRetries: {
+      value: function(value) {
+        if (typeof value != 'undefined') {
+          this.nightwatchSuite.context.setSuiteRetries(value);
+        }
+      }
+    },
+
+    waitForTimeout: {
+      value: function(value) {
+        if (typeof value == 'undefined') {
+          return this.globals.waitForConditionTimeout;
+        }
+        this.globals.waitForConditionTimeout = value;
+        this.globals.retryAssertionTimeout = value;
+      }
+    },
+
+    timeout: {
+      value: function(value) {
+        if (typeof value == 'undefined') {
+          return timeoutFn.call(this);
+        }
+
+        this.globals.unitTestsTimeout = value;
+
+        timeoutFn.call(this, value);
+      }
+    },
+
+    waitForRetryInterval: {
+      value: function(value) {
+        if (typeof value == 'undefined') {
+          return this.globals.waitForConditionPollInterval;
+        }
+
+        this.globals.waitForConditionPollInterval = value;
+      }
+    }
+  });
 };
\ No newline at end of file
diff --git a/lib/runner/test-runners/mocha/custom-runnable.js b/lib/runner/test-runners/mocha/custom-runnable.js
index 4f7b3422..83a1b6c3 100644
--- a/lib/runner/test-runners/mocha/custom-runnable.js
+++ b/lib/runner/test-runners/mocha/custom-runnable.js
@@ -80,7 +80,7 @@ module.exports = async function(fn, isHook = false) {
 
     const {nightwatchSuite} = this.parent;
 
-    const err = await nightwatchSuite.handleRunnable(this.title, () => {
+    let err = await nightwatchSuite.handleRunnable(this.title, () => {
       if (isHook && this.async === 2) {
         return new Promise((resolve, reject) => {
           args.push(function(err) {
@@ -107,6 +107,10 @@ module.exports = async function(fn, isHook = false) {
       }
     });
 
+    if (err && !(err instanceof Error)) {
+      err = undefined;
+    }
+
     if (this.async <= 1) {
       done(err);
       emitted = true;
diff --git a/lib/runner/test-runners/mocha/custom-runner.js b/lib/runner/test-runners/mocha/custom-runner.js
index d3b00626..3b4f8f63 100644
--- a/lib/runner/test-runners/mocha/custom-runner.js
+++ b/lib/runner/test-runners/mocha/custom-runner.js
@@ -46,6 +46,13 @@ module.exports = class CustomRunner extends Mocha.Runner {
     };
 
     try {
+      if (suite.disabled) {
+        // eslint-disable-next-line no-console
+        console.log(`Testsuite "${suite.title}" is disabled, skipping...`);
+
+        return fn();
+      }
+
       // create the nightwatch session
       const sessionInfo = await createSession();
     } catch (err) {
diff --git a/lib/runner/test-runners/mocha/extensions.js b/lib/runner/test-runners/mocha/extensions.js
index 0965d71d..8a244ad8 100644
--- a/lib/runner/test-runners/mocha/extensions.js
+++ b/lib/runner/test-runners/mocha/extensions.js
@@ -23,7 +23,9 @@ module.exports = class Extensions {
 
     if (parent.suites && parent.suites.length > 0) {
       parent.suites.forEach(function(item) {
-        Extensions.adaptRunnables(item);
+        return (function(_item) {
+          Extensions.adaptRunnables(_item);
+        })(item);
       });
     }
 
@@ -49,14 +51,13 @@ module.exports = class Extensions {
   }
 
   static augmentTestSuite({suite, runner, argv, settings, addtOpts}) {
-    const timeoutFn = suite.timeout;
-
     const attributes = [
       'tags',
       'desiredCapabilities',
       'endSessionOnFail',
       'skipTestcasesOnFail',
-      'unitTest'
+      'unitTest',
+      'disabled'
     ];
 
     Object.defineProperties(suite, attributes.reduce((prev, attribute) => {
@@ -78,112 +79,18 @@ module.exports = class Extensions {
       return prev;
     }, {}));
 
-    const nightwatchSuite = createNightwatchSuite({
+    const suiteIndex = Math.max(0, runner.suite.suites.length - 1);
+    createNightwatchSuite({
+      runner,
       suite,
       settings,
       argv,
-      addtOpts
-    });
-
-    Object.defineProperties(suite, {
-      nightwatchSuite: {
-        configurable: true,
-        get: function() {
-          return nightwatchSuite;
-        }
-      },
-
-      client: {
-        configurable: true,
-        get: function() {
-          return nightwatchSuite.client;
-        }
-      },
-
-      isWorker: {
-        configurable: true,
-        get: function() {
-          return runner.isWorker;
-        }
-      },
-
-      files: {
-        configurable: true,
-        get: function() {
-          return runner.files;
-        }
-      },
-
-      mochaOptions: {
-        configurable: true,
-        get: function() {
-          return runner.options;
-        }
-      }
-    });
-
-    Object.defineProperties(suite, {
-      globals: {
-        get: function() {
-          return this.nightwatchSuite.settings.globals;
-        }
-      },
-
-      settings: {
-        get: function() {
-          return this.nightwatchSuite.settings;
-        }
-      },
-
-      argv: {
-        get: function() {
-          return this.nightwatchSuite.argv;
-        }
-      },
-
-      suiteRetries: {
-        value: function(value) {
-          if (typeof value != 'undefined') {
-            this.nightwatchSuite.context.setSuiteRetries(value);
-          }
-        }
-      },
-
-      waitForTimeout: {
-        value: function(value) {
-          if (typeof value == 'undefined') {
-            return this.globals.waitForConditionTimeout;
-          }
-          this.globals.waitForConditionTimeout = value;
-          this.globals.retryAssertionTimeout = value;
-        }
-      },
-
-      timeout: {
-        value: function(value) {
-          if (typeof value == 'undefined') {
-            return timeoutFn.call(this);
-          }
-
-          this.globals.unitTestsTimeout = value;
-
-          timeoutFn.call(this, value);
-        }
-      },
-
-      waitForRetryInterval: {
-        value: function(value) {
-          if (typeof value == 'undefined') {
-            return this.globals.waitForConditionPollInterval;
-          }
-
-          this.globals.waitForConditionPollInterval = value;
-        }
-      }
+      addtOpts,
+      modulePath: runner.files[suiteIndex]
     });
 
     if (Utils.isUndefined(suite.timeout())) {
-      suite.timeout(20000);
+      suite.timeout(30000);
     }
   }
 };
\ No newline at end of file
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 1ddb1950..5c744ce9 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -86,7 +86,7 @@ class TestSuite {
       this.addPropertiesToGlobalScope();
     }
 
-    this.initClient();
+    this.initClient(opts);
 
     if (this.argv.preview) {
       this.context = {
@@ -131,6 +131,16 @@ class TestSuite {
   }
 
   updateClient() {
+    // this is necessary because mocha as a different suite setup flow
+    if (!this.settings.disable_global_apis) {
+      Object.defineProperty(global, 'browser', {
+        configurable: true,
+        get: function() {
+          return this.client.api;
+        }.bind(this)
+      });
+    }
+
     if (this.settings.sync_test_names) {
       this.client.mergeCapabilities({
         name: this.suiteName
diff --git a/lib/testsuite/runnable.js b/lib/testsuite/runnable.js
index 24fafca5..b60f43d3 100644
--- a/lib/testsuite/runnable.js
+++ b/lib/testsuite/runnable.js
@@ -148,6 +148,12 @@ class Runnable {
       if (this.isES6Async) {
         // the timeout is needed for situations where there is an async function without any await commands
         setTimeout(() => deferredFn(), 20);
+
+        // without .catch() here, we can get an unhandledRejection
+        result
+          .catch(err => {
+            this.deffered.rejectFn(err);
+          });
       } else {
         deferredFn();
       }
diff --git a/test/extra/existing-custom-commands/click.js b/test/extra/existing-custom-commands/click.js
new file mode 100644
index 00000000..450a0cf5
--- /dev/null
+++ b/test/extra/existing-custom-commands/click.js
@@ -0,0 +1,3 @@
+module.exports.command = function() {
+
+}
\ No newline at end of file
diff --git a/test/src/api/assertions/testElementsCount.js b/test/src/api/assertions/testElementsCount.js
new file mode 100644
index 00000000..0f341155
--- /dev/null
+++ b/test/src/api/assertions/testElementsCount.js
@@ -0,0 +1,137 @@
+const assert = require('assert');
+const Globals = require('../../../lib/globals.js');
+
+describe('assert.elementCounts', function () {
+  const assertionName = 'elementsCount';
+  const api = 'findElements';
+
+  function assertionTest(opts) {
+    return Globals.assertion(assertionName, api, opts);
+  }
+
+  it('elementsCount assertion passed', function () {
+    return assertionTest({
+      args: ['.test_element', 2, 'Test message'],
+      commandResult: {
+        value: [{'ELEMENT': '0'}, {'ELEMENT': '1'}]
+      },
+      assertMessage: true,
+      assertion({instance, failure}) {
+        assert.deepStrictEqual(instance.options, {elementSelector: true});
+        assert.strictEqual(instance.message, 'Test message');
+        assert.strictEqual(failure, false);
+        assert.strictEqual(instance.hasFailure(), false);
+      }
+    });
+  });
+
+  it('not.elementsCount assertion passed', function () {
+    return assertionTest({
+      args: ['.test_element', 2],
+      commandResult: {
+        value: [{'ELEMENT': '0'}]
+      },
+      negate: true,
+      assertion({reporter, instance, queueOpts, message}) {
+        assert.strictEqual(typeof err, 'undefined');
+        assert.strictEqual(queueOpts.negate, true);
+
+        assert.strictEqual(instance.hasFailure(), false);
+        assert.strictEqual(instance.getValue(), 1);
+        assert.strictEqual(instance.getActual(), 1);
+        assert.strictEqual(instance.message, 'Testing if the element count for <.test_element> is not 2');
+        assert.ok(message.startsWith('Testing if the element count for <.test_element> is not 2'), message);
+      }
+    });
+  });
+
+
+  it('elementsCount assertion failed', function () {
+    return assertionTest({
+      args: ['.test_element', 1],
+      commandResult: {
+        value: [{'ELEMENT': '0'}, {'ELEMENT': '1'}]
+      },
+      negate: false,
+      assertError: true,
+      assertion({instance, queueOpts, err}) {
+        assert.strictEqual(queueOpts.negate, false);
+        assert.strictEqual(instance.hasFailure(), false);
+        assert.strictEqual(instance.getValue(), 2);
+        assert.strictEqual(instance.getActual(), 2);
+        assert.strictEqual(err.message, `Testing if the element count for <.test_element> is 1 in 5ms - expected "counts 1" but got: "2" (${instance.elapsedTime}ms)`);
+      }
+    });
+  });
+
+  it('not.elementsCount assertion failed', function () {
+    return assertionTest({
+      args: ['.test_element', 2],
+      commandResult: {
+        value: [{'ELEMENT': '0'}, {'ELEMENT': '1'}]
+      },
+      negate: true,
+      assertError: true,
+      assertion({instance, queueOpts, err}) {
+        assert.strictEqual(queueOpts.negate, true);
+        assert.strictEqual(instance.hasFailure(), false);
+        assert.strictEqual(instance.getValue(), 2);
+        assert.strictEqual(instance.getActual(), 2);
+        assert.strictEqual(err.message, `Testing if the element count for <.test_element> is not 2 in 5ms - expected "does not count 2" but got: "2" (${instance.elapsedTime}ms)`);
+      }
+    });
+  });
+
+  it('elementCounts assertion passed with selector object', function() {
+    return assertionTest({
+      args: [{selector: '.test_element'}, 1],
+      commandResult: {
+        value: [{'ELEMENT': 0}]
+      },
+      assertArgs: true,
+      assertResult: true,
+      assertion({instance, failure, message, err}) {
+        assert.strictEqual(err, undefined);
+        assert.deepStrictEqual(instance.options, {elementSelector: true});
+        assert.strictEqual(instance.getActual(), 1);
+        assert.strictEqual(instance.hasFailure(), false);
+        assert.ok(message.startsWith('Testing if the element count for <.test_element> is 1'), message);
+        assert.strictEqual(failure, false);
+      }
+    });
+  });
+
+  it('elementsCount assertion failed', function() {
+    return assertionTest({
+      args: ['.test_element', 2, 'Test message expected element %s to have count %s'],
+      commandResult: {
+        value: [{'ELMENT': 1}]
+      },
+      assertError: true,
+      assertResult: true,
+      assertion({instance, failure}) {
+        assert.strictEqual(instance.getActual(), 1);
+        assert.strictEqual(instance.message, 'Test message expected element <.test_element> to have count 2');
+        assert.strictEqual(failure, 'Expected "counts 2" but got: "1"');
+      }
+    });
+  });
+
+  it('elementsCount assertion - no elements found', function() {
+    return assertionTest({
+      args: ['.test_element', 2],
+      commandResult: {
+        status: 0,
+        value: []
+      },
+      assertError: true,
+      assertResult: true,
+      assertion({instance, message, failure}) {
+        assert.strictEqual(message, `Testing if the element count for <.test_element> is 2 in 5ms - expected "counts 2" but got: "0" (${instance.elapsedTime}ms)`);
+        assert.strictEqual(instance.hasFailure(), false);
+        assert.strictEqual(instance.getValue(), 0);
+        assert.strictEqual(instance.getActual(), 0);
+      }
+    });
+  });
+});
diff --git a/test/src/cli/testCliRunner.js b/test/src/cli/testCliRunner.js
index f157401a..a2eb3490 100644
--- a/test/src/cli/testCliRunner.js
+++ b/test/src/cli/testCliRunner.js
@@ -933,4 +933,17 @@ describe('Test CLI Runner', function() {
 
     assert.strictEqual(runner.argv.config, './nightwatch.conf.cjs');
   });
+
+  it('using no package.json file', function() {
+    mockery.deregisterMock('package.json');
+    mockery.registerMock('package.json');
+
+    registerNoSettingsJsonMock();
+
+    const CliRunner = common.require('runner/cli/cli.js');
+    let runner = new CliRunner({});
+
+    const config = runner.getLocalConfigFileName();
+    assert.strictEqual(config, './nightwatch.conf.js');
+  });
 });
diff --git a/test/src/cli/testCliRunnerGenerate.js b/test/src/cli/testCliRunnerGenerate.js
index 9702b568..e3c0d110 100644
--- a/test/src/cli/testCliRunnerGenerate.js
+++ b/test/src/cli/testCliRunnerGenerate.js
@@ -181,11 +181,14 @@ describe('Test CLI Runner Generate', function() {
           selenium: {
             start_process: true,
             port: 4444,
+            command: 'standalone',
             server_path: '',
             cli_args: {
-              'webdriver.gecko.driver': '',
-              'webdriver.chrome.driver': ''
             }
+          },
+          webdriver: {
+            start_process: false,
+            default_path_prefix: '/wd/hub'
           }
         });
       },
@@ -200,6 +203,12 @@ describe('Test CLI Runner Generate', function() {
     }).setup();
 
     assert.strictEqual(ieRunner.argv.config, path.join(process.cwd(), 'nightwatch.conf.js'));
+
+    const chromeLocalRunner = new CliRunner({
+      config: './nightwatch.json',
+      env: 'browserstack.local_chrome'
+    }).setup();
+
     assert.deepStrictEqual(ieRunner.test_settings.desiredCapabilities, {
       browserName: 'internet explorer',
       browserVersion: '11.0',
@@ -208,12 +217,6 @@ describe('Test CLI Runner Generate', function() {
         accessKey: '${BROWSERSTACK_KEY}'
       }
     });
-
-    const chromeLocalRunner = new CliRunner({
-      config: './nightwatch.json',
-      env: 'browserstack.local_chrome'
-    }).setup();
-
     assert.deepStrictEqual(chromeLocalRunner.test_settings.desiredCapabilities, {
       browserName: 'chrome',
       'browserstack.local': true,
diff --git a/test/src/cli/testParallelExecution.js b/test/src/cli/testParallelExecution.js
index 844cb17c..ebef6c68 100644
--- a/test/src/cli/testParallelExecution.js
+++ b/test/src/cli/testParallelExecution.js
@@ -175,6 +175,20 @@ describe('test Parallel Execution', function() {
     });
   });
 
+  it('test parallel execution with parallel=count arg', function() {
+    const CliRunner = common.require('runner/cli/cli.js');
+    let runner = new CliRunner({
+      config: path.join(__dirname, '../../extra/parallelism-count.json'),
+      parallel: 2
+    });
+
+    runner.setup();
+    assert.deepStrictEqual(runner.test_settings.test_workers, {
+      enabled: true,
+      workers: 2
+    });
+  });
+
   it('test parallel execution with workers count and extended envs', function() {
     const CliRunner = common.require('runner/cli/cli.js');
     let runner = new CliRunner({
diff --git a/test/src/core/testPageObjectCommands.js b/test/src/core/testPageObjectCommands.js
index f51a2568..31e24c8b 100644
--- a/test/src/core/testPageObjectCommands.js
+++ b/test/src/core/testPageObjectCommands.js
@@ -219,5 +219,60 @@ describe('test PageObject Commands', function () {
 
     this.client.start();
   });
+
+  it('testPageObject navigate() with url param', function (done) {
+    let page = this.client.api.page.simplePageObj();
+    let urlsArr = [];
+    page.api.url = function (url) {
+      urlsArr.push(url);
+    };
+
+    page.navigate('http://local');
+
+    page.api.perform(function () {
+      page.url = function () {
+        return 'http://nightwatchjs.org';
+      };
+    });
+
+    page.api.perform(function () {
+      page.navigate();
+    });
+
+    page.api.perform(function () {
+      try {
+        assert.deepStrictEqual(urlsArr, ['http://local', 'http://nightwatchjs.org']);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    this.client.start();
+  });
+
+  it('testPageObject navigate() with url param and callback', function (done) {
+    let page = this.client.api.page.simplePageObj();
+    let urlsArr = [];
+    page.api.url = function (url, callback) {
+      urlsArr.push(url);
+      callback();
+    };
+    let called = false;
+    page.navigate('http://local', function() {
+      called = true;
+    });
+
+    page.api.perform(function () {
+      try {
+        assert.strictEqual(called, true);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    this.client.start();
+  });
 });
 
diff --git a/test/src/runner/cucumber-integration/testCliArgs.js b/test/src/runner/cucumber-integration/testCliArgs.js
index 7796c8df..76784c01 100644
--- a/test/src/runner/cucumber-integration/testCliArgs.js
+++ b/test/src/runner/cucumber-integration/testCliArgs.js
@@ -7,7 +7,7 @@ const CucumberRunner = common.require('runner/test-runners/cucumber.js');
 describe('Cucumber cli arguments', function(){
   let cliArgs;
 
-  this.beforeAll(function() {
+  before(function() {
     mockery.enable();
     mockery.registerMock('@cucumber/cucumber/lib/cli/index', {
       default: class CucumberCli {
@@ -18,7 +18,7 @@ describe('Cucumber cli arguments', function(){
     });
   });
 
-  this.afterAll(function(){
+  after(function(){
     mockery.deregisterAll();
     mockery.disable();
   });
diff --git a/test/src/runner/testRunWithExistingCommands.js b/test/src/runner/testRunWithExistingCommands.js
new file mode 100644
index 00000000..ec5a17a4
--- /dev/null
+++ b/test/src/runner/testRunWithExistingCommands.js
@@ -0,0 +1,37 @@
+const path = require('path');
+const assert = require('assert');
+const common = require('../../common.js');
+const {settings} = common;
+const {runTests} = common.require('index.js');
+
+describe('testRunWithExistingCommands', function () {
+
+  it('testRunner with element locate errors enabled', function () {
+    let testsPath = path.join(__dirname, '../../sampletests/');
+
+    let globals = {
+      retryAssertionTimeout: 90,
+      abortOnElementLocateError: true,
+      waitForConditionTimeout: 90,
+      waitForConditionPollInterval: 90,
+      reporter(results, cb) {
+
+        cb();
+      }
+    };
+
+    return runTests({
+      _source: [testsPath]
+    }, settings({
+      output: true,
+      custom_commands_path: [path.join(__dirname, '../../extra/existing-custom-commands')],
+      globals
+    })).catch(err => {
+      assert.strictEqual(err.message, 'Error while loading the API commands: the command .click() is already defined.');
+      assert.strictEqual(err.name, 'TypeError');
+    })
+
+  });
+
+
+});
diff --git a/test/src/runner/testRunnerSessionCreate.js b/test/src/runner/testRunnerSessionCreate.js
index 9765e969..b4c3ddf0 100644
--- a/test/src/runner/testRunnerSessionCreate.js
+++ b/test/src/runner/testRunnerSessionCreate.js
@@ -268,8 +268,8 @@ describe('testRunnerSessionCreate', function() {
         assert.strictEqual(results.lastError.sessionCreate, true);
         assert.strictEqual(results.lastError.showTrace, false);
         assert.ok(results.lastError instanceof Error);
-        assert.ok(results.lastError.detailedErr.includes('The specified executable path does not exist: /bin/xxxxx; verify if webdriver is configured correctly; using:'));
-        assert.strictEqual(results.lastError.message, 'Unable to create the GeckoDriver process:');
+        assert.ok(results.lastError.detailedErr.includes('verify if webdriver is configured correctly; using:'));
+        assert.strictEqual(results.lastError.message, 'Unable to create the GeckoDriver process: The specified executable path does not exist: /bin/xxxxx');
       }
     };
 
diff --git a/test/src/service-builders/testChromeDriver.js b/test/src/service-builders/testChromeDriver.js
new file mode 100644
index 00000000..0ee9b382
--- /dev/null
+++ b/test/src/service-builders/testChromeDriver.js
@@ -0,0 +1,279 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('ChromeDriver Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+
+    const {constants, rmdirSync} = require('fs');
+
+    delete require.cache['fs'];
+
+    mockery.registerMock('fs', {
+      existsSync() {
+        return true;
+      },
+      constants,
+      rmdirSync
+    });
+
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const sessionData = {
+    sessionId: '111',
+    getId() {
+      return '1111'
+    },
+    getCapabilities() {
+      return {
+        getPlatform() {
+          return 'MAC'
+        },
+        getBrowserName() {
+          return 'chrome';
+        },
+        getBrowserVersion() {
+          return '11';
+        },
+        get(name) {
+          return name;
+        },
+        keys() {
+          return new Map();
+        }
+      }
+    }
+  };
+
+  const mockDriver = {
+    async getSession() {
+      return sessionData;
+    },
+    async getExecutor() {
+      return {
+        w3c: true
+      }
+    }
+  }
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function createMockDriverBuilder() {
+    const {Builder} = require('selenium-webdriver');
+    class MockBuilder extends Builder {
+      build() {
+        return Promise.resolve(mockDriver);
+      }
+    }
+
+    return new MockBuilder();
+  }
+
+  function mockServiceBuilder({onConstruct = fn, onAddArguments = fn, onSetPort = fn, onSetHostname = fn, onSetPath = fn}) {
+    const chrome = require('selenium-webdriver/chrome');
+    const {Options, ServiceBuilder} = chrome;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/chrome');
+
+    class MockServiceBuilder extends ServiceBuilder {
+      constructor(server_path) {
+        super(server_path);
+
+        onConstruct.call(this, server_path);
+      }
+
+      setPort(port) {
+        onSetPort(port);
+      }
+
+      setHostname(h) {
+        onSetHostname(h);
+      }
+
+      setPath(h) {
+        onSetPath(h);
+      }
+
+      addArguments(args) {
+        onAddArguments(args);
+      }
+
+      enableChromeLogging() {
+        return this;
+      }
+
+      enableVerboseLogging() {
+        return this;
+      }
+
+      build() {
+        return {
+          kill() {
+
+          },
+          async start() {
+            return 'http://localhost'
+          }
+        }
+      }
+    }
+
+    mockery.registerMock('selenium-webdriver/chrome', {
+      Options,
+      ServiceBuilder: MockServiceBuilder
+    });
+  }
+
+  async function ChromeDriverTestSetup(useSettings, argv = {}) {
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'ChromeDriver');
+        assert.strictEqual(this.outputFile, '_chromedriver.log');
+        assert.strictEqual(this.requiresDriverBinary, true);
+        assert.strictEqual(this.defaultPort, undefined);
+        assert.strictEqual(this.npmPackageName, 'chromedriver');
+        assert.strictEqual(this.serviceDownloadUrl, 'https://sites.google.com/chromium.org/driver/downloads');
+      }
+
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let serverPath;
+    let serverPort;
+    let buildArgs;
+    let logFilePath;
+
+    mockServiceBuilder({
+      onConstruct(server_path) {
+        serverPath = server_path;
+      },
+
+      onAddArguments(args) {
+        buildArgs = args;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      }
+    });
+
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, argv);
+    const {transport} = client;
+    transport.createSessionBuilder = function() {
+      return createMockDriverBuilder();
+    };
+
+    const session = await transport.createSession({argv: {}, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      serverPath,
+      serverPort
+    }
+  }
+
+  it('test create session with chrome driver -- not found error', async function() {
+    let error;
+    try {
+      await ChromeDriverTestSetup({
+        desiredCapabilities: {
+          browserName: 'chrome'
+        },
+        webdriver: {
+          port: 9999,
+          start_process: true
+        }
+      });
+    } catch (err) {
+      error = err;
+    }
+
+    assert.ok(error instanceof Error);
+    assert.strictEqual(error.message, 'ChromeDriver cannot be found in the current project.');
+  });
+
+
+  it('test create session with chrome driver', async function() {
+    mockery.registerMock('chromedriver', {
+      path: '/path/to/chromedriver'
+    });
+
+    const {session, serverPath, serverPort} = await ChromeDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      webdriver: {
+        port: 9999,
+        start_process: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/chromedriver');
+    assert.strictEqual(serverPort, 9999);
+  });
+
+  it('test create session with chrome driver -- random port', async function() {
+    mockery.registerMock('chromedriver', {
+      path: '/path/to/chromedriver'
+    });
+
+    const {session, serverPath, serverPort} = await ChromeDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      webdriver: {
+        start_process: true
+      }
+    }, {
+      parallel: true
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/chromedriver');
+    assert.strictEqual(serverPort, undefined);
+  });
+
+});
diff --git a/test/src/service-builders/testEdgeDriver.js b/test/src/service-builders/testEdgeDriver.js
new file mode 100644
index 00000000..8d52fade
--- /dev/null
+++ b/test/src/service-builders/testEdgeDriver.js
@@ -0,0 +1,272 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('EdgeDriver Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+
+    const {constants, rmdirSync} = require('fs');
+
+    delete require.cache['fs'];
+
+    mockery.registerMock('fs', {
+      existsSync() {
+        return true;
+      },
+      constants,
+      rmdirSync
+    });
+
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const sessionData = {
+    sessionId: '111',
+    getId() {
+      return '1111'
+    },
+    getCapabilities() {
+      return {
+        getPlatform() {
+          return 'MAC'
+        },
+        getBrowserName() {
+          return 'MicrosoftEdge';
+        },
+        getBrowserVersion() {
+          return '11';
+        },
+        get(name) {
+          return name;
+        },
+        keys() {
+          return new Map();
+        }
+      }
+    }
+  };
+
+  const mockDriver = {
+    async getSession() {
+      return sessionData;
+    },
+    async getExecutor() {
+      return {
+        w3c: true
+      }
+    }
+  }
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function createMockDriverBuilder() {
+    const {Builder} = require('selenium-webdriver');
+    class MockBuilder extends Builder {
+      build() {
+        return Promise.resolve(mockDriver);
+      }
+    }
+
+    return new MockBuilder();
+  }
+
+  function mockServiceBuilder({onConstruct = fn, onAddArguments = fn, onSetPort = fn, onSetHostname = fn, onSetPath = fn}) {
+    const edge = require('selenium-webdriver/edge');
+    const {Options, ServiceBuilder} = edge;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/edge');
+
+    class MockServiceBuilder extends ServiceBuilder {
+      constructor(server_path) {
+        super(server_path);
+
+        onConstruct.call(this, server_path);
+      }
+
+      setPort(port) {
+        onSetPort(port);
+      }
+
+      setHostname(h) {
+        onSetHostname(h);
+      }
+
+      setPath(h) {
+        onSetPath(h);
+      }
+
+      addArguments(args) {
+        onAddArguments(args);
+      }
+
+      enableChromeLogging() {
+        return this;
+      }
+
+      enableVerboseLogging() {
+        return this;
+      }
+
+      build() {
+        return {
+          kill() {
+
+          },
+          async start() {
+            return 'http://localhost'
+          }
+        }
+      }
+    }
+
+    mockery.registerMock('selenium-webdriver/edge', {
+      Options,
+      ServiceBuilder: MockServiceBuilder
+    });
+  }
+
+  async function EdgeDriverTestSetup(useSettings, argv = {}) {
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'EdgeDriver');
+        assert.strictEqual(this.outputFile, '_msedgedriver.log');
+        assert.strictEqual(this.defaultPort, undefined);
+        assert.strictEqual(this.npmPackageName, null);
+        assert.strictEqual(this.serviceDownloadUrl, 'https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/');
+      }
+
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let serverPath;
+    let serverPort;
+    let buildArgs;
+    let logFilePath;
+
+    mockServiceBuilder({
+      onConstruct(server_path) {
+        serverPath = server_path;
+      },
+
+      onAddArguments(args) {
+        buildArgs = args;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      }
+    });
+
+    const Transport = common.require('transport/selenium-webdriver/edge.js');
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, argv);
+    const {transport} = client;
+    //const transport = new Transport(client, 'MicrosoftEdge');
+    transport.createSessionBuilder = function() {
+      return createMockDriverBuilder();
+    };
+
+    const session = await transport.createSession({argv: {}, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      serverPath,
+      serverPort
+    }
+  }
+
+  it('test create session with edge driver -- not found error', async function() {
+    let error;
+    try {
+      await EdgeDriverTestSetup({
+        desiredCapabilities: {
+          browserName: 'MicrosoftEdge'
+        },
+        webdriver: {
+          port: 9999,
+          start_process: true
+        }
+      });
+    } catch (err) {
+      error = err;
+    }
+
+    assert.ok(error instanceof Error);
+    assert.strictEqual(error.message, 'EdgeDriver cannot be found in the current project.');
+  });
+
+
+  it('test create session with edge driver', async function() {
+    const {session, serverPath, serverPort} = await EdgeDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'MicrosoftEdge'
+      },
+      webdriver: {
+        port: 9999,
+        start_process: true,
+        server_path: '/path/to/edgedriver'
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/edgedriver');
+    assert.strictEqual(serverPort, 9999);
+  });
+
+  it('test create session with edge driver -- random port', async function() {
+    const {session, serverPath, serverPort} = await EdgeDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'MicrosoftEdge'
+      },
+      webdriver: {
+        start_process: true,
+        server_path: '/path/to/edgedriver'
+      }
+    }, {parallel: true});
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/edgedriver');
+    assert.strictEqual(serverPort, undefined);
+  });
+
+});
diff --git a/test/src/service-builders/testFirefoxDriver.js b/test/src/service-builders/testFirefoxDriver.js
new file mode 100644
index 00000000..9b7fa605
--- /dev/null
+++ b/test/src/service-builders/testFirefoxDriver.js
@@ -0,0 +1,287 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('GeckoDriver Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+
+    const {constants, rmdirSync} = require('fs');
+
+    delete require.cache['fs'];
+
+    mockery.registerMock('fs', {
+      existsSync() {
+        return true;
+      },
+      constants,
+      rmdirSync
+    });
+
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const sessionData = {
+    sessionId: '111',
+    getId() {
+      return '1111'
+    },
+    getCapabilities() {
+      return {
+        getPlatform() {
+          return 'MAC'
+        },
+        getBrowserName() {
+          return 'firefox';
+        },
+        getBrowserVersion() {
+          return '11';
+        },
+        get(name) {
+          return name;
+        },
+        keys() {
+          return new Map();
+        }
+      }
+    }
+  };
+
+  const mockDriver = {
+    async getSession() {
+      return sessionData;
+    },
+    async getExecutor() {
+      return {
+        w3c: true
+      }
+    }
+  }
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function createMockDriverBuilder() {
+    const {Builder} = require('selenium-webdriver');
+    class MockBuilder extends Builder {
+      build() {
+        return Promise.resolve(mockDriver);
+      }
+    }
+
+    return new MockBuilder();
+  }
+
+  function mockServiceBuilder({onConstruct = fn, onEnableVerboseLogging = fn, onAddArguments = fn, onSetPort = fn, onSetHostname = fn, onSetPath = fn}) {
+    const firefox = require('selenium-webdriver/firefox');
+    const {Options, ServiceBuilder} = firefox;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/firefox');
+
+    class MockServiceBuilder extends ServiceBuilder {
+      constructor(server_path) {
+        super(server_path);
+
+        onConstruct.call(this, server_path);
+      }
+
+      setPort(port) {
+        onSetPort(port);
+      }
+
+      setHostname(h) {
+        onSetHostname(h);
+      }
+
+      setPath(h) {
+        onSetPath(h);
+      }
+
+      addArguments(args) {
+        onAddArguments(args);
+      }
+
+      enableVerboseLogging(value) {
+        onEnableVerboseLogging(value);
+
+        return this;
+      }
+
+      build() {
+        return {
+          kill() {
+
+          },
+          async start() {
+            return 'http://localhost'
+          }
+        }
+      }
+    }
+
+    mockery.registerMock('selenium-webdriver/firefox', {
+      Options,
+      ServiceBuilder: MockServiceBuilder
+    });
+  }
+
+  async function GeckoDriverTestSetup(useSettings, argv = {}) {
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'GeckoDriver');
+        assert.strictEqual(this.outputFile, '_geckodriver.log');
+        assert.strictEqual(this.requiresDriverBinary, true);
+        assert.strictEqual(this.defaultPort, undefined);
+        assert.strictEqual(this.npmPackageName, 'geckodriver');
+        assert.strictEqual(this.serviceDownloadUrl, 'https://github.com/mozilla/geckodriver/releases');
+      }
+
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let serverPath;
+    let serverPort;
+    let buildArgs;
+    let verboseLogging;
+
+    mockServiceBuilder({
+      onConstruct(server_path) {
+        serverPath = server_path;
+      },
+
+      onAddArguments(args) {
+        buildArgs = args;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      },
+
+      onEnableVerboseLogging(v) {
+        verboseLogging = v;
+      }
+    });
+
+    const Transport = common.require('transport/selenium-webdriver/firefox.js');
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, argv);
+    const {transport} = client;
+    transport.createSessionBuilder = function() {
+      return createMockDriverBuilder();
+    };
+
+    const session = await transport.createSession({argv: {}, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      serverPath,
+      serverPort,
+      verboseLogging
+    }
+  }
+
+  it('test create session with firefox driver -- not found error', async function() {
+    let error;
+    mockery.registerMock('geckodriver', {
+      path: ''
+    });
+    try {
+      await GeckoDriverTestSetup({
+        desiredCapabilities: {
+          browserName: 'firefox'
+        },
+        webdriver: {
+          port: 9999,
+          start_process: true
+        }
+      });
+    } catch (err) {
+      error = err;
+    }
+
+    assert.ok(error instanceof Error);
+    assert.strictEqual(error.message, 'GeckoDriver cannot be found in the current project.');
+  });
+
+
+  it('test create session with firefox driver', async function() {
+    mockery.registerMock('geckodriver', {
+      path: '/path/to/geckodriver'
+    });
+
+    const {session, serverPath, serverPort, verboseLogging} = await GeckoDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'firefox'
+      },
+      webdriver: {
+        port: 9999,
+        start_process: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/geckodriver');
+    assert.strictEqual(serverPort, 9999);
+    assert.strictEqual(verboseLogging, true);
+  });
+
+  it('test create session with gecko driver -- random port', async function() {
+    mockery.registerMock('geckodriver', {
+      path: '/path/to/geckodriver'
+    });
+
+    const {session, serverPath, serverPort} = await GeckoDriverTestSetup({
+      desiredCapabilities: {
+        browserName: 'firefox'
+      },
+      webdriver: {
+        start_process: true
+      }
+    }, {
+      parallel: true
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/geckodriver');
+    assert.strictEqual(serverPort, undefined);
+  });
+
+});
diff --git a/test/src/service-builders/testSafariDriver.js b/test/src/service-builders/testSafariDriver.js
new file mode 100644
index 00000000..874c36be
--- /dev/null
+++ b/test/src/service-builders/testSafariDriver.js
@@ -0,0 +1,246 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('SafariDriver Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function mockExecutor(results) {
+    const {Executor, HttpClient} = require('selenium-webdriver/http');
+    class MockExecutor extends Executor {
+      async execute(command) {
+        return Promise.resolve(results);
+      }
+    }
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/http');
+
+    mockery.registerMock('selenium-webdriver/http', {
+      Executor: MockExecutor,
+      HttpClient
+    });
+  }
+
+
+  function mockServiceBuilder({onConstruct = fn, onAddArguments = fn, onSetPort = fn, onSetHostname = fn, onSetPath = fn}) {
+    const safari = require('selenium-webdriver/safari');
+    const {Options, ServiceBuilder} = safari;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/safari');
+
+    class MockServiceBuilder /*extends ServiceBuilder*/ {
+      constructor(server_path) {
+        onConstruct.call(this, server_path);
+      }
+
+      setPort(port) {
+        onSetPort(port);
+      }
+
+      setHostname(h) {
+        onSetHostname(h);
+      }
+
+      setPath(h) {
+        onSetPath(h);
+      }
+
+      addArguments(args) {
+        onAddArguments(args);
+      }
+
+      build() {
+        return {
+          kill() {
+
+          },
+          async start() {
+            return 'http://localhost'
+          }
+        }
+      }
+    }
+
+    mockery.registerMock('selenium-webdriver/safari', {
+      Options,
+      ServiceBuilder: MockServiceBuilder
+    });
+  }
+
+  async function SafariTestSetup(useSettings, {onLogFile = fn} = {}) {
+    mockExecutor({
+      sessionId: '111',
+      getId() {
+        return '1111'
+      },
+      getCapabilities() {
+        return {
+          getPlatform() {
+            return 'MAC'
+          },
+          getBrowserName() {
+            return 'safari';
+          },
+          getBrowserVersion() {
+            return '11';
+          },
+          get(name) {
+            return name;
+          },
+          keys() {
+            return new Map();
+          }
+        }
+      }
+    });
+
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'SafariDriver');
+        assert.strictEqual(this.outputFile, '_safaridriver.log');
+        assert.strictEqual(this.requiresDriverBinary, false);
+        assert.strictEqual(this.defaultPort, 0);
+      }
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+        const logPath = this.getLogPath();
+        if (!logPath) {
+          return true;
+        }
+
+        const filePath = this.getOutputFilePath();
+        onLogFile(filePath);
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let serverPath;
+    let serverPort;
+    let buildArgs;
+    let logFilePath;
+
+    mockServiceBuilder({
+      onConstruct(server_path) {
+        serverPath = server_path;
+      },
+
+      onAddArguments(args) {
+        buildArgs = args;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      }
+    });
+
+    const Transport = common.require('transport/selenium-webdriver/safari.js');
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, {});
+    const transport = new Transport(client, {browserName: 'safari'});
+
+    const session = await transport.createSession({argv: {}, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      serverPath,
+      serverPort
+    }
+  }
+
+  it('test create session with safari driver', async function() {
+    let logFilePath;
+    const {session, serverPath, serverPort} = await SafariTestSetup({
+      desiredCapabilities: {
+        browserName: 'safari'
+      },
+      webdriver: {
+        port: 9999,
+        start_process: true
+      }
+    }, {
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/usr/bin/safaridriver');
+    assert.strictEqual(serverPort, 9999);
+    assert.ok(logFilePath.endsWith('logs/testModuleKey_safaridriver.log'), logFilePath);
+  });
+
+  it('test create session with safari driver -- random port', async function() {
+    const {session, serverPath, serverPort} = await SafariTestSetup({
+      desiredCapabilities: {
+        browserName: 'safari'
+      },
+      webdriver: {
+        start_process: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/usr/bin/safaridriver');
+    assert.strictEqual(serverPort, undefined);
+  });
+
+  it('test create session with safari driver using technologyPreview', async function() {
+    const {session, serverPath} = await SafariTestSetup({
+      desiredCapabilities: {
+        browserName: 'safari',
+        'safari.options': {
+          technologyPreview: true
+        }
+      },
+      webdriver: {
+        port: 9999,
+        start_process: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver');
+  });
+});
diff --git a/test/src/service-builders/testSeleniumServer.js b/test/src/service-builders/testSeleniumServer.js
new file mode 100644
index 00000000..d047e59a
--- /dev/null
+++ b/test/src/service-builders/testSeleniumServer.js
@@ -0,0 +1,376 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('SeleniumServer Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const sessionData = {
+    sessionId: '111',
+    getId() {
+      return '1111'
+    },
+    getCapabilities() {
+      return {
+        getPlatform() {
+          return 'MAC'
+        },
+        getBrowserName() {
+          return 'chrome';
+        },
+        getBrowserVersion() {
+          return '11';
+        },
+        get(name) {
+          return name;
+        },
+        keys() {
+          return new Map();
+        }
+      }
+    }
+  };
+
+  const mockDriver = {
+    async getSession() {
+      return sessionData;
+    },
+    async getExecutor() {
+      return {
+        w3c: true
+      }
+    }
+  }
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function mockExecutor(results) {
+    const {Executor, HttpClient} = require('selenium-webdriver/http');
+    class MockExecutor extends Executor {
+      async execute(command) {
+        return Promise.resolve(results);
+      }
+    }
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/http');
+
+    mockery.registerMock('selenium-webdriver/http', {
+      Executor: MockExecutor,
+      HttpClient
+    });
+  }
+
+  function createMockDriverBuilder() {
+    const {Builder} = require('selenium-webdriver');
+    class MockBuilder extends Builder {
+      build() {
+        return Promise.resolve(mockDriver);
+      }
+    }
+
+    return new MockBuilder();
+  }
+
+
+  function mockServiceBuilder({onConstruct = fn, onOpts = fn, onSetPort = fn}) {
+    const remote = require('selenium-webdriver/remote');
+    const {Options, SeleniumServer, DriverService} = remote;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/remote');
+
+    class MockSeleniumServer {
+      constructor(jar, opts) {
+        onConstruct.call(this, jar);
+        if (opts.port) {
+          onSetPort(opts.port);
+        }
+        onOpts(opts);
+      }
+
+      async start() {
+        return {}
+      }
+    }
+
+    class MockDriverService {
+      constructor(command, opts) {
+        onConstruct.call(this, command);
+
+        if (opts.port) {
+          onSetPort(opts.port);
+        }
+
+        onOpts(opts);
+      }
+
+      async start() {
+        return {}
+      }
+    }
+
+    MockSeleniumServer.Options = SeleniumServer.Options;
+
+    mockery.registerMock('selenium-webdriver/remote', {
+      Options,
+      SeleniumServer: MockSeleniumServer,
+      DriverService: MockDriverService
+    });
+  }
+
+  async function SeleniumServerTestSetup(useSettings, {selenium4 = false, onLogFile = fn} = {}) {
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'Selenium Server');
+        assert.strictEqual(this.outputFile, '_selenium-server.log');
+        assert.strictEqual(this.defaultPort, 4444);
+        assert.strictEqual(this.usingSelenium4(), selenium4);
+        assert.strictEqual(this.npmPackageName, '@nightwatch/selenium-server');
+        assert.strictEqual(this.serviceDownloadUrl, 'https://selenium.dev/download/');
+        assert.strictEqual(this.downloadMessage, 'download the selenium server jar from https://selenium.dev/download/, \n and set "selenium.server_path" config option to point to the jar file.');
+      }
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+        this.process = {};
+        const logPath = this.getLogPath();
+        if (!logPath) {
+          return true;
+        }
+
+        const filePath = this.getOutputFilePath();
+        onLogFile(filePath);
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let serverPath;
+    let serverPort;
+    let buildArgs;
+    let logFilePath;
+    let options;
+
+    mockServiceBuilder({
+      onConstruct(server_path) {
+        serverPath = server_path;
+      },
+
+      onAddArguments(args) {
+        buildArgs = args;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      },
+
+      onOpts(o) {
+        options = o;
+      }
+    });
+
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, {});
+    const {transport} = client;
+    transport.createSessionBuilder = function() {
+      return createMockDriverBuilder();
+    };
+
+    const session = await transport.createSession({argv: {}, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      serverPath,
+      serverPort,
+      options
+    }
+  }
+
+  it('test create session with selenium server 3', async function() {
+    mockery.registerMock('geckodriver', {
+      path: ''
+    });
+
+    mockery.registerMock('chromedriver', {
+      path: ''
+    });
+
+    mockery.registerMock('@nightwatch/selenium-server', {
+      path: '/path/to/selenium-server-standalone.3.0.jar'
+    });
+
+    let logFilePath;
+    const {session, serverPath, serverPort, options} = await SeleniumServerTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      selenium: {
+        port: 9999,
+        start_process: true
+      }
+    }, {
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/selenium-server-standalone.3.0.jar');
+    assert.strictEqual(serverPort, 9999);
+    assert.strictEqual(options.port, 9999);
+    assert.deepStrictEqual(options.args, []);
+    assert.deepStrictEqual(options.jvmArgs, []);
+    assert.deepStrictEqual(options.stdio, ['pipe', undefined, undefined]);
+    assert.ok(logFilePath.endsWith('testModuleKey_selenium-server.log'));
+  });
+
+  it('test create session with selenium server 3 -- with drivers', async function() {
+    mockery.registerMock('geckodriver', {
+      path: '/path/to/geckodriver'
+    });
+
+    mockery.registerMock('chromedriver', {
+      path: '/path/to/chromedriver'
+    });
+
+    mockery.registerMock('@nightwatch/selenium-server', {
+      path: '/path/to/selenium-server-standalone.3.0.jar'
+    });
+
+    let logFilePath;
+    const {session, serverPath, serverPort, options} = await SeleniumServerTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      selenium: {
+        port: 9999,
+        start_process: true,
+        cli_args: {
+          '-timeout': 1000
+        }
+      }
+    }, {
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.strictEqual(serverPath, '/path/to/selenium-server-standalone.3.0.jar');
+    assert.deepStrictEqual(options.args, ['-timeout', 1000]);
+    assert.deepStrictEqual(options.jvmArgs,  [
+      '-Dwebdriver.gecko.driver=/path/to/geckodriver',
+      '-Dwebdriver.chrome.driver=/path/to/chromedriver'
+    ]);
+    assert.deepStrictEqual(options.stdio, ['pipe', undefined, undefined]);
+    assert.ok(logFilePath.endsWith('testModuleKey_selenium-server.log'));
+  });
+
+  it('test create session with selenium server 3 -- random port', async function() {
+    mockery.registerMock('@nightwatch/selenium-server', {
+      path: '/path/to/selenium-server-standalone.3.0.jar'
+    });
+
+    const {session, serverPath, serverPort} = await SeleniumServerTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      selenium: {
+        port: null,
+        start_process: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(serverPath, '/path/to/selenium-server-standalone.3.0.jar');
+    assert.ok(!!serverPort);
+  });
+
+  it('test create session with selenium server 4 -- and no drivers', async function() {
+    mockery.registerMock('geckodriver', {
+      path: ''
+    });
+
+    mockery.registerMock('chromedriver', {
+      path: ''
+    });
+
+    mockery.registerMock('@nightwatch/selenium-server', {
+      path: '/path/to/selenium-server.4.0.jar'
+    });
+
+    let logFilePath;
+    const {session, serverPath, serverPort, options} = await SeleniumServerTestSetup({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      selenium: {
+        port: 9999,
+        command: 'standalone',
+        start_process: true,
+        cli_args: {
+          '--allow-cors': true
+        }
+      }
+    }, {
+      selenium4: true,
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+
+    assert.strictEqual(serverPort, 9999);
+    assert.strictEqual(options.port, 9999);
+    assert.strictEqual(options.path, '/wd/hub');
+
+    assert.deepStrictEqual(options.args, [
+      '-jar',
+      '/path/to/selenium-server.4.0.jar',
+      'standalone',
+      '--port',
+      9999,
+      '--allow-cors',
+      true
+    ]);
+
+    assert.deepStrictEqual(options.stdio, ['pipe', undefined, undefined]);
+  });
+});