Repository: nightwatchjs/nightwatch
Pull request #4299 (2024-11-15 08:48:16)

Pass local context to `browser.debug()` command.
With this enhancement in the `browser.debug()` command, users will now be able to pass the local variables from their test into the Debug mode and use them there.

### Example

On running the below example, `someLocalFunction` and `someLocalVariable` will be directly available inside the DEBUG mode (along with `browser`) and can be used like a local variable.

```js
function someLocalFunction() {
  console.log('local function running');

  return 'local function result';
}

describe('duckduckgo example', function() {
  it('Search Nightwatch.js and check results', async function(browser) {
    const someLocalVariable = 'something random';

    browser
      .navigateTo('http://duckduckgo.com')
      .click('input[name=q]');

    browser.debug({context: {someLocalVariable, someLocalFunction}});
  });
});
```

<img width="508" alt="image" src="https://github.com/user-attachments/assets/905d8584-c529-4625-8d78-39e0c99fd1a6">


github-actions[bot]: 
## Status
* ❌ No modified files found in the **types** directory.
Please make sure to include types for any changes you have made. Thank you!.

Patch:
diff --git a/lib/api/_loaders/command.js b/lib/api/_loaders/command.js
index 3afd576d..ee961116 100644
--- a/lib/api/_loaders/command.js
+++ b/lib/api/_loaders/command.js
@@ -151,6 +151,18 @@ class CommandLoader extends BaseCommandLoader {
 
   createWrapper() {
     if (this.module) {
+      // this place is only reached by client-commands, protocol commands and custom-commands (no assertions or element-commands).
+      if (this.isUserDefined) {
+        // only custom-commands will reach here.
+        // later extend this to client-commands and protocol commands as well.
+        Object.defineProperty(this.module, 'rejectNodeOnAbortFailure', {
+          configurable: true,
+          get() {
+            return true;
+          }
+        });
+      }
+
       this.commandFn = function commandFn({args, stackTrace}) {
         const instance = CommandLoader.createInstance(this.nightwatchInstance, this.module, {
           stackTrace,
@@ -183,7 +195,16 @@ class CommandLoader extends BaseCommandLoader {
           })
           .catch(err => {
             if (instance instanceof EventEmitter) {
-              instance.emit('error', err);
+              if (instance.needsPromise) {
+                // if the instance has `needsPromise` set to `true`, the `error` event is listened
+                // on the `context` object, not on the `instance` object (in `treenode.js`).
+                this.emit('error', err);
+              } else {
+                // for class-based commands that inherit from EventEmitter.
+                // Since the `needsPromise` is set to `false` in this case, the `complete` and `error`
+                // events are listened on the `instance` object.
+                instance.emit('error', err);
+              }
 
               return;
             }
diff --git a/lib/api/client-commands/debug.js b/lib/api/client-commands/debug.js
index e5632223..bde1049c 100644
--- a/lib/api/client-commands/debug.js
+++ b/lib/api/client-commands/debug.js
@@ -1,6 +1,7 @@
 const EventEmitter = require('events');
 const NightwatchRepl = require('../../testsuite/repl');
 const Debuggability = require('../../utils/debuggability.js');
+const {By} = require('selenium-webdriver');
 
 /**
  * This command halts the test execution and provides users with a REPL interface where they can type
@@ -37,24 +38,37 @@ class Debug extends EventEmitter {
   }
 
   command(config, callback) {
+    // Create context for vm
+    const context = {
+      browser: this.api,
+      app: this.api,
+      by: By,
+      By: By
+    };
+
+    // set the user provided context
+    if (config.context) {
+      Object.assign(context, config.context);
+      delete config.context;
+    }
+
     const repl = new NightwatchRepl(config);
 
     // eslint-disable-next-line
     console.log(NightwatchRepl.introMessage());
 
-    // Create context for vm
-    const context = {
-      browser: this.api,
-      app: this.api
-    };
+    // TODO: what's the use of this `if` block?
     if (config?.selector) {
       this.api.executeScript('console.log("Element ' + config.selector + ':", document.querySelector("' + config.selector + '"))');
     }
+
     // Before starting REPL server, Set debugMode to true
     Debuggability.debugMode = true;
+
     // Set isES6AsyncTestcase to true in debugmode
     const isES6AsyncTestcase = this.client.isES6AsyncTestcase;
     this.client.isES6AsyncTestcase = true;
+
     repl.startServer(context);
 
     repl.onExit(() => {
diff --git a/lib/api/client-commands/perform.js b/lib/api/client-commands/perform.js
index 15d3c66b..f81f0b81 100644
--- a/lib/api/client-commands/perform.js
+++ b/lib/api/client-commands/perform.js
@@ -62,9 +62,13 @@ class Perform extends EventEmitter {
     return true;
   }
 
+  static get avoidPrematureParentNodeResolution() {
+    return true;
+  }
+
   command(callback = function() {}) {
     let doneCallback;
-    let asyncHookTimeout = this.client.settings.globals.asyncHookTimeout;
+    const asyncHookTimeout = this.client.settings.globals.asyncHookTimeout;
 
     this.timeoutId = setTimeout(() => {
       this.emit('error', new Error(`Timeout while waiting (${asyncHookTimeout}ms) for the .perform() command callback to be called.`));
@@ -99,7 +103,7 @@ class Perform extends EventEmitter {
       };
     } else {
       doneCallback = () => {
-        let args = [(result) => {
+        const args = [(result) => {
           clearTimeout(this.timeoutId);
           this.emit('complete', result);
         }];
diff --git a/lib/api/web-element/assert/element-assertions.js b/lib/api/web-element/assert/element-assertions.js
index 170e8b3f..d8721b29 100644
--- a/lib/api/web-element/assert/element-assertions.js
+++ b/lib/api/web-element/assert/element-assertions.js
@@ -90,7 +90,7 @@ module.exports.create = function createAssertions(scopedElement, {negated = fals
       value(message) {
         return instance.assert(async (assertApi, element) => {
           const el = await element;
-          const id = await el.getId();
+          const id = await el?.getId();
 
           return assertApi.ok(el instanceof WebElement, message || `Testing if the element <WebElement: ${id}> is present.`);
         });
diff --git a/lib/api/web-element/scoped-element.js b/lib/api/web-element/scoped-element.js
index 3d2e55ee..49c3a5b7 100644
--- a/lib/api/web-element/scoped-element.js
+++ b/lib/api/web-element/scoped-element.js
@@ -180,11 +180,14 @@ class ScopedWebElement {
 
         return await this.findElement({parentElement, selector: condition, index, timeout, retryInterval});
       } catch (error) {
-        if (this.suppressNotFoundErrors) {
+        const narrowedError = createNarrowedError({error, condition, timeout});
+        if (
+          this.suppressNotFoundErrors &&
+          narrowedError.name === 'NoSuchElementError'
+        ) {
           return null;
         }
 
-        const narrowedError = createNarrowedError({error, condition, timeout});
         Logger.error(narrowedError);
 
         if (abortOnFailure) {
@@ -356,6 +359,8 @@ function createNarrowedError({error, condition, timeout}) {
     return err;
   }
 
+  error.message = `Error occurred while trying to locate element "${condition}": ${error.message || 'unknown error'}`;
+
   return error;
 }
 
diff --git a/lib/core/asynctree.js b/lib/core/asynctree.js
index 926a45a1..abf85cde 100644
--- a/lib/core/asynctree.js
+++ b/lib/core/asynctree.js
@@ -106,7 +106,7 @@ class AsyncTree extends EventEmitter{
   shouldRejectNodePromise(err, abortOnFailure, node = this.currentNode) {
     const rejectNodeOnAbortFailure = node.options?.rejectNodeOnAbortFailure;
 
-    if ((err.isExpect || node.namespace === 'assert' || (abortOnFailure && rejectNodeOnAbortFailure)) && this.currentNode.isES6Async) {
+    if ((err.isExpect || node.namespace === 'assert' || (abortOnFailure && rejectNodeOnAbortFailure)) && node.isES6Async) {
       return true;
     }
 
@@ -134,7 +134,6 @@ class AsyncTree extends EventEmitter{
     this.emit('asynctree:command:start', {node});
 
     const result = await node.run();
-    const {parent} = this.currentNode;
 
     let abortOnFailure = false;
     let err;
@@ -155,7 +154,7 @@ class AsyncTree extends EventEmitter{
       }
 
       if (this.shouldRejectParentNodePromise(err, node)) {
-        parent.reject(err);
+        node.parent.reject(err);
       }
     } else {
       node.resolveValue = result;
@@ -212,6 +211,15 @@ class AsyncTree extends EventEmitter{
   }
 
   async done(err = null) {
+    // `this.currentTestCaseResult` represents the return value of the
+    // currently executing `it` test case or hook.
+    // We do not want to clear the tree if the test case is still running.
+    // In case of an error, the tree will be cleared in the `runChildNode`
+    // method itself.
+    if (this.currentTestCaseResult instanceof Promise && !this.currentTestCaseResult.settled) {
+      return err;
+    }
+
     this.emit('asynctree:finished', this);
     this.empty();
     this.createRootNode();
diff --git a/lib/core/client.js b/lib/core/client.js
index bd5d1554..81d6371d 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -680,10 +680,12 @@ class NightwatchClient extends EventEmitter {
   // Initialize the APIs
   //////////////////////////////////////////////////////////////////////////////////////////
 
-  async initialize() {
+  async initialize(loadNightwatchApis = true) {
     this.loadKeyCodes();
 
-    return this.loadNightwatchApis();
+    if (loadNightwatchApis) {
+      return this.loadNightwatchApis();
+    }
   }
 
   setCurrentTest() {
diff --git a/lib/core/queue.js b/lib/core/queue.js
index 3e8b3a71..eac2f339 100644
--- a/lib/core/queue.js
+++ b/lib/core/queue.js
@@ -145,7 +145,8 @@ class CommandQueue extends EventEmitter {
     return Promise.resolve();
   }
 
-  run() {
+  run(currentTestCaseResult) {
+    this.tree.currentTestCaseResult = currentTestCaseResult;
     if (this.tree.started) {
       return this;
     }
diff --git a/lib/core/treenode.js b/lib/core/treenode.js
index 1bfa4ae3..057cbb0f 100644
--- a/lib/core/treenode.js
+++ b/lib/core/treenode.js
@@ -206,6 +206,9 @@ class TreeNode {
       commandResult = this.instance;
       if (this.instance.needsPromise) {
         this.needsPromise = true;
+        // this change was done because the only way function-styled custom commands could be resolved
+        // is if they contain another NW API command, which could call `node.context.emit('complete')`
+        // inside `asynctree.js > resolveNode` method for the custom command node.
         commandResult = this.context;
       }
     } else if (this.context instanceof EventEmitter) { // Chai assertions
diff --git a/lib/http/request.js b/lib/http/request.js
index c815e337..0b655422 100644
--- a/lib/http/request.js
+++ b/lib/http/request.js
@@ -114,7 +114,7 @@ class HttpRequest extends EventEmitter {
     this.setPathPrefix(options);
 
     const {method} = options;
-    if (options.data && !(method === 'POST' || method === 'PUT')) {
+    if (options.data && !(method === 'POST' || method === 'PUT' || method === 'PATCH')) {
       options.data = '';
     }
 
diff --git a/lib/index.js b/lib/index.js
index 11eb1129..b3f88009 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -142,10 +142,10 @@ module.exports.createClient = function({
       return cliRunner.globals.runGlobalHook('after', [client.settings]);
     },
 
-    launchBrowser() {
+    launchBrowser({loadNightwatchApis = true} = {}) {
       const {argv} = cliRunner;
 
-      return client.initialize()
+      return client.initialize(loadNightwatchApis)
         .then(() => {
           return client.createSession({argv});
         })
diff --git a/lib/runner/cli/nightwatch.conf.ejs b/lib/runner/cli/nightwatch.conf.ejs
index b45f7899..c90f45c0 100644
--- a/lib/runner/cli/nightwatch.conf.ejs
+++ b/lib/runner/cli/nightwatch.conf.ejs
@@ -115,7 +115,7 @@ module.exports = {
         start_process: true,
         server_path: '',
         cli_args: [
-          // --verbose
+          // '--verbose'
         ]
       }
     },
@@ -138,7 +138,7 @@ module.exports = {
         //  and set the location below:
         server_path: '',
         cli_args: [
-          // --verbose
+          // '--verbose'
         ]
       }
     },
@@ -195,7 +195,7 @@ module.exports = {
       disable_error_log: true,
       webdriver: {
         timeout_options: {
-          timeout: 15000,
+          timeout: 60000,
           retry_attempts: 3
         },
         keep_alive: true,
diff --git a/lib/transport/factory.js b/lib/transport/factory.js
index 95092c1b..84c5be85 100644
--- a/lib/transport/factory.js
+++ b/lib/transport/factory.js
@@ -48,8 +48,12 @@ module.exports = class TransportFactory {
     }
   }
 
+  static usingBrowserstackTurboScale(settings) {
+    return settings.webdriver.host && (settings.webdriver.host.endsWith('.browserstack-ats.com') || settings.webdriver.host.startsWith('browserstack-turboscale-grid'));
+  }
+
   static usingBrowserstack(settings) {
-    return settings.webdriver.host && (settings.webdriver.host.endsWith('.browserstack.com') || settings.webdriver.host.endsWith('.browserstack-ats.com') || settings.webdriver.host.startsWith('browserstack-turboscale-grid'));
+    return settings.webdriver.host && (settings.webdriver.host.endsWith('.browserstack.com') || TransportFactory.usingBrowserstackTurboScale(settings));
   }
 
   static getBrowserName(nightwatchInstance) {
@@ -124,6 +128,12 @@ module.exports = class TransportFactory {
         return new AppAutomate(nightwatchInstance, browserName);
       }
 
+      if (TransportFactory.usingBrowserstackTurboScale(nightwatchInstance.settings)) {
+        const AutomateTurboScale = require('./selenium-webdriver/browserstack/automateTurboScale.js');
+
+        return new AutomateTurboScale(nightwatchInstance, browserName);
+      }
+
       const Automate = require('./selenium-webdriver/browserstack/automate.js');
 
       return new Automate(nightwatchInstance, browserName);
diff --git a/lib/transport/selenium-webdriver/browserstack/automateTurboScale.js b/lib/transport/selenium-webdriver/browserstack/automateTurboScale.js
new file mode 100644
index 00000000..8c50ad19
--- /dev/null
+++ b/lib/transport/selenium-webdriver/browserstack/automateTurboScale.js
@@ -0,0 +1,77 @@
+const stripAnsi = require('strip-ansi');
+const Automate = require('./automate.js');
+
+class AutomateTurboScale extends Automate {
+  get buildUrl() {
+    return `https://${this.productNamespace}.browserstack.com/dashboard`;
+  }
+
+  get ApiUrl() {
+    return 'https://api.browserstack.com/automate-turboscale/v1';
+  }
+
+  get productNamespace() {
+    return 'grid';
+  }
+
+  async getBuildId() {
+    try {
+      const builds = await this.sendHttpRequest({
+        url: `${this.ApiUrl}/builds?status=running`,
+        method: 'GET',
+        use_ssl: true,
+        port: 443,
+        auth: {
+          user: this.username,
+          pass: this.accessKey
+        }
+      });
+
+      const buildData = builds.builds;
+      const currentBuild = buildData.find((item) => {
+        return item.name === this.build;
+      });
+
+      return currentBuild && currentBuild.hashed_id;
+    } catch (err) {
+      console.error(err);
+    }
+  }
+
+  async sendReasonToBrowserstack(isFailure = false, reason = '') {
+    const sessionDetails = await this.sendHttpRequest({
+      url: `${this.ApiUrl}/sessions/${this.sessionId}`,
+      method: 'GET',
+      use_ssl: true,
+      port: 443,
+      auth: {
+        user: this.username,
+        pass: this.accessKey
+      }
+    });
+
+    const status = sessionDetails?.status;
+    if (['passed', 'failed'].includes(status)) {
+      // status has already been set by user
+      return;
+    }
+
+    reason = stripAnsi(reason);
+    await this.sendHttpRequest({
+      url: `${this.ApiUrl}/sessions/${this.sessionId}`,
+      method: 'PATCH',
+      use_ssl: true,
+      port: 443,
+      data: {
+        status: isFailure ? 'failed' : 'passed',
+        reason
+      },
+      auth: {
+        user: this.username,
+        pass: this.accessKey
+      }
+    });
+  }
+}
+
+module.exports = AutomateTurboScale;
diff --git a/lib/transport/selenium-webdriver/browserstack/browserstack.js b/lib/transport/selenium-webdriver/browserstack/browserstack.js
index 4c7876ea..efc1bf4e 100644
--- a/lib/transport/selenium-webdriver/browserstack/browserstack.js
+++ b/lib/transport/selenium-webdriver/browserstack/browserstack.js
@@ -9,6 +9,10 @@ class Browserstack extends AppiumBaseServer {
     return this.settings.desiredCapabilities['bstack:options'];
   }
 
+  get buildUrl() {
+    return `https://${this.productNamespace}.browserstack.com`;
+  }
+
   get accessKey() {
     return this.bStackOptions().accessKey;
   }
@@ -167,7 +171,7 @@ class Browserstack extends AppiumBaseServer {
         await this.sendReasonToBrowserstack(!!err, reason);
         // eslint-disable-next-line no-console
         console.log('\n  ' + 'See more info, video, & screenshots on Browserstack:\n' +
-          '  ' + Logger.colors.light_cyan(`https://${this.productNamespace}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
+          '  ' + Logger.colors.light_cyan(`${this.buildUrl}/builds/${this.buildId}/sessions/${this.sessionId}`));
       }
 
       if (this.uploadedAppUrl) {
diff --git a/package-lock.json b/package-lock.json
index 1a8598c8..adef0454 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nightwatch",
-  "version": "3.9.0",
+  "version": "3.10.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nightwatch",
-      "version": "3.9.0",
+      "version": "3.10.2",
       "license": "MIT",
       "dependencies": {
         "@nightwatch/chai": "5.0.3",
@@ -37,7 +37,7 @@
         "open": "8.4.2",
         "ora": "5.4.1",
         "piscina": "^4.3.1",
-        "selenium-webdriver": "4.26.0",
+        "selenium-webdriver": "4.27.0",
         "semver": "7.5.4",
         "stacktrace-parser": "0.1.10",
         "strip-ansi": "6.0.1",
@@ -2943,10 +2943,11 @@
       }
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -8098,10 +8099,19 @@
       "dev": true
     },
     "node_modules/selenium-webdriver": {
-      "version": "4.26.0",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.26.0.tgz",
-      "integrity": "sha512-nA7jMRIPV17mJmAiTDBWN96Sy0Uxrz5CCLb7bLVV6PpL417SyBMPc2Zo/uoREc2EOHlzHwHwAlFtgmSngSY4WQ==",
-      "license": "Apache-2.0",
+      "version": "4.27.0",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.27.0.tgz",
+      "integrity": "sha512-LkTJrNz5socxpPnWPODQ2bQ65eYx9JK+DQMYNihpTjMCqHwgWGYQnQTCAAche2W3ZP87alA+1zYPvgS8tHNzMQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/SeleniumHQ"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/selenium"
+        }
+      ],
       "dependencies": {
         "@bazel/runfiles": "^6.3.1",
         "jszip": "^3.10.1",
diff --git a/package.json b/package.json
index e9c2f0b1..1b01b077 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "nightwatch",
   "description": "Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.",
-  "version": "3.9.0",
+  "version": "3.10.2",
   "author": "Andrei Rusu",
   "homepage": "https://nightwatchjs.org",
   "main": "./lib/index.js",
@@ -43,7 +43,7 @@
     "open": "8.4.2",
     "ora": "5.4.1",
     "piscina": "^4.3.1",
-    "selenium-webdriver": "4.26.0",
+    "selenium-webdriver": "4.27.0",
     "semver": "7.5.4",
     "stacktrace-parser": "0.1.10",
     "strip-ansi": "6.0.1",
diff --git a/types/custom-command.d.ts b/types/custom-command.d.ts
index a9bb75e0..b36e14f6 100644
--- a/types/custom-command.d.ts
+++ b/types/custom-command.d.ts
@@ -1,3 +1,7 @@
+import {CommandInstance} from './index';
+
+export interface CustomCommandInstance extends CommandInstance {}
+
 export interface NightwatchCustomCommandsModel {
   /**
    * Define a custom command
@@ -5,14 +9,14 @@ export interface NightwatchCustomCommandsModel {
    * @example
    * class LogMessage implements NightwatchCustomCommandsModel {
    *   command() {
-   *     
+   *
    *     return Promise.resolve();
    *   }
    * }
    *
    * @see https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html#define-a-custom-command
    */
-  command: (...args: any) => unknown | Promise<unknown>;
+  command: (...args: any[]) => unknown | Promise<unknown>;
 }
 
 /**
diff --git a/types/index.d.ts b/types/index.d.ts
index b56a057f..ae650929 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1153,6 +1153,77 @@ export interface NightwatchClientObject {
   sessionId: string | null;
 }
 
+export interface HttpRequestOptions {
+  /**
+   * The pathname of the endpoint to call. Ex: `'/session/:sessionId/url'`.
+   *
+   * Alternatively, url property could be provided with the full URL.
+   */
+  path?: string;
+  data?: unknown;
+
+  /**
+   * For custom-commands, set to `this.api.sessionId`.
+   */
+  sessionId: string;
+
+  method: 'POST' | 'GET' | 'DELETE' | 'PUT';
+  use_ssl?: boolean;
+  host?: string;
+  port?: number;
+
+  /**
+   * The full URL to call. Ex: `http://localhost:4444/session/:sessionId/url`.
+   */
+  url?: string;
+
+  auth?: {
+    user: string;
+    pass: string;
+  }
+}
+
+export interface CommandInstance {
+  get api(): NightwatchAPI;
+  get client(): NightwatchClient;
+  get commandArgs(): unknown[];
+  get commandFileName(): string;
+  get driver(): WebDriver;
+  get isES6AsyncCommand(): boolean;
+  get reuseBrowser(): boolean;
+
+  /**
+   * Direct access to methods present in the `lib/transport/selenium-webdriver/method-mappings.js` file
+   * of Nightwatch code.
+   *
+   * TODO: complete the type definition.
+   *
+   * For now, you would need to create custom interface to use this property, like below:
+   * ```ts
+   * interface TransportActions {
+   *   getCurrentUrl(): Promise<NightwatchCallbackResult<string>>;
+   * }
+   * ```
+   * then use it inside your custom command like:
+   * ```ts
+   * const currentUrl = await (this.transportActions as TransportActions).getCurrentUrl();
+   * ```
+   */
+  get transportActions(): unknown;
+
+  /**
+   * Directly call the HTTP endpoints of the Selenium/WebDriver server.
+   *
+   * This is useful when you need to call a command that is not directly supported by Nightwatch API.
+   *
+   * @see https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html#postdoc-directly-calling-seleniumwebdriver-endpoints
+   */
+  httpRequest(options: HttpRequestOptions): Promise<unknown>;
+
+  toString(): string;
+  complete(...args: unknown[]): void;
+}
+
 export interface CreateClientParams {
   browserName?: string | null;
   headless?: boolean;
diff --git a/types/web-element.d.ts b/types/web-element.d.ts
index 6505b388..76f7d65b 100644
--- a/types/web-element.d.ts
+++ b/types/web-element.d.ts
@@ -360,10 +360,9 @@ export type ScopedElementRect = {
   readonly height: number;
 };
 
-export type DragAndDropDestination = {
-  readonly xOffset: number;
-  readonly yOffset: number;
-};
+export type DragAndDropDestination = 
+  | {readonly x: number; readonly y: number;}
+  | WebElement
 
 export interface ElementFunction
   extends Pick<


Test patch:
diff --git a/lib/testsuite/context.js b/lib/testsuite/context.js
index b32f9f9c..f12e9715 100644
--- a/lib/testsuite/context.js
+++ b/lib/testsuite/context.js
@@ -316,7 +316,20 @@ class Context extends EventEmitter {
       throw new Error(`The "${testName}" test script must be a function. "${typeof testFn}" given.`);
     }
 
-    // TODO: warn if test name already exists
+    if (this.allScreenedTests.includes(testName)) {
+      const {Logger} = Utils;
+
+      const err = new Error(
+        'An error occurred while loading the testsuite:\n' +
+        `A testcase with name "${testName}" already exists. Testcases must have unique names inside the test suite, ` +
+        'otherwise testcases with duplicate names might not run at all.\n\n' +
+        'This testsuite has been disabled, please fix the error to run it again properly.'
+      );
+      Logger.error(err);
+
+      this.setAttribute('@disabled', true);
+    }
+
     if (!skipTest) {
       this.tests.push(testName);
     } else {
@@ -512,7 +525,12 @@ class Context extends EventEmitter {
       args[0] = client.api;
     }
 
-    return this.testsuite[fnName].apply(context, args);
+    const result = this.testsuite[fnName].apply(context, args);
+    if (this.currentRunnable) {
+      this.currentRunnable.currentTestCaseResult = result;
+    }
+
+    return result;
   }
 
   /**
@@ -528,7 +546,12 @@ class Context extends EventEmitter {
     const fnAsync = Utils.makeFnAsync(expectedArgs, this.testsuite[fnName], context);
     const args = this.getHookFnArgs(done, api, expectedArgs);
 
-    return fnAsync.apply(context, args);
+    const result = fnAsync.apply(context, args);
+    if (this.currentRunnable) {
+      this.currentRunnable.currentTestCaseResult = result;
+    }
+
+    return result;
   }
 
   ////////////////////////////////////////////////////////////////
diff --git a/lib/testsuite/interfaces/describe.js b/lib/testsuite/interfaces/describe.js
index 35d4d1e6..d0bd0a45 100644
--- a/lib/testsuite/interfaces/describe.js
+++ b/lib/testsuite/interfaces/describe.js
@@ -229,8 +229,10 @@ class Describe extends Common {
       context.xcontext =
         context.describe.skip = (title, describeFn) => {
           this.instance.once('module-loaded', () => {
-            // in case tests have been declared using other interfaces (e.g. exports)
+            // in case tests have been declared using other interfaces (e.g. exports),
+            // we do not want to disable the suite.
             if (this.instance.tests.length === 0) {
+              // if no tests are added after all interfaces are loaded, disable the suite.
               this.instance.setAttribute('@disabled', true);
             }
           });
diff --git a/lib/testsuite/repl.js b/lib/testsuite/repl.js
index 3acc45f4..cf056691 100644
--- a/lib/testsuite/repl.js
+++ b/lib/testsuite/repl.js
@@ -62,7 +62,7 @@ module.exports = class NightwatchRepl {
   }
 
   async _handleResult(result, callback) {
-    const resultIsPromise = result instanceof Promise;
+    const resultIsPromise = result instanceof Promise || (result && typeof result.then === 'function');
 
     if (!resultIsPromise) {
       return callback(null, result);
diff --git a/lib/testsuite/runnable.js b/lib/testsuite/runnable.js
index b60f43d3..bb42d03e 100644
--- a/lib/testsuite/runnable.js
+++ b/lib/testsuite/runnable.js
@@ -103,8 +103,8 @@ class Runnable {
   }
 
   setDoneCallback(cb) {
-    let originalResolve = this.deffered.resolveFn;
-    let originalReject = this.deffered.rejectFn;
+    const originalResolve = this.deffered.resolveFn;
+    const originalReject = this.deffered.rejectFn;
     this.deffered.resolveFn = function() {};
     this.deffered.rejectFn = function() {};
 
@@ -159,7 +159,32 @@ class Runnable {
       }
     }
 
-    this.queue.run().then(err => {
+    // `this.currentTestCaseResult` represents the return value of the currently
+    // running test case or hook.
+    // in case the runnable is executing something other than a test case/hook,
+    // `this.currentTestCaseResult` will be `undefined`.
+    if (this.currentTestCaseResult instanceof Promise) {
+      this.currentTestCaseResult
+        .catch(() => {
+          // to avoid unhandledRejections
+          // although the test case promises are already handled for rejections
+          // above (`result.catch()`), if we don't use `.catch()` here again,
+          // `.finally` will return a new promise that will be rejected without
+          // any error handling.
+        })
+        .finally(() => {
+          // mark the promise as settled as a cue to the asynctree so that it
+          // can get cleared out and subsequently call the queue `done` method.
+          this.currentTestCaseResult.settled = true;
+
+          // sometimes this promise is settled after the last call of
+          // asynctree `done` method, so we need schedule a tree traversal
+          // again to clear out the tree and call the queue `done` method.
+          this.queue.scheduleTraverse();
+        });
+    }
+
+    this.queue.run(this.currentTestCaseResult).then(err => {
       if (err) {
         return this.deffered.rejectFn(err);
       }
diff --git a/test/extra/commands/customCommandWithFailureClass.js b/test/extra/commands/customCommandWithFailureClass.js
new file mode 100644
index 00000000..5c869a6c
--- /dev/null
+++ b/test/extra/commands/customCommandWithFailureClass.js
@@ -0,0 +1,5 @@
+module.exports = class CustomCommandWithFailureClass{
+  async command() {
+    await this.api.waitForElementPresent('#badElement', 100);
+  }
+};
diff --git a/test/sampletests/webelement/findWithSuppressNotFoundErrors.js b/test/sampletests/webelement/findWithSuppressNotFoundErrors.js
new file mode 100644
index 00000000..3f71876f
--- /dev/null
+++ b/test/sampletests/webelement/findWithSuppressNotFoundErrors.js
@@ -0,0 +1,8 @@
+describe('test', function () {
+  test('test find with suppressNotFoundErrors', async (browser) => {
+    browser.element.find({
+      selector: browser.globals.selector,
+      suppressNotFoundErrors: browser.globals.suppressNotFoundErrors
+    });
+  });
+});
diff --git a/test/sampletests/withcustomcommands/sampleWithAsyncFailures.js b/test/sampletests/withcustomcommands/sampleWithAsyncFailures.js
new file mode 100644
index 00000000..c89c227a
--- /dev/null
+++ b/test/sampletests/withcustomcommands/sampleWithAsyncFailures.js
@@ -0,0 +1,15 @@
+module.exports = {
+  before(client) {
+    client.globals.increment++;
+  },
+
+  async demoTestAsync(client) {
+    await client.url('http://localhost').customCommandWithFailureClass();
+    // below statement should be unreachable because the custom command should be rejected
+    client.globals.increment++;
+  },
+
+  after(client) {
+    client.globals.increment++;
+  }
+};
\ No newline at end of file
diff --git a/test/sampletests/withdescribe/basic2/sample.js b/test/sampletests/withdescribe/basic2/sample.js
new file mode 100644
index 00000000..6a03ec49
--- /dev/null
+++ b/test/sampletests/withdescribe/basic2/sample.js
@@ -0,0 +1,80 @@
+const assert = require('assert');
+
+describe('basic describe test', async function () {
+  const nwClient = this['[client]'];
+
+  const asynctreeEventListener = () => {
+    this.globals.asynctreeFinishedEventCount++;
+  };
+
+  const queueEventListener = () => {
+    this.globals.queueFinishedEventCount++;
+  };
+
+  this.desiredCapabilities = {
+    name: 'basicDescribe'
+  };
+
+  test('with awaited JS command bw Nightwatch commands', async function (client) {
+    nwClient.queue.tree.on('asynctree:finished', asynctreeEventListener);
+
+    // queue event listeners are automatically removed after
+    // every (test case + afterEach + after (if applicable))
+    // so, no need to remove this listener manually before next test case run.
+    nwClient.queue.on('queue:finished', queueEventListener);
+
+    await client.assert.strictEqual(client.options.desiredCapabilities.name, 'basicDescribe');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 1);
+
+    await client.url('http://localhost');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 2);
+
+    await new Promise((resolve) => {
+      setTimeout(function() {
+        resolve(null);
+      }, 50);
+    });
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 2);
+
+    await client.assert.elementPresent('#weblogin');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 3);
+
+    // queue:finished event shouldn't be emitted in between the test.
+    assert.equal(this.globals.queueFinishedEventCount, 0);
+  });
+
+  test('with await in last command', async function (client) {
+    nwClient.queue.tree.on('asynctree:finished', asynctreeEventListener);
+
+    await client.assert.strictEqual(client.options.desiredCapabilities.name, 'basicDescribe');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 1);
+
+    await client.url('http://localhost');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 2);
+
+    await client.assert.elementPresent('#weblogin');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 3);
+  });
+
+  test('without await in last command', async function (client) {
+    nwClient.queue.tree.on('asynctree:finished', asynctreeEventListener);
+
+    await client.assert.strictEqual(client.options.desiredCapabilities.name, 'basicDescribe');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 1);
+
+    await client.url('http://localhost');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 2);
+
+    client.assert.elementPresent('#weblogin');
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 3);
+  });
+
+  afterEach(function () {
+    nwClient.queue.tree.removeListener('asynctree:finished', asynctreeEventListener);
+    assert.equal(nwClient.queue.tree.rootNode.childNodes.length, 0);
+  });
+
+  after(function (client) {
+    client.end();
+  });
+});
diff --git a/test/sampletests/withdescribe/skipped/duplicateTestcases.js b/test/sampletests/withdescribe/skipped/duplicateTestcases.js
new file mode 100644
index 00000000..c3ec3519
--- /dev/null
+++ b/test/sampletests/withdescribe/skipped/duplicateTestcases.js
@@ -0,0 +1,11 @@
+describe('test', function () {
+  test('test find', async (browser) => {
+    browser.globals.calls++;
+    browser.element.find('#weblogin');
+  });
+
+  test('test find', async (browser) => {
+    browser.globals.calls++;
+    browser.element.find('#weblogin');
+  });
+});
diff --git a/test/src/api/commands/web-element/testFindOnErrors.js b/test/src/api/commands/web-element/testFindOnErrors.js
new file mode 100644
index 00000000..09810ed4
--- /dev/null
+++ b/test/src/api/commands/web-element/testFindOnErrors.js
@@ -0,0 +1,171 @@
+const assert = require('assert');
+const path = require('path');
+const MockServer  = require('../../../../lib/mockserver.js');
+const Mocks = require('../../../../lib/command-mocks.js');
+const CommandGlobals = require('../../../../lib/globals/commands-w3c.js');
+const common = require('../../../../common.js');
+const NightwatchClient = common.require('index.js');
+const {settings} = common;
+
+describe('element().isPresent() command', function() {
+  before(function (done) {
+    CommandGlobals.beforeEach.call(this, done);
+
+  });
+
+  after(function (done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: true) suppresses NoSuchElementError', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '#wrong'}),
+      response: JSON.stringify({
+        value: []
+      })
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        if (Object.prototype.hasOwnProperty.call(results, 'lastError')) {
+          assert.notStrictEqual(results.lastError.name, 'NoSuchElementError');
+        }
+      },
+      waitForConditionTimeout: 100,
+      selector: '#wrong',
+      suppressNotFoundErrors: true
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: false) does not suppress NoSuchElementError', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '#wrong'}),
+      response: JSON.stringify({
+        value: []
+      })
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'NoSuchElementError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '#wrong',
+      suppressNotFoundErrors: false
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: true) does not suppress other errors', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '@wrong'}),
+      response: {
+        value: {
+          error: 'invalid selector',
+          message: 'invalid selector',
+          stacktrace: ''
+        }
+      },
+      statusCode: 400
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'InvalidSelectorError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '@wrong',
+      suppressNotFoundErrors: true
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: false) does not suppress other errors', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '@wrong'}),
+      response: {
+        value: {
+          error: 'invalid selector',
+          message: 'invalid selector',
+          stacktrace: ''
+        }
+      },
+      statusCode: 400
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'InvalidSelectorError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '@wrong',
+      suppressNotFoundErrors: false
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+});
diff --git a/test/src/cli/testCliRunnerGenerate.js b/test/src/cli/testCliRunnerGenerate.js
index 7870d200..cfa21aca 100644
--- a/test/src/cli/testCliRunnerGenerate.js
+++ b/test/src/cli/testCliRunnerGenerate.js
@@ -131,7 +131,7 @@ describe('Test CLI Runner Generate', function() {
           disable_error_log: true,
           webdriver: {
             timeout_options: {
-              timeout: 15000,
+              timeout: 60000,
               retry_attempts: 3
             },
             keep_alive: true,
diff --git a/test/src/core/testAsyncTree.js b/test/src/core/testAsyncTree.js
new file mode 100644
index 00000000..676698ca
--- /dev/null
+++ b/test/src/core/testAsyncTree.js
@@ -0,0 +1,47 @@
+const assert = require('assert');
+const path = require('path');
+const CommandGlobals = require('../../lib/globals/commands-w3c.js');
+const common = require('../../common.js');
+const {runTests} = common.require('index.js');
+const {settings} = common;
+
+describe('element().isPresent() command', function() {
+  before(function (done) {
+    CommandGlobals.beforeEach.call(this, done);
+  });
+
+  after(function (done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('test runner with multiple test interfaces - exports/describe', function () {
+    const srcFolders = [
+      path.join(__dirname, '../../sampletests/withdescribe/basic2')
+    ];
+
+    const globals = {
+      asynctreeFinishedEventCount: 0,
+      queueFinishedEventCount: 0,
+      reporter(results, cb) {
+        if (results.lastError) {
+          throw results.lastError;
+        }
+
+        // should equal the number of test cases.
+        assert.equal(globals.asynctreeFinishedEventCount, 3);
+
+        // only counts for the first test case + afterEach
+        assert.equal(globals.queueFinishedEventCount, 2);
+
+        cb();
+      }
+    };
+
+    return runTests(settings({
+      globals,
+      src_folders: srcFolders,
+      output: true,
+      silent: false
+    }));
+  });
+});
diff --git a/test/src/index/testProgrammaticApis.js b/test/src/index/testProgrammaticApis.js
index e8075612..d77dd9f5 100644
--- a/test/src/index/testProgrammaticApis.js
+++ b/test/src/index/testProgrammaticApis.js
@@ -882,4 +882,85 @@ describe('test programmatic apis', function () {
     CliRunner.createDefaultConfig = createDefaultConfig;
     CliRunner.prototype.loadConfig = loadConfig;
   });
+
+  it('test multiple calls to launchBrowser() on same client', async function() {
+    const CliRunner = common.require('runner/cli/cli.js');
+    const Nightwatch = common.require('index.js');
+    MockServer.createChromeSession({sessionId: '12345678'});
+    MockServer.createChromeSession({sessionId: '87654321'});
+
+    const defaultConfig = {
+      test_settings: {
+        default: {
+          launchUrl: 'http://localhost'
+        },
+
+        chrome: {
+          desiredCapabilities: {
+            browserName: 'chrome'
+          }
+        }
+      },
+      selenium: {
+        port: 10195,
+        start_process: false
+      },
+      selenium_host: 'localhost'
+    };
+
+    const createDefaultConfig = CliRunner.createDefaultConfig;
+    const loadConfig = CliRunner.prototype.loadConfig;
+
+    CliRunner.createDefaultConfig = function(destFileName) {
+      return defaultConfig;
+    };
+
+    CliRunner.prototype.loadConfig = function () {
+      return defaultConfig;
+    };
+
+    const clientChrome = Nightwatch.createClient({
+      browserName: 'chrome',
+      headless: true
+    });
+
+    const session = await clientChrome.launchBrowser();
+
+    assert.strictEqual(session.sessionId, '12345678');
+    assert.strictEqual(session.options.webdriver.port, 10195);
+    assert.deepStrictEqual(session.capabilities, {
+      acceptInsecureCerts: false,
+      browserName: 'chrome',
+      browserVersion: '90'
+    });
+
+    await session.end();
+    assert.strictEqual(session.sessionId, null);
+
+    let launchBrowserError;
+    try {
+      await clientChrome.launchBrowser();
+    } catch (err) {
+      launchBrowserError = err;
+    }
+    assert.notStrictEqual(launchBrowserError, undefined);
+    assert.strictEqual(launchBrowserError.message.includes('Error while loading the API commands'), true);
+
+    const session2 = await clientChrome.launchBrowser({loadNightwatchApis: false});
+
+    assert.strictEqual(session2.sessionId, '87654321');
+    assert.strictEqual(session2.options.webdriver.port, 10195);
+    assert.deepStrictEqual(session2.capabilities, {
+      acceptInsecureCerts: false,
+      browserName: 'chrome',
+      browserVersion: '90'
+    });
+
+    await session2.quit();
+    // TODO: calling `.quit()` does not clear the sessionId
+    assert.notStrictEqual(session2.sessionId, null);
+
+    CliRunner.createDefaultConfig = createDefaultConfig;
+    CliRunner.prototype.loadConfig = loadConfig;
+  });
 });
diff --git a/test/src/index/transport/testBrowserstackTransport.js b/test/src/index/transport/testBrowserstackTransport.js
index 88e3ff2d..92066725 100644
--- a/test/src/index/transport/testBrowserstackTransport.js
+++ b/test/src/index/transport/testBrowserstackTransport.js
@@ -5,6 +5,7 @@ const NightwatchClient = common.require('index.js');
 const SeleniumRemote = common.require('transport/selenium-webdriver/selenium.js');
 const Automate = common.require('transport/selenium-webdriver/browserstack/automate.js');
 const AppAutomate = common.require('transport/selenium-webdriver/browserstack/appAutomate.js');
+const AutomateTurboScale = common.require('transport/selenium-webdriver/browserstack/automateTurboScale.js');
 
 describe('BrowserstackTransport', function () {
   beforeEach(function() {
@@ -15,6 +16,7 @@ describe('BrowserstackTransport', function () {
   });
 
   afterEach(function() {
+    nock.cleanAll();
     nock.restore();
   });
 
@@ -339,7 +341,90 @@ describe('BrowserstackTransport', function () {
     assert.strictEqual(transport.sessionId, null);
 
     assert.strictEqual(transport.buildId, '123-567-89');
+  });
+  
+  it('test create Transport for Browserstack - Automate TurboScale', async function() {
+    const client = NightwatchClient.client({
+      webdriver: {
+        host: 'hub-cloud.browserstack-ats.com',
+        port: 443,
+        start_process: true
+      },
+      desiredCapabilities: {
+        'browserstack.user': 'test-access-user',
+        'browserstack.key': 'test-access-key',
+        browserName: 'chrome'
+      }
+    });
 
+    nock('https://hub-cloud.browserstack-ats.com')
+      .post('/wd/hub/session')
+      .reply(201, function (uri, requestBody) {
+        return {
+          value: {
+            sessionId: '1352110219202',
+            capabilities: requestBody.capabilities
+          }
+        };
+      });
+    
+    nock('https://api.browserstack.com')
+      .get('/automate-turboscale/v1/builds?status=running')
+      .reply(200, {
+        'builds': [
+          {
+            name: 'nightwatch-test-build',
+            hashed_id: '123-567-89'
+          },
+          {
+            name: 'test-build'
+          }
+        ]
+      });
+
+    assert.ok(client.transport instanceof AutomateTurboScale);
+    assert.strictEqual(client.settings.webdriver.host, 'hub-cloud.browserstack-ats.com');
+    assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
+    assert.strictEqual(client.settings.webdriver.ssl, true);
+
+    const {transport} = client;
+    assert.ok(transport instanceof SeleniumRemote);
+
+    let result = await transport.createSession({argv: undefined, moduleKey: ''});
+    assert.strictEqual(result.sessionId, '1352110219202');
+
+    client.emit('nightwatch:session.create', result);
+
+    assert.strictEqual(transport.username, 'test-access-user');
+    assert.strictEqual(transport.accessKey, 'test-access-key');
+    assert.strictEqual(client.settings.webdriver.start_process, false);
+
+    let sessionNockCalled = 0;
+      
+    nock('https://api.browserstack.com')
+      .get('/automate-turboscale/v1/sessions/1352110219202')
+      .reply(200, function(){
+        sessionNockCalled++;
+
+        return {status: 'done'};
+      });
+    nock('https://api.browserstack.com')
+      .patch('/automate-turboscale/v1/sessions/1352110219202', {
+        status: 'passed',
+        reason: ''  
+      })
+      .reply(200, function(){
+        sessionNockCalled++;
+
+        return {};
+      });
+
+    result = await transport.testSuiteFinished(false);
+    assert.strictEqual(result, true);
+    assert.strictEqual(transport.sessionId, null);
+
+    assert.strictEqual(sessionNockCalled, 2);
+    assert.strictEqual(transport.buildId, '123-567-89');
   });
 
   it('test create Transport for Browserstack with failures', async function() {
diff --git a/test/src/runner/testRunTestcase.js b/test/src/runner/testRunTestcase.js
index bbf956d4..291385b0 100644
--- a/test/src/runner/testRunTestcase.js
+++ b/test/src/runner/testRunTestcase.js
@@ -266,6 +266,25 @@ describe('testRunTestcase', function() {
     }));
   });
 
+  it('testRunner with duplicated testcases', function() {
+    const testsPath = path.join(__dirname, '../../sampletests/withdescribe/skipped/duplicateTestcases.js');
+    const globals = {
+      calls: 0,
+      reporter(results, cb) {
+        assert.strictEqual(globals.calls, 0);
+        assert.strictEqual(results.skipped, 2);
+        cb();
+      },
+      retryAssertionTimeout: 0
+    };
+
+    return runTests({
+      _source: [testsPath]
+    }, settings({
+      globals
+    }));
+  });
+
   it('testRunner with skipped beforeEach afterEach hooks', function() {
     const testsPath = path.join(__dirname, '../../sampletests/withdescribe/skipped/skipBeforeAfterEach.js');
     const globals = {
diff --git a/test/src/runner/testRunWithCustomCommands.js b/test/src/runner/testRunWithCustomCommands.js
index 79d12676..fcc7956d 100644
--- a/test/src/runner/testRunWithCustomCommands.js
+++ b/test/src/runner/testRunWithCustomCommands.js
@@ -34,14 +34,14 @@ describe('testRunWithCustomCommands', function() {
   });
 
   it('testRunner with custom command which has failures', function() {
-    let testsPath = path.join(__dirname, '../../sampletests/withcustomcommands');
-    let globals = {
+    const testsPath = path.join(__dirname, '../../sampletests/withcustomcommands');
+    const globals = {
       increment: 0,
       retryAssertionTimeout: 0,
       waitForConditionPollInterval: 10,
       waitForConditionTimeout: 20,
       reporter(results, cb) {
-        assert.strictEqual(globals.increment, 4);
+        assert.strictEqual(globals.increment, 6);
         cb();
       }
     };
@@ -55,12 +55,12 @@ describe('testRunWithCustomCommands', function() {
   });
 
   it('testRunner with ES6 Async custom commands', function() {
-    let testsPath = path.join(__dirname, '../../sampletests/withes6asynccommands');
+    const testsPath = path.join(__dirname, '../../sampletests/withes6asynccommands');
     let testResults;
     const origExit = process.exit;
     process.exit = function() {};
 
-    let globals = {
+    const globals = {
       increment: 0,
       logResult: null,
       retryAssertionTimeout: 0,
@@ -101,12 +101,12 @@ describe('testRunWithCustomCommands', function() {
   });
 
   it('testRunner with ES6 Async custom commands', function() {
-    let testsPath = path.join(__dirname, '../../sampletests/withes6asynccommands');
+    const testsPath = path.join(__dirname, '../../sampletests/withes6asynccommands');
     let testResults;
     const origExit = process.exit;
     process.exit = function() {};
 
-    let globals = {
+    const globals = {
       increment: 0,
       logResult: null,
       retryAssertionTimeout: 0,
@@ -147,12 +147,12 @@ describe('testRunWithCustomCommands', function() {
   });
 
   it('testRunner custom command which extends built-in command', function() {
-    let testsPath = path.join(__dirname, '../../sampletests/withcustomcommands/element');
+    const testsPath = path.join(__dirname, '../../sampletests/withcustomcommands/element');
     let testResults;
     const origExit = process.exit;
     process.exit = function() {};
 
-    let globals = {
+    const globals = {
       increment: 0,
       logResult: null,
       retryAssertionTimeout: 0,
@@ -229,12 +229,12 @@ describe('testRunWithCustomCommands', function() {
   });
 
   it('testRunner custom command path as glob pattern', function() {
-    let testsPath = path.join(__dirname, '../../sampletests/withcustomcommands/element');
+    const testsPath = path.join(__dirname, '../../sampletests/withcustomcommands/element');
     let testResults;
     const origExit = process.exit;
     process.exit = function() {};
 
-    let globals = {
+    const globals = {
       increment: 0,
       logResult: null,
       retryAssertionTimeout: 0,
diff --git a/types/tests/customCommands.test-d.ts b/types/tests/customCommands.test-d.ts
new file mode 100644
index 00000000..67093847
--- /dev/null
+++ b/types/tests/customCommands.test-d.ts
@@ -0,0 +1,58 @@
+import {expectType} from 'tsd';
+import {CustomCommandInstance, NightwatchCallbackResult, NightwatchCustomCommandsModel} from '..';
+
+export class AngularCommand implements NightwatchCustomCommandsModel {
+  async command(this: CustomCommandInstance, listName: string, cb = function<T>(r: T) {return r}) {
+    // Script to be executed in the browser
+    const script = function(listName: string) {
+      // executed in the browser context
+      // eslint-disable-next-line
+      var elements = document.querySelectorAll('*[ng-repeat$="'+listName+'"]');
+
+      if (elements) {return elements}
+
+      return null;
+    };
+
+    // Arguments to be passed to the script function above
+    const args: [string] = [listName];
+
+    // Callback to be called when the script finishes its execution,
+    // with the result returned by the script passed as argument.
+    const callback = async function(result: any) {
+      const cbResult = await cb(result);
+
+      if (cbResult.value) {
+        return cbResult.value;
+      }
+
+      return cbResult;
+    };
+
+    // Execute the script defined above, along with arguments and
+    // the callback function.
+    await this.api.executeScript(script, args, callback);
+
+
+    // CUSTOM COMMAND WITH `this.httpRequest` EXAMPLE
+    const response = await this.httpRequest({
+      path: '/session/:sessionId/url',
+      sessionId: this.api.sessionId,
+      method: 'POST',
+      data: {
+        url: 'https://nightwatchjs.org'
+      }
+    });
+    expectType<unknown>(response);
+
+    // CUSTOM COMMAND WITH `this.transportActions` EXAMPLE (still incomplete)
+    const currentUrl = await (this.transportActions as TransportActions).getCurrentUrl();
+    expectType<NightwatchCallbackResult<string>>(currentUrl);
+
+    this.complete('hello', 1, true, {value: null});
+  }
+}
+
+interface TransportActions {
+  getCurrentUrl(): Promise<NightwatchCallbackResult<string>>;
+}
diff --git a/types/tests/webElement.test-d.ts b/types/tests/webElement.test-d.ts
index 6bd10a5e..c0634a29 100644
--- a/types/tests/webElement.test-d.ts
+++ b/types/tests/webElement.test-d.ts
@@ -191,7 +191,8 @@ describe('new element() api', function () {
     expectType<Promise<WebElement>>(elem.submit());
     expectType<Promise<WebElement>>(elem.setProperty('type', 'text'));
     expectType<Promise<WebElement>>(elem.setAttribute('role', 'button'));
-    expectType<Promise<WebElement>>(elem.dragAndDrop({xOffset: 150, yOffset: 500}));
+    expectType<Promise<WebElement>>(elem.dragAndDrop({x: 150, y: 500}));
+    expectType<Promise<WebElement>>(elem.dragAndDrop(elem.webElement));
     expectType<Promise<WebElement>>(elem.moveTo(100, 100));
     expectType<Promise<WebElement>>(elem.clickAndHold());
     expectType<Promise<WebElement>>(elem.doubleClick());