Repository: nightwatchjs/nightwatch
Pull request #3364 (2022-09-01 07:26:35)

Execute tests in parallel by default
closes  #3331


harshit-bs: @swrdfish It would be great to update the description or issue to check what is completed. Since I can't see the code for below requirement.

- the number of parallels mentioned are more than the CPU cores, a warning message should be shown

swrdfish: @harshit-bs made the necessary changes.



Patch:
diff --git a/bin/runner.js b/bin/runner.js
index 427b7761..5823bff6 100644
--- a/bin/runner.js
+++ b/bin/runner.js
@@ -3,31 +3,16 @@
  */
 const Nightwatch = require('../lib/index.js');
 const {Logger, shouldReplaceStack, alwaysDisplayError} = require('../lib/utils');
-const path = require('path');
 
 try {
-  const projectTsFile = path.join(process.cwd(), 'nightwatch', 'tsconfig.json');
-  let projectTsExists = false;
-  try {
-    require(projectTsFile);
-    projectTsExists = true;
-
-    require('ts-node').register({
-      esm: false,
-      project: projectTsFile
-    });
-  } catch (err) {
-    // eslint-disable-line
-  }
-
-  Nightwatch.cli(function(argv) {
+  Nightwatch.cli(function (argv) {
     argv._source = argv['_'].slice(0);
 
     const runner = Nightwatch.CliRunner(argv);
 
     return runner
       .setupAsync()
-      .catch(err => {
+      .catch((err) => {
         if (err.code === 'ERR_REQUIRE_ESM') {
           err.showTrace = false;
         }
@@ -36,10 +21,10 @@ try {
       })
       .then(() => runner.runTests())
       .catch((err) => {
-        if (!err.displayed || alwaysDisplayError(err) && !err.displayed) {
+        if (!err.displayed || (alwaysDisplayError(err) && !err.displayed)) {
           Logger.error(err);
         }
-      
+
         runner.processListener.setExitCode(10).exit();
       });
   });
diff --git a/lib/api/_loaders/command.js b/lib/api/_loaders/command.js
index f362362b..60a55636 100644
--- a/lib/api/_loaders/command.js
+++ b/lib/api/_loaders/command.js
@@ -84,6 +84,10 @@ class CommandLoader extends BaseCommandLoader {
         return nightwatchInstance.api;
       }
 
+      get reuseBrowser() {
+        return nightwatchInstance.argv['reuse-browser'] || (nightwatchInstance.settings.globals && nightwatchInstance.settings.globals.reuseBrowserSession);
+      }      
+      
       get isES6AsyncCommand() {
         return Utils.isES6AsyncFn(
           CommandLoader.isDeprecatedCommandStyle(CommandModule) ? CommandModule.command: this.command
diff --git a/lib/api/client-commands/end.js b/lib/api/client-commands/end.js
index 987c6663..5c35382d 100644
--- a/lib/api/client-commands/end.js
+++ b/lib/api/client-commands/end.js
@@ -15,10 +15,16 @@ const EventEmitter = require('events');
  * @api protocol.sessions
  */
 class End extends EventEmitter {
-  command(callback) {
+  command(forceEnd = !this.reuseBrowser, callback) {
+
+    if (arguments.length === 1 && typeof arguments[0] === 'function') {
+      callback = forceEnd;
+      forceEnd = !this.reuseBrowser;
+    }
+
     const client = this.client;
 
-    if (this.api.sessionId) {
+    if (this.api.sessionId && forceEnd) {
       this.api.session('delete', result => {
         client.sessionId = null;
         client.setApiProperty('sessionId', null);
diff --git a/lib/api/protocol/moveTo.js b/lib/api/protocol/moveTo.js
index 17f3c554..cb2015d5 100644
--- a/lib/api/protocol/moveTo.js
+++ b/lib/api/protocol/moveTo.js
@@ -1,3 +1,4 @@
+const {Origin} = require('selenium-webdriver');
 const ProtocolAction = require('./_base-action.js');
 
 /**
@@ -19,8 +20,28 @@ const ProtocolAction = require('./_base-action.js');
  * @param {function} [callback] Optional callback function to be called when the command finishes.
  * @api protocol.useractions
  */
-module.exports = class Session extends ProtocolAction {
-  command(webElementId, xoffset, yoffset, callback) {
+module.exports = class Command extends ProtocolAction {
+
+  command(webElementId, xoffset, yoffset, callback = function() {}) {
+   
+    if (arguments.length >= 3 && typeof arguments[0] === 'string' || arguments[0] === null) {
+      if (webElementId === null) {
+        webElementId = Origin.POINTER;
+      } else {
+        ProtocolAction.validateElementId(webElementId);
+      }
+     
+    } else if (arguments.length >= 2 && typeof arguments[0] === 'number') {
+      webElementId = Origin.POINTER;
+      xoffset = arguments[0];
+      yoffset = arguments[1];
+      callback = arguments.length === 3 ? arguments[2] : callback;
+      
+    } else {
+      throw new Error('Invalid Parameters for moveTo Command');
+    }
+
     return this.transportActions.moveTo(webElementId, xoffset, yoffset, callback);
+    
   }
 };
diff --git a/lib/core/asynctree.js b/lib/core/asynctree.js
index 639457ac..1f0f846b 100644
--- a/lib/core/asynctree.js
+++ b/lib/core/asynctree.js
@@ -179,7 +179,7 @@ class AsyncTree extends EventEmitter{
         node.context.emit('complete');
       }
 
-      const isAssertion = node.parent && node.parent.namespace === 'assert';
+      const isAssertion = node.parent && (node.parent.namespace === 'assert' || node.parent.namespace === 'verify');
       if (node.parent && !node.parent.isRootNode && !isAssertion && !stillInProgress) {
         node.done = true;
         this.resolveNode(node.parent, result, ++times);
diff --git a/lib/runner/cli/argv-setup.js b/lib/runner/cli/argv-setup.js
index 90f76adb..01dbf19e 100644
--- a/lib/runner/cli/argv-setup.js
+++ b/lib/runner/cli/argv-setup.js
@@ -406,8 +406,18 @@ module.exports = new (function () {
       });
 
       // $ nightwatch --parallel
-      this.option('parallel', {
-        description: 'Enable running the tests in parallel mode, via test workers; can also specify de desired workers count (e.g. --parallel=4)'
+      // this.option('parallel', {
+      //   description: 'Enable running the tests in parallel mode, via test workers.'
+      // });
+
+      // $ nightwatch --workers=5
+      this.option('workers', {
+        description: 'Max number of test files running at the same time (default: CPU cores; e.g. workers=4)'
+      });
+
+      // $ nightwatch --sequential
+      this.option('serial', {
+        description: 'Executes tests serially (disables parallel mode)'
       });
 
       // $ nightwatch --headless
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index b42ad4b3..4d088676 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -18,6 +18,10 @@ class CliRunner {
     return './nightwatch.conf.cjs';
   }
 
+  static get CONFIG_FILE_TS() {
+    return './nightwatch.conf.ts';
+  }
+ 
   static createDefaultConfig(destFileName) {
     // eslint-disable-next-line no-console
     console.log(Logger.colors.cyan('No config file found in the current working directory, creating nightwatch.conf.js in the current folder...'));
@@ -129,13 +133,13 @@ class CliRunner {
   }
 
   setCurrentTestEnv() {
-    if (!this.argv.env) {
-      return this;
-    }
-
     this.testEnv = Utils.isString(this.argv.env) ? this.argv.env : Settings.DEFAULT_ENV;
     this.testEnvArray = this.testEnv.split(',');
 
+    if (!this.baseSettings) {
+      return this;
+    }  
+
     this.availableTestEnvs = Object.keys(this.baseSettings.test_settings).filter(key => {
       return Utils.isObject(this.baseSettings.test_settings[key]);
     });
@@ -181,6 +185,7 @@ class CliRunner {
 
     this.createTestRunner();
     this.setupConcurrency();
+    this.loadTypescriptTranspiler();
 
     analyticsCollector.updateSettings(this.test_settings);
     analyticsCollector.updateLogger(Logger);
@@ -198,6 +203,17 @@ class CliRunner {
     return this;
   }
 
+  loadTypescriptTranspiler() {
+    const isTypescriptProject = Utils.fileExistsSync(path.join(process.cwd(), 'tsconfig.json'));
+    if (isTypescriptProject) {
+      Logger.info('Now you can run TS tests directly using Nightwatch.');
+    }
+    const projectTsFilePath = Utils.findTSConfigFile(this.test_settings.tsconfig_path);
+    if (projectTsFilePath !== '' && !this.test_settings.disable_typescript) {
+      Utils.loadTSNode(projectTsFilePath);
+    }
+  }
+
   isConfigDefault(configFile, localJsValue = CliRunner.CONFIG_FILE_JS) {
     return ArgvSetup.isDefault('config', configFile) || path.resolve(configFile) === localJsValue;
   }
@@ -213,7 +229,16 @@ class CliRunner {
     packageInfo = packageInfo || {};
     const usingESM = packageInfo.type === 'module';
 
-    return path.resolve(usingESM ? CliRunner.CONFIG_FILE_CJS : CliRunner.CONFIG_FILE_JS);
+    const usingTS = Utils.fileExistsSync(CliRunner.CONFIG_FILE_TS);
+
+    if (usingESM) {
+      return path.resolve(CliRunner.CONFIG_FILE_CJS);
+    }
+    if (usingTS) {
+      return path.resolve(CliRunner.CONFIG_FILE_TS);
+    }
+ 
+    return path.resolve(CliRunner.CONFIG_FILE_JS);
   }
 
   loadConfig() {
@@ -221,21 +246,21 @@ class CliRunner {
       return null;
     }
 
-    const localJsValue = this.getLocalConfigFileName();
+    const localJsOrTsValue = this.getLocalConfigFileName();
 
     // use default nightwatch.json file if we haven't received another value
-    if (this.isConfigDefault(this.argv.config, localJsValue)) {
+    if (this.isConfigDefault(this.argv.config, localJsOrTsValue)) {
       let newConfigCreated = false;
       const defaultValue = ArgvSetup.getDefault('config');
-      const hasJsConfig = Utils.fileExistsSync(localJsValue);
+      const hasJsOrTsConfig = Utils.fileExistsSync(localJsOrTsValue);
       const hasJsonConfig = Utils.fileExistsSync(defaultValue);
 
-      if (!hasJsConfig && !hasJsonConfig) {
-        newConfigCreated = CliRunner.createDefaultConfig(localJsValue);
+      if (!hasJsOrTsConfig && !hasJsonConfig) {
+        newConfigCreated = CliRunner.createDefaultConfig(localJsOrTsValue);
       }
 
-      if (hasJsConfig || newConfigCreated) {
-        this.argv.config = localJsValue;
+      if (hasJsOrTsConfig || newConfigCreated) {
+        this.argv.config = localJsOrTsValue;
       } else if (hasJsonConfig) {
         this.argv.config = path.join(path.resolve('./'), this.argv.config);
       }
@@ -263,6 +288,7 @@ class CliRunner {
    * @returns {CliRunner}
    */
   parseTestSettings(settings = {}) {
+    this.userSettings = settings;
     this.initTestSettings(settings, this.baseSettings, this.argv, this.testEnv);
 
     return this;
@@ -278,6 +304,9 @@ class CliRunner {
 
   validateTestEnvironments() {
     for (let i = 0; i < this.testEnvArray.length; i++) {
+      if (this.testEnvArray[i] === 'default') {
+        continue;
+      }
       if (!(this.testEnvArray[i] in this.baseSettings.test_settings)) {
         const error = new Error(`Invalid testing environment specified: ${this.testEnvArray[i]}. \n\n ${Logger.colors.light_cyan('Available environments are:')}\n ${Logger.inspectObject(this.availableTestEnvs)}`);
         error.showTrace = false;
@@ -299,11 +328,7 @@ class CliRunner {
     return this.test_settings.testWorkersEnabled && !singleSourceFile(this.argv);
   }
 
-  parallelMode(modules = null) {
-    if (this.testWorkersMode && Array.isArray(modules) && modules.length <= 1) {
-      return false;
-    }
-
+  parallelMode() {
     return this.testEnvArray.length > 1 || this.testWorkersMode;
   }
 
@@ -313,8 +338,8 @@ class CliRunner {
     return this;
   }
 
-  isConcurrencyEnabled(modules) {
-    return this.testRunner.supportsConcurrency && this.parallelMode(modules);
+  isConcurrencyEnabled() {
+    return this.testRunner.supportsConcurrency && this.parallelMode();
   }
 
   ///////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -336,8 +361,14 @@ class CliRunner {
     return this;
   }
 
-  getTestsFiles() {
-    return Runner.readTestSource(this.test_settings, this.argv);
+  async getTestsFiles() {
+    const modules = {};
+    for (const env of this.testEnvArray) {
+      const envSettings = Settings.parse(this.userSettings, this.baseSettings, this.argv, env);
+      modules[env] = await Runner.readTestSource(envSettings, this.argv);
+    }
+
+    return modules;
   }
 
   /**
@@ -369,7 +400,7 @@ class CliRunner {
         }
 
         return promise.then(() => {
-          if (this.isConcurrencyEnabled(modules)) {
+          if (this.isConcurrencyEnabled()) {
             return this.testRunner.runConcurrent(this.testEnvArray, modules)
               .then(exitCode => {
                 if (exitCode > 0) {
@@ -378,7 +409,7 @@ class CliRunner {
               });
           }
 
-          return this.executeTestRunner(modules);
+          return this.executeTestRunner(modules[this.testEnv]);
         });
       })
       .catch(err => {
diff --git a/lib/runner/cli/nightwatch.conf.ejs b/lib/runner/cli/nightwatch.conf.ejs
index ea52855c..4f3501a3 100644
--- a/lib/runner/cli/nightwatch.conf.ejs
+++ b/lib/runner/cli/nightwatch.conf.ejs
@@ -33,6 +33,11 @@ module.exports = {
   globals_path : '',
 
   webdriver: {},
+  
+  test_workers: {
+    enabled: true,
+    workers: 'auto'
+  },
 
   test_settings: {
     default: {
@@ -162,7 +167,7 @@ module.exports = {
           // auto_start_session: true
 
           // use parallel execution in Cucumber
-          // parallel: 2 // set number of workers to use (can also be defined in the cli as --parallel 2
+          // workers: 2 // set number of workers to use (can also be defined in the cli as --workers=2
         }
       }
     },
diff --git a/lib/runner/concurrency/index.js b/lib/runner/concurrency/index.js
index 3c86cb5a..df564f36 100644
--- a/lib/runner/concurrency/index.js
+++ b/lib/runner/concurrency/index.js
@@ -2,13 +2,15 @@ const EventEmitter = require('events');
 const Utils = require('../../utils');
 const ChildProcess = require('./child-process.js');
 const {isObject, isNumber} = Utils;
+const lodashClone = require('lodash.clone');
+const {Logger} = Utils;
 
 class Concurrency extends EventEmitter {
   constructor(settings = {}, argv = {}) {
     super();
 
     this.argv = argv;
-    this.settings = settings;
+    this.settings = lodashClone(settings, true);
     this.childProcessOutput = {};
     this.globalExitCode = 0;
   }
@@ -19,7 +21,7 @@ class Concurrency extends EventEmitter {
 
     for (let i = 2; i < process.argv.length; i++) {
       arg = process.argv[i];
-      if (envs.length > 1 && (arg === '-e' || arg === '--env')) {
+      if (arg === '-e' || arg === '--env') {
         i++;
       } else {
         childProcessArgs.push(arg);
@@ -52,7 +54,28 @@ class Concurrency extends EventEmitter {
     let args = Concurrency.getChildProcessArgs(envs);
 
     if (this.settings.testWorkersEnabled) {
-      return this.runMultipleTestWorkers(modules, args, availColors);
+      const jobs = [];
+      if (envs.length > 0) {
+        envs.forEach((env) => {
+          const envModules = modules[env];
+          const jobList = envModules.map((module) => {
+            return {
+              env,
+              module
+            };
+          });
+          jobs.push(...jobList);
+        });
+      } else {
+        const jobList = modules.map((module) => {
+          return {
+            module
+          };
+        });
+        jobs.push(...jobList);
+      }
+
+      return this.runMultipleTestWorkers(jobs, args, availColors);
     }
 
     return this.runTestEnvironments(envs, args, availColors);
@@ -83,16 +106,24 @@ class Concurrency extends EventEmitter {
     let maxWorkerCount = this.getTestWorkersCount();
     let remaining = modules.length;
 
+    maxWorkerCount = Math.min(maxWorkerCount, remaining);
+
     if (this.settings.output) {
-      // eslint-disable-next-line no-console
-      console.log(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`);
+      Logger.info(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`);
     }
 
 
     return new Promise((resolve, reject) => {
-      Concurrency.buildProcessQueue(maxWorkerCount, modules, (modulePath, index, next) => {
+      Concurrency.buildProcessQueue(maxWorkerCount, modules, (env, modulePath, index, next) => {
         let outputLabel = Utils.getModuleKey(modulePath, this.settings.src_folders, modules);
-        let childProcess = this.createChildProcess(outputLabel, args, ['--test', modulePath, '--test-worker'], index);
+        const flags = ['--test', modulePath, '--test-worker'];
+
+        if (env) {
+          flags.unshift('--env', env);
+          outputLabel = `${env}: ${outputLabel}`;
+        }
+
+        const childProcess = this.createChildProcess(outputLabel, args, flags, index);
 
         return this.runChildProcess(childProcess, outputLabel, availColors, 'workers')
           .then(_ => {
@@ -183,6 +214,10 @@ class Concurrency extends EventEmitter {
     let workers = require('os').cpus().length;
 
     if (isObject(test_workers) && isNumber(test_workers.workers)) {
+      if (workers < test_workers) {
+        // eslint-disable-next-line no-console
+        Logger.warn(`Number of max workers is set to ${test_workers.workers} while the number of cpu cores is ${workers}. This can cause performance issues...\n`);
+      }
       workers = test_workers.workers;
     }
 
@@ -194,9 +229,9 @@ class Concurrency extends EventEmitter {
    * @param {Array} modules
    * @param {function} fn
    */
-  static buildProcessQueue(concurrency, modules, fn) {
-    let maxWorkers = Math.min(concurrency, modules.length);
-    let queue = modules.slice(0);
+  static buildProcessQueue(maxWorkers, modules, fn) {
+    const queue = modules;
+    
     let workers = 0;
     let index = 0;
 
@@ -211,7 +246,7 @@ class Concurrency extends EventEmitter {
 
         if (queue.length) {
           let item = queue.shift();
-          fn(item, index++, next);
+          fn(item.env, item.module, index++, next);
         } else {
           done();
         }
diff --git a/lib/settings/defaults.js b/lib/settings/defaults.js
index 930cdbb1..5548f12c 100644
--- a/lib/settings/defaults.js
+++ b/lib/settings/defaults.js
@@ -35,6 +35,9 @@ module.exports = {
   //  property globals on the main client instance.
   globals_path: null,
 
+  // Location of the existing TypeScript config file. If not provided Nightwatch will search for "nightwatch/tsconfig.json" or "tsconfig.nightwatch.json".
+  tsconfig_path: '',
+
   // An object which will be made available on the main test api, throughout the test execution
   globals: {
     
diff --git a/lib/settings/settings.js b/lib/settings/settings.js
index 5ed7f3b8..e68dc030 100644
--- a/lib/settings/settings.js
+++ b/lib/settings/settings.js
@@ -103,6 +103,12 @@ class Settings {
   get testWorkersEnabled() {
     const {test_workers} = this.settings;
 
+    if (this.argv.parallel === false || this.argv.parallel === 'false' 
+        || this.argv.serial === true 
+        || isNumber(this.argv.workers) && this.argv.workers === 1) {
+      return false;
+    }
+
     return this.argv.parallel === true || test_workers === true || isObject(test_workers) && test_workers.enabled;
   }
 
@@ -176,7 +182,7 @@ class Settings {
    * @returns {Settings} for chaining
    */
   setWebdriverHttpOption(newSetting, oldSetting, opts = {}) {
-    let webdriverOpts = this.settings.webdriver;
+    const webdriverOpts = this.settings.webdriver;
 
     if (this.isSettingsDefined(newSetting)) {
       return this;
@@ -189,7 +195,7 @@ class Settings {
     }
 
     for (let i = 0; i < oldSetting.length; i++) {
-      let item = oldSetting[i];
+      const item = oldSetting[i];
       if (isDefined(this.settings[item])) {
         webdriverOpts[newSetting] = this.settings[item];
 
@@ -308,7 +314,15 @@ class Settings {
         };
       }
 
-      this.settings.test_workers.workers = this.argv.parallel;
+      this.settings.test_workers.workers = this.argv.parallel || this.settings.test_workers.workers;
+    } else if (isNumber(this.argv.workers)) {
+      if (!isObject(this.settings.test_workers)) {
+        this.settings.test_workers = {
+          enabled: true
+        };
+      }
+
+      this.settings.test_workers.workers = this.argv.workers || this.settings.test_workers.workers;
     }
 
     this.settings.testWorkersEnabled = this.testWorkersEnabled;
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index a1b9b585..e796cfc3 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -293,7 +293,7 @@ class Transport extends BaseTransport {
 
     } catch (err) {
       const error = this.handleConnectError(err);
-      this.showConnectSpinner(colors.red(`Failed to connect to ${this.serviceName} on ${host} with ${portStr}.`), 'warn');
+      this.showConnectSpinner(colors.red(`Failed to connect to ${this.serviceName} on ${host} with ${colors.stack_trace(portStr)}.`), 'warn');
 
       throw error;
     }
@@ -303,7 +303,7 @@ class Transport extends BaseTransport {
     const {sessionInfo, sessionId, capabilities, elementKey} = sessionExports;
 
     this.__elementKey = elementKey;
-    await this.showConnectInfo({startTime, host, port, start_process, sessionInfo});
+    await this.showConnectInfo({startTime, host, portStr, start_process, sessionInfo});
 
     return {
       sessionId,
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index 83efe391..3292b92e 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -792,6 +792,7 @@ module.exports = class MethodMappings {
         async moveTo(origin, x, y, duration, options = {}) {
           switch (origin) {
             case Origin.POINTER:
+              break;
             case Origin.VIEWPORT:
               break;
 
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 67ff1e42..73142f49 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -330,7 +330,7 @@ class Utils {
       if (glob.hasMagic(source)) {
         return resolve();
       }
-      
+
       fs[followSymlinks ? 'stat' : 'lstat'](source, function(err, stats) {
         if (err) {
           return reject(err.code === 'ENOENT' && originalErr || err);
@@ -361,7 +361,7 @@ class Utils {
         }
         resolve(list);
       };
-      
+
       glob.hasMagic(source) ? glob(source, callback) : fs.readdir(source, callback);
     });
   }
@@ -540,6 +540,45 @@ class Utils {
   static shouldReplaceStack(err) {
     return !alwaysDisplayError(err);
   }
+
+  static findTSConfigFile(existingTSConfig) {
+    const projectTsFileLocation1 = path.join(process.cwd(), 'nightwatch', 'tsconfig.json');
+    const projectTsFileLocation2 = path.join(process.cwd(), 'tsconfig.nightwatch.json');
+
+    if (Utils.fileExistsSync(existingTSConfig)) {
+      return existingTSConfig;
+    }
+
+    if (Utils.fileExistsSync(projectTsFileLocation1)) {
+      return projectTsFileLocation1;
+    } 
+    if (Utils.fileExistsSync(projectTsFileLocation2)) {
+      return projectTsFileLocation2;
+    }
+
+    return '';
+  }
+
+  static loadTSNode(projectTsFile) {
+    try {
+      require('ts-node').register({
+        esm: false,
+        project: projectTsFile
+      });
+    } catch (err) {
+      if (err.code === 'MODULE_NOT_FOUND') {
+        const error = new Error(`ts-node needs to be installed as a project dependency. You can install ts-node from NPM using:\n\n        ${Logger.colors.light_green('npm i ts-node --save-dev')}`);
+        error.showTrace = false;
+        error.displayed = false;
+
+        throw error;
+      }
+
+      err.showTrace = false;
+
+      throw err;
+    }
+  }
 }
 
 lodashMerge(Utils, {
diff --git a/package-lock.json b/package-lock.json
index 62622bbb..3fc7b66d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
       },
       "devDependencies": {
         "@cucumber/cucumber": "^8.2.1",
+        "@types/nightwatch": "^2.3.4",
         "@types/node": "^16.7.1",
         "chromedriver": "^106.0.1",
         "coveralls": "^3.1.1",
@@ -1095,6 +1096,12 @@
         "@types/responselike": "*"
       }
     },
+    "node_modules/@types/chai": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz",
+      "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==",
+      "dev": true
+    },
     "node_modules/@types/http-cache-semantics": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
@@ -1121,6 +1128,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/nightwatch": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/@types/nightwatch/-/nightwatch-2.3.4.tgz",
+      "integrity": "sha512-MZ/lj9dWzNdPavY0m1gbUo8yYUeb8G4LBSPeKw4vQnqZwOEoqSsApRX0N67IEWluGZAd31Ma77Fu/tZNP7h3dg==",
+      "dev": true,
+      "dependencies": {
+        "@types/chai": "*",
+        "@types/selenium-webdriver": "*",
+        "devtools-protocol": "^0.0.1025565"
+      }
+    },
     "node_modules/@types/node": {
       "version": "16.7.1",
       "dev": true,
@@ -1135,11 +1153,29 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/selenium-webdriver": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz",
+      "integrity": "sha512-NCn1vqHC2hDgZmOuiDa4xgyo3FBZuqB+wXg5t8YcwnjIi2ufGTowqcnbUAKGHxY7z/OFfv4MnaVfuJ7gJ/xBsw==",
+      "dev": true,
+      "dependencies": {
+        "@types/ws": "*"
+      }
+    },
     "node_modules/@types/uuid": {
       "version": "8.3.4",
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/ws": {
+      "version": "8.5.3",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
+      "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/yauzl": {
       "version": "2.9.2",
       "dev": true,
@@ -2235,6 +2271,12 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/devtools-protocol": {
+      "version": "0.0.1025565",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1025565.tgz",
+      "integrity": "sha512-0s5sbGQR/EfYQhd8EpZgphpndsv+CufTlaeUyA6vYXCA0H5kMAsHCS/cHtUFWoKJCO125hpoKicQCfpxRj4oqw==",
+      "dev": true
+    },
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "license": "Apache-2.0"
@@ -7242,6 +7284,12 @@
         "@types/responselike": "*"
       }
     },
+    "@types/chai": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz",
+      "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==",
+      "dev": true
+    },
     "@types/http-cache-semantics": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
@@ -7267,6 +7315,17 @@
       "version": "4.14.182",
       "dev": true
     },
+    "@types/nightwatch": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/@types/nightwatch/-/nightwatch-2.3.4.tgz",
+      "integrity": "sha512-MZ/lj9dWzNdPavY0m1gbUo8yYUeb8G4LBSPeKw4vQnqZwOEoqSsApRX0N67IEWluGZAd31Ma77Fu/tZNP7h3dg==",
+      "dev": true,
+      "requires": {
+        "@types/chai": "*",
+        "@types/selenium-webdriver": "*",
+        "devtools-protocol": "^0.0.1025565"
+      }
+    },
     "@types/node": {
       "version": "16.7.1",
       "dev": true
@@ -7280,10 +7339,28 @@
         "@types/node": "*"
       }
     },
+    "@types/selenium-webdriver": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz",
+      "integrity": "sha512-NCn1vqHC2hDgZmOuiDa4xgyo3FBZuqB+wXg5t8YcwnjIi2ufGTowqcnbUAKGHxY7z/OFfv4MnaVfuJ7gJ/xBsw==",
+      "dev": true,
+      "requires": {
+        "@types/ws": "*"
+      }
+    },
     "@types/uuid": {
       "version": "8.3.4",
       "dev": true
     },
+    "@types/ws": {
+      "version": "8.5.3",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
+      "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/yauzl": {
       "version": "2.9.2",
       "dev": true,
@@ -7983,6 +8060,12 @@
       "version": "1.0.0",
       "dev": true
     },
+    "devtools-protocol": {
+      "version": "0.0.1025565",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1025565.tgz",
+      "integrity": "sha512-0s5sbGQR/EfYQhd8EpZgphpndsv+CufTlaeUyA6vYXCA0H5kMAsHCS/cHtUFWoKJCO125hpoKicQCfpxRj4oqw==",
+      "dev": true
+    },
     "didyoumean": {
       "version": "1.2.2"
     },
diff --git a/package.json b/package.json
index 21b066b8..8fca3ff8 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
   },
   "devDependencies": {
     "@cucumber/cucumber": "^8.2.1",
+    "@types/nightwatch": "^2.3.4",
     "@types/node": "^16.7.1",
     "chromedriver": "^106.0.1",
     "coveralls": "^3.1.1",


Test patch:
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index dfcb9a4e..737ae474 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -636,7 +636,7 @@ class TestSuite {
     if (endSession) {
       const runnable = new Runnable('terminate', _ => {
         if (this.api && isFunction(this.api.end)) {
-          this.api.end();
+          this.api.end(endSession);
         }
       }, {
         isES6Async: this.isES6Async
diff --git a/test/extra/parallelism-envs.json b/test/extra/parallelism-envs.json
index 91dd18e6..4ad841a4 100644
--- a/test/extra/parallelism-envs.json
+++ b/test/extra/parallelism-envs.json
@@ -18,7 +18,7 @@
       "filter" : "mixed/*"
     },
     "env2" : {
-      "filter" : "simple/*"
+      "filter" : "simple/**"
     }
   }
 }
diff --git a/test/sampletests/reusebrowser/firstTest.js b/test/sampletests/reusebrowser/firstTest.js
index 9ab15680..266095ca 100644
--- a/test/sampletests/reusebrowser/firstTest.js
+++ b/test/sampletests/reusebrowser/firstTest.js
@@ -17,6 +17,7 @@ module.exports = {
   after(client, callback) {
     setTimeout(function() {
       client.globals.calls++;
+      client.end();
       callback();
     }, 10);
   }
diff --git a/test/sampletests/reusebrowser/secondTest.js b/test/sampletests/reusebrowser/secondTest.js
index ace989be..1b702220 100644
--- a/test/sampletests/reusebrowser/secondTest.js
+++ b/test/sampletests/reusebrowser/secondTest.js
@@ -17,6 +17,7 @@ module.exports = {
   after(client, callback) {
     setTimeout(function() {
       client.globals.calls++;
+      client.end();
       callback();
     }, 10);
   }
diff --git a/test/sampletests/withverify/verifyWithinPerform.js b/test/sampletests/withverify/verifyWithinPerform.js
new file mode 100644
index 00000000..11ce95a6
--- /dev/null
+++ b/test/sampletests/withverify/verifyWithinPerform.js
@@ -0,0 +1,14 @@
+module.exports = {  
+  demoTest(client) {
+    client.url('http://localhost')
+      .verify.equal(0, 1, 'custom message')
+      .perform(function () {
+        client
+          .pause(1000)
+          .verify.containsText('#weblogin', 'first');
+
+      })
+      .end();
+  }
+};
+  
\ No newline at end of file
diff --git a/test/src/api/commands/client/testEnd.js b/test/src/api/commands/client/testEnd.js
index d4c6be98..499696cb 100644
--- a/test/src/api/commands/client/testEnd.js
+++ b/test/src/api/commands/client/testEnd.js
@@ -24,6 +24,22 @@ describe('.end()', function() {
       this.client.start(done);
     });
 
+    it('browser.end(true) - forceEnd by default true', function (done) { 
+      this.client.api.end(true, result => {
+        assert.strictEqual(result.status, 0);
+        assert.strictEqual(this.client.sessionId, null);
+      });
+      this.client.start(done);
+    });
+
+    it('browser.end(false) - do not end sesion', function(done) {
+      this.client.api.end(false, result=> {
+        assert.strictEqual(result, null);
+        assert.strictEqual(this.client.sessionId, '1352110219202');
+      });
+      this.client.start(done);
+    });
+
     it('browser.end() - no session id', function (done) {
       this.client.api.end();
       this.client.api.end(function callback(result) {
@@ -87,6 +103,21 @@ describe('.end()', function() {
       }).catch(err => done(err));
     });
 
+
+    it('browser.end() - reuse browser session', function (done) {
+
+      Nightwatch.initClient({globals: {reuseBrowserSession: true}})
+        .then(client => {
+          client.api.end(function callback(result) {
+            assert.strictEqual(result, null);
+            assert.strictEqual(client.sessionId, '1352110219202');
+          });
+
+          client.start(done);
+        }).catch(err => done(err));
+    });
+
+
     it('browser.end() - failures and screenshots disabled', function (done) {
       MockServer.addMock({
         url: '/wd/hub/session/1352110219202/screenshot',
diff --git a/test/src/api/protocol/testMoveTo.js b/test/src/api/protocol/testMoveTo.js
index 3440fe97..85ccad58 100644
--- a/test/src/api/protocol/testMoveTo.js
+++ b/test/src/api/protocol/testMoveTo.js
@@ -2,20 +2,63 @@ const assert = require('assert');
 const Globals = require('../../../lib/globals.js');
 
 describe('browser commands', function() {
+  let args;
   before(function() {
-    Globals.protocolBefore();
+    Globals.protocolBefore.call(this);
+    
+  });
+
+  beforeEach(function() {
+    this.client.transport.Actions.session.moveTo = function(opts) {
+      args = opts.args;
+
+      return Promise.resolve(null);
+    };
+  });
+
+  afterEach(function() {
+    args = null;
   });
 
   it('testMoveTo', function () {
 
-    return Globals.protocolTest({
-      assertion: function(opts) {
-        assert.strictEqual(opts.method, 'POST');
-        assert.strictEqual(opts.path, '/session/1352110219202/moveto');
-        assert.deepStrictEqual(opts.data, {element: 'testElement', xoffset: 0, yoffset: 1});
-      },
+    return Globals.runProtocolTest({
+      assertion: function() {},
       commandName: 'moveTo',
       args: ['testElement', 0, 1]
+    }, this.client).then(_ => {
+      assert.deepStrictEqual(args, ['testElement', 0, 1]);
+    });
+  });
+
+  it('testMoveTo - no webElementId', function() {
+
+    return Globals.runProtocolTest({
+      assertion: function() {},
+      commandName: 'moveTo',
+      args: [2, 1]
+    }, this.client).then(_ => {
+      assert.deepStrictEqual(args, ['pointer', 2, 1]);
+    });
+  });
+
+  it('testMoveTo - null webElementId', function() {
+    return Globals.runProtocolTest({
+      assertion: function() {},
+      commandName: 'moveTo',
+      args: [null, 2, 1]
+    }, this.client).then(_ => {
+      assert.deepStrictEqual(args, ['pointer', 2, 1]);
     });
   });
+    
+  it('testMove - invalid parameters', function(done) {
+    Globals.runProtocolTest({
+      assertion: function() {},
+      commandName: 'moveTo',
+      args: ['test-element']
+    }, this.client)
+      .then(_ => done(new Error('should result into error')))
+      .catch(err => done());
+  });
 });
diff --git a/test/src/apidemos/custom-commands/testCustomCommandsAsync.js b/test/src/apidemos/custom-commands/testCustomCommandsAsync.js
index d568bbb4..da4c7703 100644
--- a/test/src/apidemos/custom-commands/testCustomCommandsAsync.js
+++ b/test/src/apidemos/custom-commands/testCustomCommandsAsync.js
@@ -97,5 +97,4 @@ describe('custom commands with findElements es6 async', function() {
       globals
     }));
   });
-
 });
\ No newline at end of file
diff --git a/test/src/cli/testCliRunner.js b/test/src/cli/testCliRunner.js
index ed4b0263..cbcd76d4 100644
--- a/test/src/cli/testCliRunner.js
+++ b/test/src/cli/testCliRunner.js
@@ -23,7 +23,7 @@ describe('Test CLI Runner', function() {
       }
     });
 
-    let config = {
+    const config = {
       src_folders: ['tests'],
       test_settings: {
         'default': {
@@ -32,7 +32,7 @@ describe('Test CLI Runner', function() {
       }
     };
 
-    let promiseConfig = Promise.resolve({
+    const promiseConfig = Promise.resolve({
       src_folders: ['promiseTests'],
       test_settings: {
         default: {
@@ -295,6 +295,14 @@ describe('Test CLI Runner', function() {
           throw new Error('Does not exist');
         }
 
+        if (module === './nightwatch.conf.ts'){
+          return {
+            isFile: function() {
+              return false;
+            }
+          };
+        }
+
         return {
           isFile: function() {
             return true;
@@ -310,7 +318,7 @@ describe('Test CLI Runner', function() {
     registerNoSettingsJsonMock();
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './nightwatch.json'
     }).setup();
 
@@ -354,7 +362,7 @@ describe('Test CLI Runner', function() {
   it('should override settings with CLI arguments', function() {
     registerNoSettingsJsonMock();
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './nightwatch.json',
       verbose: 'yes',
       output: 'test-output-folder',
@@ -397,7 +405,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './output_disabled.json',
       env: 'default'
     }).setup();
@@ -451,7 +459,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './settings.json',
       env: 'default',
       output: 'output',
@@ -490,7 +498,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'extra'
     }).setup();
@@ -553,7 +561,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'default',
       test: 'demoTest'
@@ -570,8 +578,8 @@ describe('Test CLI Runner', function() {
   });
 
   it('testGetTestSourceSingleWithAbsolutePath', function() {
-    let ABSOLUTE_PATH = '/path/to/test';
-    let ABSOLUTE_SRC_PATH = ABSOLUTE_PATH + '.js';
+    const ABSOLUTE_PATH = '/path/to/test';
+    const ABSOLUTE_SRC_PATH = ABSOLUTE_PATH + '.js';
     let statSyncCalled = false;
 
     mockery.registerMock('fs', {
@@ -614,7 +622,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'default',
       test: ABSOLUTE_PATH
@@ -633,8 +641,8 @@ describe('Test CLI Runner', function() {
   });
 
   it('testGetTestSourceSingleWithRelativePath', function() {
-    let RELATIVE_PATH = '../path/to/test';
-    let TEST_SRC_PATH = process.cwd() + '/path/to/test.js';
+    const RELATIVE_PATH = '../path/to/test';
+    const TEST_SRC_PATH = process.cwd() + '/path/to/test.js';
     let statSyncCalled = false;
 
     mockery.registerMock('fs', {
@@ -676,7 +684,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'default',
       test: RELATIVE_PATH
@@ -719,7 +727,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'default',
       group: 'demoGroup'
@@ -730,7 +738,7 @@ describe('Test CLI Runner', function() {
     const walker = Runner.getTestSource(runner.test_settings, runner.argv);
     assert.deepStrictEqual(walker.testSource, ['tests/demoGroup']);
 
-    let otherRunner = new CliRunner({
+    const otherRunner = new CliRunner({
       config: './custom.json',
       env: 'default',
       group: 'tests/demoGroup'
@@ -739,7 +747,7 @@ describe('Test CLI Runner', function() {
     const walker2 = Runner.getTestSource(otherRunner.test_settings, otherRunner.argv);
     assert.deepStrictEqual(walker2.testSource, ['tests/demoGroup']);
 
-    let simpleRunner = new CliRunner({
+    const simpleRunner = new CliRunner({
       config: './custom.json',
       env: 'default'
     }).setup();
@@ -747,7 +755,7 @@ describe('Test CLI Runner', function() {
     const walker3 = Runner.getTestSource(simpleRunner.test_settings, simpleRunner.argv);
     assert.deepStrictEqual(walker3.testSource, ['tests']);
 
-    let invalidGroupRunner = new CliRunner({
+    const invalidGroupRunner = new CliRunner({
       config: './custom.json',
       env: 'default',
       group: 'group_doesnotexist'
@@ -756,7 +764,7 @@ describe('Test CLI Runner', function() {
     const walker4 = Runner.getTestSource(invalidGroupRunner.test_settings, invalidGroupRunner.argv);
     assert.deepStrictEqual(walker4.testSource, ['tests/group_doesnotexist']);
 
-    let invalidGroupInMultiSrcRunner = new CliRunner({
+    const invalidGroupInMultiSrcRunner = new CliRunner({
       config: './multi_test_paths.json',
       env: 'default',
       group: 'group_doesnotexist'
@@ -798,7 +806,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'default',
       group: 'demoGroup1,demoGroup2'
@@ -809,7 +817,7 @@ describe('Test CLI Runner', function() {
     const walker = Runner.getTestSource(runner.test_settings, runner.argv);
     assert.deepStrictEqual(walker.testSource, ['tests/demoGroup1', 'tests/demoGroup2']);
 
-    let invalidGroupRunner = new CliRunner({
+    const invalidGroupRunner = new CliRunner({
       config: './custom.json',
       env: 'default',
       group: 'demoGroup1,demoGroup2,group_doesnotexist'
@@ -818,7 +826,7 @@ describe('Test CLI Runner', function() {
     const walker2 = Runner.getTestSource(invalidGroupRunner.test_settings, invalidGroupRunner.argv);
     assert.deepStrictEqual(walker2.testSource, ['tests/demoGroup1', 'tests/demoGroup2', 'tests/group_doesnotexist']);
 
-    let stripMissingInMultiRunner = new CliRunner({
+    const stripMissingInMultiRunner = new CliRunner({
       config: './multi_test_paths.json',
       env: 'default',
       group: 'demoGroup1,demoGroup2,group_doesnotexist'
@@ -846,7 +854,7 @@ describe('Test CLI Runner', function() {
 
     const CliRunner = common.require('runner/cli/cli.js');
 
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './null.json',
       env: 'default'
     });
@@ -898,7 +906,7 @@ describe('Test CLI Runner', function() {
     });
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './custom.json',
       env: 'extra'
     }).setup({
@@ -976,13 +984,46 @@ describe('Test CLI Runner', function() {
     registerNoSettingsJsonMock();
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './nightwatch.json'
     }).setup();
 
     assert.strictEqual(runner.argv.config, './nightwatch.conf.cjs');
   });
 
+  it('using Typescript config file', function () {
+    mockery.deregisterMock('package.json');
+    mockery.deregisterMock('fs');
+    mockery.registerMock('./nightwatch.conf.ts', {
+      src_folders: ['tests'],
+      test_settings: {
+        default: {
+          silent: true
+        }
+      }
+    });
+    mockery.registerMock('fs', {
+      statSync: function (module) {
+        if (module === './nightwatch.conf.ts') {
+          return {
+            isFile: function () {
+              return true;
+            }
+          };
+        }
+      },
+      constants,
+      rmdirSync
+    });
+
+    const CliRunner = common.require('runner/cli/cli.js');
+    const runner = new CliRunner({
+      config: './nightwatch.json'
+    }).setup();
+
+    assert.strictEqual(runner.argv.config, './nightwatch.conf.ts');
+  });
+
   it('using no package.json file', function() {
     mockery.deregisterMock('package.json');
     mockery.registerMock('package.json');
@@ -990,7 +1031,7 @@ describe('Test CLI Runner', function() {
     registerNoSettingsJsonMock();
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({});
+    const runner = new CliRunner({});
 
     const config = runner.getLocalConfigFileName();
     assert.strictEqual(config, './nightwatch.conf.js');
diff --git a/test/src/cli/testCliRunnerMocha.js b/test/src/cli/testCliRunnerMocha.js
index 59a4ee75..07c7a8f5 100644
--- a/test/src/cli/testCliRunnerMocha.js
+++ b/test/src/cli/testCliRunnerMocha.js
@@ -55,7 +55,7 @@ describe('test CLI Runner Mocha', function() {
   });
 
   it('testRunWithMochaDefaults', function() {
-    let testFiles = [];
+    const testFiles = [];
 
     const defaultOptions = {timeout: 20000, reporterOptions: {}};
     if (isCi) {
@@ -78,7 +78,7 @@ describe('test CLI Runner Mocha', function() {
     }, testFiles);
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './withmocha.json',
       env: 'default',
       reporter: 'junit'
@@ -90,7 +90,7 @@ describe('test CLI Runner Mocha', function() {
   });
 
   it('testRunWithMochaPerEnvironment', function() {
-    let testFiles = [];
+    const testFiles = [];
     const defaultOptions = {timeout: 20000, reporterOptions: {}};
     if (isCi) {
       defaultOptions.color = false;
@@ -115,7 +115,7 @@ describe('test CLI Runner Mocha', function() {
     }, testFiles);
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './withmocha.json',
       env: 'mochatests',
       reporter: 'junit'
@@ -127,7 +127,7 @@ describe('test CLI Runner Mocha', function() {
   });
 
   it('testRunWithMochaCustomOpts', function() {
-    let testFiles = [];
+    const testFiles = [];
 
     const defaultOptions = {
       ui: 'tdd',
@@ -160,7 +160,7 @@ describe('test CLI Runner Mocha', function() {
     }, testFiles);
 
     const CliRunner = common.require('runner/cli/cli.js');
-    let runner = new CliRunner({
+    const runner = new CliRunner({
       config: './withmocha.json',
       env: 'default',
       reporter: 'junit'
diff --git a/test/src/cli/testDemoTypescriptFile.js b/test/src/cli/testDemoTypescriptFile.js
new file mode 100644
index 00000000..eeaa9de7
--- /dev/null
+++ b/test/src/cli/testDemoTypescriptFile.js
@@ -0,0 +1,74 @@
+const path = require('path');
+const MockServer = require('../../lib/mockserver.js');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Utils = common.require('utils');
+
+describe('Typescript demos', function () {
+  const oldLoadTSNode = Utils.loadTSNode;
+  let tsNode;
+
+  before(function(done) {
+    Utils.loadTSNode = function(projectTsFile) {
+      
+      tsNode = require('ts-node').register({
+        esm: false,
+        project: projectTsFile
+      });
+      
+    };
+    done();
+  });
+
+  beforeEach(function (done) {
+    this.server = MockServer.init();
+    this.server.on('listening', () => {
+      done();
+    });
+  });
+
+  afterEach(function (done) {
+    this.server.close(function () {
+      done();
+    });
+  });
+
+  after(function(done) {
+    tsNode.enabled(false);
+    Utils.loadTSNode = oldLoadTSNode;
+    done();
+  });
+
+  it('run basic typescript tests', function () {
+    this.timeout(5000);
+    const testsPath = path.join(__dirname, '../../typescript-tests/demo.ts');
+    const originalCwd = process.cwd();
+    process.chdir(path.join(__dirname, '../../typescript-tests'));
+
+    const globals = {
+      waitForConditionPollInterval: 50,
+
+      reporter(results) {
+        if (results.lastError) {
+          throw results.lastError;
+        }
+      }
+    };
+
+    return NightwatchClient.runTests(testsPath, {
+      selenium: {
+        host: 'localhost',
+        port: 10195,
+        start_process: false
+      },
+      output: false,
+      skip_testcases_on_fail: false,
+      silent: true,
+      persist_globals: true,
+      globals,
+      output_folder: false
+    }).then(() => {
+      process.chdir(originalCwd);
+    });
+  });
+});
diff --git a/test/src/cli/testParallelExecution.js b/test/src/cli/testParallelExecution.js
index f62d6f8b..5c16655b 100644
--- a/test/src/cli/testParallelExecution.js
+++ b/test/src/cli/testParallelExecution.js
@@ -184,12 +184,12 @@ describe('test Parallel Execution', function() {
     });
   });
 
-  it('test parallel execution with parallel=count arg', function() {
+  it('test parallel execution with workers=count arg', function() {
     const CliRunner = common.require('runner/cli/cli.js');
     let runner = new CliRunner({
       reporter: 'junit',
       config: path.join(__dirname, '../../extra/parallelism-count.json'),
-      parallel: 2
+      workers: 2
     });
 
     runner.setup();
@@ -239,8 +239,8 @@ describe('test Parallel Execution', function() {
 
     runner.setup();
 
-    assert.strictEqual(runner.isConcurrencyEnabled([runner.argv._source]), false);
-
+    // run in parallel mode even if single source file is provided
+    assert.strictEqual(runner.isConcurrencyEnabled([runner.argv._source]), true);
   });
 
   it('test parallel execution with workers and single source folder', function() {
diff --git a/test/src/runner/testRunWithVerify.js b/test/src/runner/testRunWithVerify.js
index 85a0d802..e65d5275 100644
--- a/test/src/runner/testRunWithVerify.js
+++ b/test/src/runner/testRunWithVerify.js
@@ -27,9 +27,9 @@ describe('testRunWithVerify', function() {
     CommandGlobals.afterEach.call(this, done);
   });
 
-  it('using verify in tests', function() {
+  it('using verify in tests with simple failures', function() {
     return runTests({
-      _source: ['./withverify']
+      _source: ['./withverify/verifySampleFailures.js']
     }, settings({
       globals: {
         waitForConditionPollInterval: 20,
@@ -47,5 +47,24 @@ describe('testRunWithVerify', function() {
     }));
   });
 
+  it('using verify within perform in tests', function() {
+    return runTests({
+      _source: ['./withverify/verifyWithinPerform.js']
+    }, settings({
+      globals: {
+        waitForConditionPollInterval: 20,
+        waitForConditionTimeout: 50,
+        retryAssertionTimeout: 50,
+        reporter(results, cb) {
+          assert.ok('verifyWithinPerform' in results.modules);
+          assert.strictEqual(results.passed, 0);
+          assert.strictEqual(results.failed, 2);
+          assert.strictEqual(results.assertions, 2);
+
+          cb();
+        }
+      }
+    }));
+  });
 
 });
diff --git a/test/src/runner/testRunnerSessionCreate.js b/test/src/runner/testRunnerSessionCreate.js
index 1bf3b17c..148759fd 100644
--- a/test/src/runner/testRunnerSessionCreate.js
+++ b/test/src/runner/testRunnerSessionCreate.js
@@ -349,6 +349,16 @@ describe('testRunnerSessionCreate', function() {
       })
     }, true);
 
+    MockServer.addMock({
+      url: '/session/1352110219202',
+      method: 'DELETE',
+      statusCode: 200,
+      response: JSON.stringify({
+        status: 0
+      })
+    }, true);
+
+
    
 
     const globals = {
@@ -392,6 +402,16 @@ describe('testRunnerSessionCreate', function() {
       })
     }, true);
 
+    MockServer.addMock({
+      url: '/session/1352110219202',
+      method: 'DELETE',
+      statusCode: 200,
+      response: JSON.stringify({
+        status: 0
+      })
+    }, true);
+
+
    
 
     const globals = {
diff --git a/test/src/utils/testUtils.js b/test/src/utils/testUtils.js
index f34dcf13..9a39ad08 100644
--- a/test/src/utils/testUtils.js
+++ b/test/src/utils/testUtils.js
@@ -2,6 +2,10 @@ const assert = require('assert');
 const path = require('path');
 const common = require('../../common.js');
 const Utils = common.require('utils');
+const mockery = require('mockery');
+
+delete require.cache['fs'];
+delete require.cache['path'];
 
 describe('test Utils', function() {
 
@@ -201,4 +205,88 @@ describe('test Utils', function() {
     // eslint-disable-next-line no-global-assign
     console = oldConsole;
   });
+
+
+  describe('test findTSConfigFile', function () {
+    const {constants, rmdirSync} = require('fs');
+
+    beforeEach(function () {
+      mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+    });
+
+    afterEach(function () {
+      mockery.deregisterAll();
+      mockery.resetCache();
+      mockery.disable();
+    });
+
+    it('loads default tsconfig correctly', function () {
+      const tsConfigPath = path.join(__dirname, '../../typescript-tests/tsconfig.nightwatch.json');
+
+      const tsConfigFile = Utils.findTSConfigFile(tsConfigPath);
+
+      assert.strictEqual(tsConfigFile, tsConfigPath);
+    });
+
+    it('loads `tsconfig.nightwatch.json` correctly', function () {
+      mockery.registerMock('path', {
+        join: function (a, b) {
+          if (b === 'tsconfig.nightwatch.json') {
+            return '/path/to/tsconfig.nightwatch.json';
+          }
+        }
+      });
+      mockery.registerMock('fs', {
+        statSync: function (module) {
+          if (module === '/path/to/tsconfig.nightwatch.json') {
+            return {
+              isFile: function () {
+                return true;
+              }
+            };
+          }
+          throw new Error('Does not exist');
+        },
+        constants,
+        rmdirSync
+      });
+
+      const tsConfigPath = '/path/to/tsconfig.nightwatch.json';
+      const localUtils = common.require('utils');
+
+      const tsConfigFile = localUtils.findTSConfigFile('');
+
+      assert.strictEqual(tsConfigFile, tsConfigPath);
+    });
+
+    it('loads `nightwatch/tsconfig.json` correctly', function () {
+      mockery.registerMock('path', {
+        join: function (a, b) {
+          if (b === 'nightwatch') {
+            return '/path/to/nightwatch/tsconfig.json';
+          }
+        }
+      });
+      mockery.registerMock('fs', {
+        statSync: function (module) {
+          if (module === '/path/to/nightwatch/tsconfig.json') {
+            return {
+              isFile: function () {
+                return true;
+              }
+            };
+          }
+          throw new Error('Does not exist');
+        },
+        constants,
+        rmdirSync
+      });
+      const tsConfigPath = '/path/to/nightwatch/tsconfig.json';
+      const localUtils = common.require('utils');
+
+      const tsConfigFile = localUtils.findTSConfigFile('');
+
+      assert.strictEqual(tsConfigFile, tsConfigPath);
+    });
+  });
 });
diff --git a/test/typescript-tests/demo.ts b/test/typescript-tests/demo.ts
new file mode 100644
index 00000000..eb089e21
--- /dev/null
+++ b/test/typescript-tests/demo.ts
@@ -0,0 +1,10 @@
+import { NightwatchTests } from 'nightwatch';
+import assert from 'assert';
+
+const home: NightwatchTests = {
+  'Demo test': () => {
+    assert.strictEqual(1+1, 2);
+  }
+};
+
+export default home;
diff --git a/test/typescript-tests/tsconfig.nightwatch.json b/test/typescript-tests/tsconfig.nightwatch.json
new file mode 100644
index 00000000..844d6b85
--- /dev/null
+++ b/test/typescript-tests/tsconfig.nightwatch.json
@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "commonjs",
+    "strict": true,
+    "rootDir": ".",
+    "outDir": "dist",
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "moduleResolution": "node",
+    "sourceMap": false,
+    "resolveJsonModule": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitAny": true,
+    "noImplicitThis": true,
+    "strictNullChecks": true,
+    "skipLibCheck": true
+  }
+}