Repository: nightwatchjs/nightwatch
Pull request #3435 (2022-10-14 15:36:15)

Features/update end command to handle reuse browser session
## Changes
- added `forceEnd` for `end` command
- handled `.end` command while resuing browser sessions

## Impacts
- `.end` command will not end sessions while reusing browser session unless forced.


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/runner/cli/cli.js b/lib/runner/cli/cli.js
index b42ad4b3..793c7d8c 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...'));
@@ -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);
       }
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/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/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/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/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/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
+  }
+}