Repository: nightwatchjs/nightwatch
Pull request #3148 (2022-04-13 06:21:09)

fix: accept standard browserstack environment variables
BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY are the env variables specified in docs, check for these. Also accept buildName, local if sent inside 'bstack:options'
Added an issue here: https://github.com/nightwatchjs/nightwatch/issues/3149

Thanks in advance for your contribution. Please follow the below steps in submitting a pull request, as it will help us with reviewing it quicker.

- [x] Create a new branch from master (e.g. `features/my-new-feature` or `issue/123-my-bugfix`);
- [x] If you're fixing a bug also create an issue if one doesn't exist yet;
- [ ] If it's a new feature explain why do you think it's necessary. Please check with the maintainers beforehand to make sure it is something that we will accept. Usually we only accept new features if we feel that they will benefit the entire community;
- [ ] Please avoid sending PRs which contain drastic or low level changes. If you are certain that the changes are needed, please discuss them beforehand and indicate what the impact will be;
- [ ] If your change is based on existing functionality please consider refactoring first. Pull requests that duplicate code will most likely be ignored;
- [ ] Do not include changes that are not related to the issue at hand;
- [ ] Follow the same coding style with regards to spaces, semicolons, variable naming etc.;
- [ ] Always add unit tests - PRs without tests are most of the times ignored.


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/nightwatchjs/nightwatch?pullRequest=3148) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/nightwatchjs/nightwatch?pullRequest=3148) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/nightwatchjs/nightwatch?pullRequest=3148) it.</sub>
gravityvi: Thanks, @Ankit098 for raising this PR, can you fix the lint issues.

Patch:
diff --git a/.github/workflows/build-node.yaml b/.github/workflows/build-node.yaml
index fa5484c2..5826f553 100644
--- a/.github/workflows/build-node.yaml
+++ b/.github/workflows/build-node.yaml
@@ -16,7 +16,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [12.x, 14.x, 15.x, 16.x]
+        node-version: [12.x, 14.x, 16.x]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
 
     steps:
@@ -34,9 +34,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [12.x, 14.x, 15.x, 16.x]
-        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
-
+        node-version: [16.x]
     steps:
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
diff --git a/README.md b/README.md
index c6609cc8..dba77b41 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ Nightwatch uses a [WebDriver](https://www.w3.org/TR/webdriver/) compatible serve
 Nightwatch includes support for automatically managing the following services:
 #### ChromeDriver 
 - for running tests against the Chrome browser;
-- download url: [https://sites.google.com/a/chromium.org/chromedriver/downloads](https://sites.google.com/a/chromium.org/chromedriver/downloads).
+- download url: [https://sites.google.com/chromium.org/driver/](https://sites.google.com/chromium.org/driver/).
 
 #### GeckoDriver
 - for running tests against the Mozilla Firefox browser;
diff --git a/cucumber-js/_setup_cucumber_runner.js b/cucumber-js/_setup_cucumber_runner.js
index 8cf5e084..62553828 100644
--- a/cucumber-js/_setup_cucumber_runner.js
+++ b/cucumber-js/_setup_cucumber_runner.js
@@ -63,7 +63,15 @@ Before(function({pickle}) {
   }
 });
 
-After(async function() {
+After(async function(testCase) {
+
+  //send test-case result to cloud provider
+  const {result} = testCase;
+  if (this.client && result) {
+    const error = result.status === 'FAILED' ? new Error(result.message) : null;
+    await this.client.transport.testSuiteFinished(error);
+  }
+
   if (this.browser) {
     await this.browser.quit();
   }
diff --git a/lib/api/_loaders/element-global.js b/lib/api/_loaders/element-global.js
index f250921a..c4448b4d 100644
--- a/lib/api/_loaders/element-global.js
+++ b/lib/api/_loaders/element-global.js
@@ -156,7 +156,17 @@ class ElementGlobal {
     if (locator instanceof Element) {
       this.locator = locator;
     } else {
-      this.locator = Locator.create(locator);
+      let value;
+      if (isString(locator) && this.client && this.client.locateStrategy) {
+        value = {
+          value: locator,
+          using: this.client.locateStrategy
+        };  
+      } else {
+        value = locator;
+      }
+
+      this.locator = Locator.create(value);
     }
   }
 
diff --git a/lib/api/_loaders/expect.js b/lib/api/_loaders/expect.js
index ce373b21..23583f89 100644
--- a/lib/api/_loaders/expect.js
+++ b/lib/api/_loaders/expect.js
@@ -108,6 +108,13 @@ class ExpectLoader extends BaseCommandLoader {
         stackTrace
       });
 
+      if (isES6Async && (expectCommand.instance instanceof Promise)) {
+        // prevent unhandledRejection errors
+        expectCommand.instance.catch(err => {
+          deferred.reject(err);
+        });
+      }
+      
       return expectCommand.instance;
     }.bind(this));
   }
diff --git a/lib/api/assertions/domPropertyMatches.js b/lib/api/assertions/domPropertyMatches.js
index fbcb8481..e6945ecf 100644
--- a/lib/api/assertions/domPropertyMatches.js
+++ b/lib/api/assertions/domPropertyMatches.js
@@ -5,15 +5,15 @@
  * @example
  * 
  * ```
- *    this.demoTest = function (browser) {
- *      browser.assert.domPropertyMatches('#main', 'tagName', '(frame)');
- *
+ * this.demoTest = function (browser) {
+ *   browser.assert.domPropertyMatches('#main', 'tagName', /^frame/);
+ * }
  * ```
  * 
  * @method domPropertyMatches
  * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide#element-properties).
  * @param {string} domProperty The DOM property name.
- * @param {string|RegExp} regexExpression TRegex expression to match text.
+ * @param {string|RegExp} regexExpression Regex to match against.
  * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default.
  * @api assertions
  */
diff --git a/lib/api/assertions/titleContains.js b/lib/api/assertions/titleContains.js
index 70fb5dd8..d0206e02 100644
--- a/lib/api/assertions/titleContains.js
+++ b/lib/api/assertions/titleContains.js
@@ -7,8 +7,8 @@
  *    };
  * ```
  *
- * @method title
- * @param {string} expected The expected page title.
+ * @method titleContains
+ * @param {string} value The value to look for.
  * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default.
  * @api assertions
  */
diff --git a/lib/api/element-commands/moveToElement.js b/lib/api/element-commands/moveToElement.js
index dc17bcf8..d97bd2a0 100644
--- a/lib/api/element-commands/moveToElement.js
+++ b/lib/api/element-commands/moveToElement.js
@@ -14,8 +14,8 @@ const BaseElementCommand = require('./_baseElementCommand.js');
  * @syntax .moveToElement(using, selector, xoffset, yoffset, [callback])
  * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies)
  * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide#element-properties).
- * @param {number} xoffset X offset to move to, relative to the top-left corner of the element.
- * @param {number} yoffset Y offset to move to, relative to the top-left corner of the element.
+ * @param {number} xoffset X offset to move to, relative to the center of the element.
+ * @param {number} yoffset Y offset to move to, relative to the center of the element.
  * @param {function} [callback] Optional callback function to be called when the command finishes.
  * @jsonwire
  * @api protocol.elementinteraction
diff --git a/lib/api/protocol/frame.js b/lib/api/protocol/frame.js
index 6f3cc520..28562042 100644
--- a/lib/api/protocol/frame.js
+++ b/lib/api/protocol/frame.js
@@ -1,4 +1,6 @@
 const ProtocolAction = require('./_base-action.js');
+const {isString} = require('../../utils');
+const {By} = require('selenium-webdriver');
 
 /**
  * Change focus to another frame on the page. If the frame id is missing or null, the server should switch to the page's default content.
@@ -8,7 +10,13 @@ const ProtocolAction = require('./_base-action.js');
  *    browser.frame('<ID>', function(result) {
  *      console.log(result);
  *    });
- * }
+ *  }
+ *  
+ *  this.demoTest = function (browser) {
+ *    browser.frame('<NAME>', function(result) {
+ *      console.log(result);
+ *    });
+ *  }
  *
  * @link /#switch-to-frame
  * @param {string|number|null} [frameId] Identifier for the frame to change focus to.
@@ -16,11 +24,22 @@ const ProtocolAction = require('./_base-action.js');
  * @api protocol.frames
  */
 module.exports = class Session extends ProtocolAction {
-  command(frameId, callback) {
+  async command(frameId, callback) {
     if (arguments.length === 1 && typeof frameId === 'function') {
       callback = frameId;
 
       return this.transportActions.switchToFrame(callback);
+    } 
+    
+    if (arguments.length === 2 && isString(frameId)){ 
+      try {
+        frameId = await this.driver.findElement(By.css(`#${frameId}, *[name="${frameId}"]`));
+      } catch (err){
+        return {
+          status: -1,
+          error: err
+        };
+      }
     }
 
     return this.transportActions.switchToFrame(frameId, callback);
diff --git a/lib/api/protocol/moveTo.js b/lib/api/protocol/moveTo.js
index e64256ef..17f3c554 100644
--- a/lib/api/protocol/moveTo.js
+++ b/lib/api/protocol/moveTo.js
@@ -14,8 +14,8 @@ const ProtocolAction = require('./_base-action.js');
  * @syntax .moveTo(null, xoffset, yoffset, [callback])
  * @editline L1335
  * @param {string} [webElementId] The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) assigned to the element to move to. If not specified or is null, the offset is relative to current position of the mouse.
- * @param {number} xoffset X offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
- * @param {number} yoffset Y offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
+ * @param {number} xoffset X offset to move to, relative to the center of the element. If not specified, the mouse will move to the middle of the element.
+ * @param {number} yoffset Y offset to move to, relative to the center of the element. If not specified, the mouse will move to the middle of the element.
  * @param {function} [callback] Optional callback function to be called when the command finishes.
  * @api protocol.useractions
  */
diff --git a/lib/index.js b/lib/index.js
index 6c599d59..741430d1 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -131,6 +131,11 @@ Nightwatch.createClient = function({
         return client.settings;
       }
     },
+    transport: {
+      get: function() {
+        return client.transport;
+      }
+    },
     nightwatch_client: {
       configurable: true,
       get: function() {
diff --git a/lib/reporter/results.js b/lib/reporter/results.js
index 661905be..3b82863c 100644
--- a/lib/reporter/results.js
+++ b/lib/reporter/results.js
@@ -36,7 +36,7 @@ module.exports = class Results {
       tests: 0
     };
 
-    this.initCount();
+    this.initCount(tests);
   }
 
   get initialResult() {
@@ -320,11 +320,11 @@ module.exports = class Results {
     return this;
   }
 
-  initCount() {
+  initCount(tests) {
     this.__passedCount = 0;
     this.__failedCount = 0;
     this.__errorsCount = 0;
-    this.__skippedCount = 0;
+    this.__skippedCount = tests.length;
     this.__testsCount = 0;
     this.__timestamp = new Date().toUTCString();
     this.errmessages = [];
@@ -368,6 +368,7 @@ module.exports = class Results {
     let index = this.skipped.indexOf(testName);
     if (index > -1) {
       this.skipped.splice(index, 1);
+      this.__skippedCount -= 1;
     }
 
     this.__testsCount++;
diff --git a/lib/runner/cli/nightwatch.conf.ejs b/lib/runner/cli/nightwatch.conf.ejs
index cb7853ce..17475e1c 100644
--- a/lib/runner/cli/nightwatch.conf.ejs
+++ b/lib/runner/cli/nightwatch.conf.ejs
@@ -171,21 +171,21 @@ module.exports = {
     // Configuration for when using the browserstack.com cloud service               |
     //                                                                               |
     // Please set the username and access key by setting the environment variables:  |
-    // - BROWSERSTACK_USER                                                           |
-    // - BROWSERSTACK_KEY                                                            |
+    // - BROWSERSTACK_USERNAME                                                       |
+    // - BROWSERSTACK_ACCESS_KEY                                                     |
     // .env files are supported                                                      |
     //////////////////////////////////////////////////////////////////////////////////
     browserstack: {
       selenium: {
-        host: 'hub-cloud.browserstack.com',
+        host: 'hub.browserstack.com',
         port: 443
       },
       // More info on configuring capabilities can be found on:
       // https://www.browserstack.com/automate/capabilities?tag=selenium-4
       desiredCapabilities: {
         'bstack:options' : {
-          userName: '${BROWSERSTACK_USER}',
-          accessKey: '${BROWSERSTACK_KEY}',
+          userName: '${BROWSERSTACK_USERNAME}',
+          accessKey: '${BROWSERSTACK_ACCESS_KEY}',
         }
       },
 
@@ -253,6 +253,61 @@ module.exports = {
       }
     },
     //////////////////////////////////////////////////////////////////////////////////
+    // Configuration for when using the SauceLabs cloud service                      |
+    //                                                                               |
+    // Please set the username and access key by setting the environment variables:  |
+    // - SAUCE_USERNAME                                                              |
+    // - SAUCE_ACCESS_KEY                                                            |
+    //////////////////////////////////////////////////////////////////////////////////
+    saucelabs: {
+      selenium: {
+        host: 'ondemand.saucelabs.com',
+        port: 443
+      },
+      // More info on configuring capabilities can be found on:
+      // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options
+      desiredCapabilities: {
+        'sauce:options' : {
+          username: '${SAUCE_USERNAME}',
+          accessKey: '${SAUCE_ACCESS_KEY}',
+          // https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/#--region
+          // region: 'us-west-1'
+          // https://docs.saucelabs.com/dev/test-configuration-options/#tunnelidentifier
+          // parentTunnel: '',
+          // tunnelIdentifier: '',
+        }
+      },
+      disable_error_log: false,
+      webdriver: {
+        start_process: false
+      }
+    },
+    'saucelabs.chrome': {
+      extends: 'saucelabs',
+      desiredCapabilities: {
+        browserName: 'chrome',
+        screenResolution: '1280x1024',
+        browserVersion: 'latest',
+        javascriptEnabled: true,
+        acceptSslCerts: true,
+        timeZone: 'London',
+        chromeOptions : {
+          w3c: true
+        }
+      }
+    },
+    'saucelabs.firefox': {
+      extends: 'saucelabs',
+      desiredCapabilities: {
+        browserName: 'firefox',
+        screenResolution: '1280x1024',
+        browserVersion: 'latest',
+        javascriptEnabled: true,
+        acceptSslCerts: true,
+        timeZone: 'London'
+      }
+    },
+    //////////////////////////////////////////////////////////////////////////////////
     // Configuration for when using the Selenium service, either locally or remote,  |
     //  like Selenium Grid                                                           |
     //////////////////////////////////////////////////////////////////////////////////
diff --git a/lib/transport/selenium-webdriver/browserstack.js b/lib/transport/selenium-webdriver/browserstack.js
index 4a21fade..fa8289de 100644
--- a/lib/transport/selenium-webdriver/browserstack.js
+++ b/lib/transport/selenium-webdriver/browserstack.js
@@ -8,15 +8,19 @@ class Browserstack extends Selenium {
   }
 
   get accessKey() {
-    return this.settings.desiredCapabilities['browserstack.key'];
+    return this.bstackOptions.accessKey;
   }
 
   get username() {
-    return this.settings.desiredCapabilities['browserstack.user'];
+    return this.bstackOptions.userName;
   }
 
   get build() {
-    return this.settings.desiredCapabilities.build;
+    return this.bstackOptions.buildName;
+  }
+
+  get local() {
+    return this.bstackOptions.local;
   }
 
   constructor(nightwatchInstance, browserName) {
@@ -25,7 +29,6 @@ class Browserstack extends Selenium {
     this.adaptSettings();
     this.verifySettings();
     this.useLocal = false;
-
     this.nightwatchInstance.on('nightwatch:session.create', (data) => {
       this.sessionId = data.sessionId;
       this.getBuildId().then(buildId => {
@@ -41,19 +44,31 @@ class Browserstack extends Selenium {
     this.settings.webdriver.start_process = false;
     this.settings.webdriver.port = Number(this.settings.webdriver.port);
 
+    const {desiredCapabilities} = this.settings;
+    //checking for legacy-ways for providing config
+    this.bstackOptions = {
+      userName: desiredCapabilities['browserstack.user'], 
+      accessKey: desiredCapabilities['browserstack.key'], 
+      buildName: desiredCapabilities.build, 
+      local: desiredCapabilities['browserstack.local']
+    };
+
+    Object.assign(this.bstackOptions, this.settings.desiredCapabilities['bstack:options']);
+
+
     if (!this.accessKey && process.env.BROWSERSTACK_KEY) {
-      this.settings.desiredCapabilities['browserstack.key'] = process.env.BROWSERSTACK_KEY;
+      this.bstackOptions.accessKey = process.env.BROWSERSTACK_KEY;
     }
 
     if (!this.username && process.env.BROWSERSTACK_USER) {
-      this.settings.desiredCapabilities['browserstack.user'] = process.env.BROWSERSTACK_USER;
+      this.bstackOptions.userName = process.env.BROWSERSTACK_USER;
     }
 
     if (!this.build) {
-      this.settings.desiredCapabilities.build = 'nightwatch-test-build';
+      this.bstackOptions.buildName = 'nightwatch-test-build';
     }
 
-    if (this.settings.desiredCapabilities['browserstack.local']) {
+    if (this.local) {
       this.useLocal = true;
     }
   }
@@ -98,29 +113,29 @@ class Browserstack extends Selenium {
     }
   }
 
-  async testSuiteFinished(failures) {
-    try {
-      let reason = failures instanceof Error;
-      if (reason) {
-        reason = stripAnsi(`${failures.name}: ${failures.message}`);
-      } else {
-        reason = '';
+  async sendReasonToBrowserstack(isFailure = false, reason = '') {
+    reason = stripAnsi(reason);
+    await this.sendHttpRequest({
+      url: `${Browserstack.ApiUrl}/sessions/${this.sessionId}.json`,
+      method: 'PUT',
+      use_ssl: true,
+      data: {
+        status: isFailure ? 'failed' : 'passed',
+        reason
+      },
+      auth: {
+        user: this.username,
+        pass: this.accessKey
       }
+    });
 
-      await this.sendHttpRequest({
-        url: `${Browserstack.ApiUrl}/sessions/${this.sessionId}.json`,
-        method: 'PUT',
-        use_ssl: true,
-        data: {
-          status: !!failures ? 'failed' : 'passed',
-          reason
-        },
-        auth: {
-          user: this.username,
-          pass: this.accessKey
-        }
-      });
+  }
 
+  async testSuiteFinished(failures) {
+    
+    try {
+      const reason = failures instanceof Error ? `${failures.name}: ${failures.message}` : '';
+      await this.sendReasonToBrowserstack(!!failures, reason);
       // eslint-disable-next-line no-console
       console.log('\n  ' + 'See more info, video, & screenshots on Browserstack:\n' +
         '  ' + Logger.colors.light_cyan(`https://automate.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index 418018e8..c62f6abd 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -1,4 +1,4 @@
-const {WebElement, Origin, By} = require('selenium-webdriver');
+const {WebElement, Origin} = require('selenium-webdriver');
 const {Locator} = require('../../element');
 const {isString} = require('../../utils');
 
@@ -586,9 +586,10 @@ module.exports = class MethodMappings {
 
         async sendKeysToElement(webElementOrId, value) {
           if (Array.isArray(value)) {
+            if (value.includes(undefined) || value.includes(null)){
+              throw TypeError('each key must be a number or string; got ' + value);
+            }
             value = value.join('');
-          } else {
-            value = String(value);
           }
 
           const element = this.getWebElement(webElementOrId);
diff --git a/lib/utils/index.js b/lib/utils/index.js
index a49e1e50..5471c1d9 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -20,6 +20,7 @@ const getAllClassMethodNames = require('./getAllClassMethodNames.js');
 const printVersionInfo = require('./printVersionInfo.js');
 const {filterStack, filterStackTrace, showStackTrace, stackTraceFilter, errorToStackTrace} = require('./stackTrace.js');
 const beautifyStackTrace = require('./beautifyStackTrace.js');
+const SafeJSON = require('./safeStringify.js');
 const formatRegExp = /%[sdj%]/g;
 const testSuiteNameRegxp = /(_|-|\.)*([A-Z]*)/g;
 const nameSeparatorRegxp = /(\s|\/)/;
@@ -543,6 +544,7 @@ lodashMerge(Utils, {
   requireModule,
   createPromise,
   getAllClassMethodNames,
+  SafeJSON,
 
   filterStack,
   filterStackTrace,
diff --git a/lib/utils/safeStringify.js b/lib/utils/safeStringify.js
new file mode 100644
index 00000000..d99fd43d
--- /dev/null
+++ b/lib/utils/safeStringify.js
@@ -0,0 +1,46 @@
+class SafeStringify {
+  static visit(obj, seen) {
+    if (obj == null || typeof obj !== 'object') {
+      return obj;
+    }
+
+    if (seen.indexOf(obj) !== -1) {
+      return '[Circular]';
+    }
+    seen.push(obj);
+
+    if (typeof obj.toJSON === 'function') {
+      const result = this.visit(obj.toJSON());
+      seen.pop();
+
+      return result;
+    }
+    if (Array.isArray(obj)) {
+      const result = obj.map(val => this.visit(val, seen));
+      seen.pop();
+
+      return result;
+    }
+
+    const result = Object.keys(obj).reduce((result, prop) => {
+      result[prop] = this.visit(obj[prop], seen);
+      
+      return result;
+    }, {});
+    seen.pop();
+    
+    return result;
+  }
+
+  static safeJSON(obj) {
+    const seen = []; 
+    
+    return this.visit(obj, seen);
+  }
+
+  static stringify(obj) {
+    return JSON.stringify(this.safeJSON(obj));
+  }
+}
+
+module.exports = SafeStringify;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index eeef6232..c2549d75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nightwatch",
-  "version": "2.1.3",
+  "version": "2.1.4",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "nightwatch",
-      "version": "2.1.3",
+      "version": "2.1.4",
       "license": "MIT",
       "dependencies": {
         "@nightwatch/chai": "5.0.2",
@@ -865,14 +865,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/@nightwatch/chai/node_modules/pathval": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
-      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/@nightwatch/ejs": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/@nightwatch/ejs/-/ejs-3.2.0.tgz",
@@ -5366,10 +5358,9 @@
       }
     },
     "node_modules/pathval": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
-      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
       "engines": {
         "node": "*"
       }
@@ -7414,13 +7405,6 @@
         "loupe": "2.3.4",
         "pathval": "1.1.1",
         "type-detect": "4.0.8"
-      },
-      "dependencies": {
-        "pathval": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
-          "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
-        }
       }
     },
     "@nightwatch/ejs": {
@@ -10860,10 +10844,9 @@
       "peer": true
     },
     "pathval": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
-      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
-      "dev": true
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
     },
     "pend": {
       "version": "1.2.0",
diff --git a/package.json b/package.json
index 6ad869d6..2681b05f 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": "2.1.3",
+  "version": "2.1.4",
   "author": "Andrei Rusu",
   "homepage": "https://nightwatchjs.org",
   "main": "./lib/index.js",


Test patch:
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 408aedc6..5d051a4e 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -9,7 +9,7 @@ const SuiteRetries = require('./retries.js');
 const NightwatchClient = require('../core/client.js');
 const Concurrency = require('../runner/concurrency');
 const ElementGlobal = require('../api/_loaders/element-global.js');
-const {Logger, Screenshots, alwaysDisplayError, isString, isFunction} = require('../utils');
+const {Logger, Screenshots, alwaysDisplayError, isString, isFunction, SafeJSON} = require('../utils');
 const {By, Key, locateWith, withTagName} = require('selenium-webdriver');
 
 class TestSuite {
@@ -279,7 +279,7 @@ class TestSuite {
     Object.defineProperty(global, 'element', {
       configurable: true,
       value: function(locator, options = {}) {
-        return ElementGlobal.element({locator, testSuite: this, options});
+        return ElementGlobal.element({locator, testSuite: this, options, client: this.client});
       }.bind(this),
       writable: false
     });
@@ -576,7 +576,7 @@ class TestSuite {
     this.currentRunnable = null;
 
     if (Concurrency.isChildProcess() && typeof process.send == 'function') {
-      process.send(JSON.stringify({
+      process.send(SafeJSON.stringify({
         type: 'testsuite_finished',
         itemKey: process.env.__NIGHTWATCH_ENV_LABEL,
         results: this.reporter.exportResults()
diff --git a/test/apidemos/elements/testGlobalLocateStrategy.js b/test/apidemos/elements/testGlobalLocateStrategy.js
new file mode 100644
index 00000000..8d8d7a4a
--- /dev/null
+++ b/test/apidemos/elements/testGlobalLocateStrategy.js
@@ -0,0 +1,16 @@
+const assert = require('assert');
+const {WebElement} = require('selenium-webdriver');
+
+describe('element-global demo for locateStrategy', function () {
+  after(browser => browser.end());
+
+  test('to determine whether or not globally set locate-strategy is ignored',  async function() {
+    const weblogin = element('//weblogin');
+    const webElement = await weblogin.getWebElement();
+
+    assert.ok(webElement instanceof WebElement);
+    assert.strictEqual(weblogin.locateStrategy, 'use_xpath');
+    assert.strictEqual(browser.options.use_xpath, true);
+  });
+  
+});
diff --git a/test/extra/cucumber-config.js b/test/extra/cucumber-config.js
index 8d637537..df952e24 100644
--- a/test/extra/cucumber-config.js
+++ b/test/extra/cucumber-config.js
@@ -26,6 +26,17 @@ module.exports = {
       feature_path: path.join(__dirname, '../cucumber-integration-tests/sample_cucumber_tests/integration/sample.feature')
     }
   },
-  output: false,
-  silent: false
+  test_settings: {
+    browserstack: {
+      selenium: {
+        host: 'hub-cloud.browserstack.com',
+        port: 443
+      },
+      desiredCapabilities: {
+        'browserstack.user': 'test-access-user',
+        'browserstack.key': 'test-access-key',
+        browserName: 'chrome'
+      }
+    }
+  }
 };
\ No newline at end of file
diff --git a/test/sampletests/asyncwithexpectfailures/sample.js b/test/sampletests/asyncwithexpectfailures/sample.js
new file mode 100644
index 00000000..aa929c30
--- /dev/null
+++ b/test/sampletests/asyncwithexpectfailures/sample.js
@@ -0,0 +1,11 @@
+describe('browse.expect with failures in async step', function() {
+  it('async step failure', async function() {
+    browser.url('http://localhost');
+    browser.expect.element('#weblogin').to.be.contain.text('some text');
+  });
+
+  it('second step without failure', function() {
+    browser.url('http://localhost');
+    browser.expect.element('#weblogin').to.be.present;
+  });
+});
\ No newline at end of file
diff --git a/test/src/api/commands/testFrame.js b/test/src/api/commands/testFrame.js
new file mode 100644
index 00000000..2f9a7a2f
--- /dev/null
+++ b/test/src/api/commands/testFrame.js
@@ -0,0 +1,104 @@
+const assert = require('assert');
+const MockServer  = require('../../../lib/mockserver.js');
+const CommandGlobals = require('../../../lib/globals/commands.js');
+
+describe('updateValue', function() {
+
+  before(function(done) {
+    CommandGlobals.beforeEach.call(this, done);
+  });
+
+  after(function(done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('client.frame()', function(done) {
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/element',
+      method: 'POST',
+      postdata: {
+        using: 'css selector',
+        value: '#frameid, *[name="frameid"]'
+      },
+      response: {
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': '5cc459b8-36a8-3042-8b4a-258883ea642b'
+        }
+      }
+    });
+
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/element',
+      method: 'POST',
+      postdata: {
+        using: 'css selector',
+        value: '#framename, *[name="framename"]'
+      },
+      response: {
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': '5cc459b8-36a8-3042-8b4a-258883ea642b'
+        }
+      }
+    });
+
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/frame',
+      method: 'POST',
+      response: {
+        value: null,
+        status: 0
+      }
+    });
+
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/element',
+      method: 'POST',
+      postdata: {
+        using: 'css selector',
+        value: '#no-frame, *[name="no-frame"]'
+      },
+      statusCode: 404,
+      response: {
+        value: {
+          error: 'no such element',
+          message: 'Unable to locate element: .no-frame',
+          stacktrace: ''
+        }
+      }
+    });
+
+    const client = this.client;
+
+    client.api.element('id', 'frameid', function callback(res){
+      const frameId = res.value;
+
+      client.api
+        .frame('frameid', function callback(result) {
+          assert.strictEqual(result.status, 0);
+          assert.strictEqual(result.value, null);
+        })
+        .frame('framename', function callback(result) {
+          assert.strictEqual(result.status, 0);
+          assert.strictEqual(result.value, null);
+        })
+        .frame(undefined, function callback(result) {
+          assert.strictEqual(result.status, 0);
+          assert.strictEqual(result.value, null);
+        })
+        .frame(frameId, function callback(result) {
+          assert.strictEqual(result.status, 0);
+          assert.strictEqual(result.value, null);
+        })
+        .frame(null, function callback(result) {
+          assert.strictEqual(result.status, 0);
+          assert.strictEqual(result.value, null);
+        })
+        .frame('no-frame', function callback(result) {
+          assert.strictEqual(result.status, -1);
+          assert.strictEqual(result.error.name, 'NoSuchElementError');
+        });
+    });
+
+    this.client.start(done);
+  });
+});
diff --git a/test/src/api/commands/testSendKeys.js b/test/src/api/commands/testSendKeys.js
index 8cbbb1e0..079e7980 100644
--- a/test/src/api/commands/testSendKeys.js
+++ b/test/src/api/commands/testSendKeys.js
@@ -3,10 +3,14 @@ const MockServer  = require('../../../lib/mockserver.js');
 const CommandGlobals = require('../../../lib/globals/commands.js');
 
 describe('sendKeys', function() {
-
+ 
   before(function(done) {
     CommandGlobals.beforeEach.call(this, done, {
-      output: false
+      output: false,
+      globals: {
+        waitForConditionPollInterval: 10,
+        waitForConditionTimeout: 11
+      }
     });
   });
 
@@ -43,6 +47,22 @@ describe('sendKeys', function() {
       })
       .sendKeys('#weblogin', 'password', function callback(result) {
         assert.strictEqual(result.status, 0);
+      })
+      .sendKeys('#weblogin', undefined, function callback(result) {
+        assert.strictEqual(result.status, -1);
+        assert.ok(result.value.message.includes('each key must be a number'))
+      })
+      .sendKeys('#weblogin', ['password', undefined], function callback(result) {
+        assert.strictEqual(result.status, -1);
+        assert.ok(result.value.message.includes('each key must be a number'))
+      })
+      .sendKeys('#weblogin', null, function callback(result) {
+        assert.strictEqual(result.status, -1);
+        assert.ok(result.value.message.includes('each key must be a number'))
+      })
+      .sendKeys('#weblogin', ['password', null], function callback(result) {
+        assert.strictEqual(result.status, -1);
+        assert.ok(result.value.message.includes('each key must be a number'))
       });
 
     this.client.start(done);
diff --git a/test/src/apidemos/elements/testElementGlobal.js b/test/src/apidemos/elements/testElementGlobal.js
index 12762fd0..b2034096 100644
--- a/test/src/apidemos/elements/testElementGlobal.js
+++ b/test/src/apidemos/elements/testElementGlobal.js
@@ -46,5 +46,13 @@ describe('element global demos', function() {
     }));
   });
 
+  it('set use_xpath locate-strategy in element global', function() {
+    const testsPath = path.join(__dirname, '../../../apidemos/elements/testGlobalLocateStrategy.js');
+
+    return NightwatchClient.runTests(testsPath, settings({
+      output: false,
+      use_xpath: true
+    }));
+  });
   
 });
diff --git a/test/src/cli/testCliRunnerGenerate.js b/test/src/cli/testCliRunnerGenerate.js
index e3c0d110..61eab2e9 100644
--- a/test/src/cli/testCliRunnerGenerate.js
+++ b/test/src/cli/testCliRunnerGenerate.js
@@ -107,13 +107,13 @@ describe('Test CLI Runner Generate', function() {
         assert.deepStrictEqual(configData.test_settings.browserstack, {
           desiredCapabilities: {
             'bstack:options': {
-              userName: '${BROWSERSTACK_USER}',
-              accessKey: '${BROWSERSTACK_KEY}'
+              userName: '${BROWSERSTACK_USERNAME}',
+              accessKey: '${BROWSERSTACK_ACCESS_KEY}'
             }
           },
 
           selenium: {
-            host: 'hub-cloud.browserstack.com',
+            host: 'hub.browserstack.com',
             port: 443
           },
 
@@ -167,6 +167,57 @@ describe('Test CLI Runner Generate', function() {
           }
         });
 
+        assert.deepStrictEqual(configData.test_settings.saucelabs, {
+          desiredCapabilities: {
+            'sauce:options': {
+              username: '${SAUCE_USERNAME}',
+              accessKey: '${SAUCE_ACCESS_KEY}'
+              // https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/#--region
+              // region: 'us-west-1'
+              // https://docs.saucelabs.com/dev/test-configuration-options/#tunnelidentifier
+              // parentTunnel: ''
+              // tunnelIdentifier: ''
+            }
+          },
+
+          selenium: {
+            host: 'ondemand.saucelabs.com',
+            port: 443
+          },
+
+          disable_error_log: false,
+          webdriver: {
+            start_process: false
+          }
+        });
+
+        assert.deepStrictEqual(configData.test_settings['saucelabs.chrome'], {
+          extends: 'saucelabs',
+          desiredCapabilities: {
+            browserName: 'chrome',
+            screenResolution: '1280x1024',
+            browserVersion: 'latest',
+            javascriptEnabled: true,
+            acceptSslCerts: true,
+            timeZone: 'London',
+            chromeOptions: {
+              w3c: true
+            }
+          }
+        });
+
+        assert.deepStrictEqual(configData.test_settings['saucelabs.firefox'], {
+          extends: 'saucelabs',
+          desiredCapabilities: {
+            browserName: 'firefox',
+            screenResolution: '1280x1024',
+            browserVersion: 'latest',
+            javascriptEnabled: true,
+            acceptSslCerts: true,
+            timeZone: 'London'
+          }
+        });
+
         assert.deepStrictEqual(configData.test_settings['cucumber-js'], {
           src_folders: ['examples/cucumber-js/features/step_definitions'],
           test_runner: {
@@ -213,16 +264,16 @@ describe('Test CLI Runner Generate', function() {
       browserName: 'internet explorer',
       browserVersion: '11.0',
       'bstack:options': {
-        userName: '${BROWSERSTACK_USER}',
-        accessKey: '${BROWSERSTACK_KEY}'
+        userName: '${BROWSERSTACK_USERNAME}',
+        accessKey: '${BROWSERSTACK_ACCESS_KEY}'
       }
     });
     assert.deepStrictEqual(chromeLocalRunner.test_settings.desiredCapabilities, {
       browserName: 'chrome',
       'browserstack.local': true,
       'bstack:options': {
-        userName: '${BROWSERSTACK_USER}',
-        accessKey: '${BROWSERSTACK_KEY}'
+        userName: '${BROWSERSTACK_USERNAME}',
+        accessKey: '${BROWSERSTACK_ACCESS_KEY}'
       }
     });
   });
diff --git a/test/src/index/testSettings.js b/test/src/index/testSettings.js
index 917dcd89..84f13419 100644
--- a/test/src/index/testSettings.js
+++ b/test/src/index/testSettings.js
@@ -7,7 +7,7 @@ const eq = assert.strictEqual;
 
 describe('test Settings', function () {
   it('testSetOptions', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       use_xpath: true,
       launch_url: '/home'
     });
@@ -21,7 +21,7 @@ describe('test Settings', function () {
 
   it('test set test_runner invalid value', function () {
     assert.throws(function() {
-      let client = Nightwatch.createClient({
+      const client = Nightwatch.createClient({
         test_runner: null
       });
     }, /Error: Invalid "test_runner" settings specified; received: null/);
@@ -29,7 +29,7 @@ describe('test Settings', function () {
 
   it('test set test_runner invalid value - function', function () {
     assert.throws(function() {
-      let client = Nightwatch.createClient({
+      const client = Nightwatch.createClient({
         test_runner: function() {}
       });
     }, /Error: Invalid "test_runner" settings specified; received: function\(\) \{\}/);
@@ -37,7 +37,7 @@ describe('test Settings', function () {
 
   it('test set test_runner string value', function () {
 
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       test_runner: 'mocha'
     });
 
@@ -49,7 +49,7 @@ describe('test Settings', function () {
 
   it('testSetWebdriverOptionsDefaults', function () {
     HttpRequest.globalSettings = {};
-    let client = Nightwatch.createClientDefaults();
+    const client = Nightwatch.createClientDefaults();
 
     eq(client.options.webdriver.host, 'localhost');
     eq(client.options.webdriver.port, 4444);
@@ -63,7 +63,7 @@ describe('test Settings', function () {
   });
 
   it('testSetWebdriverOptions', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       webdriver: {
         host: '127.0.0.1',
         ssl: false,
@@ -78,8 +78,8 @@ describe('test Settings', function () {
       }
     });
 
-    let HttpRequest = common.require('http/request.js');
-    let request = new HttpRequest({});
+    const HttpRequest = common.require('http/request.js');
+    const request = new HttpRequest({});
 
     eq(request.reqOptions.host, '127.0.0.1');
     eq(request.reqOptions.port, 10195);
@@ -124,7 +124,7 @@ describe('test Settings', function () {
     const Nightwatch = common.require('index.js');
     const Settings = common.require('settings/settings.js');
 
-    let settings = Settings.parse({
+    const settings = Settings.parse({
       selenium: {
         start_process: false
       },
@@ -134,8 +134,8 @@ describe('test Settings', function () {
 
     Nightwatch.client(settings, null);
 
-    let HttpRequest = common.require('http/request.js');
-    let request = new HttpRequest({});
+    const HttpRequest = common.require('http/request.js');
+    const request = new HttpRequest({});
 
     eq(request.reqOptions.port, 80);
   });
@@ -143,7 +143,7 @@ describe('test Settings', function () {
   it('test create Transport for remote with Firefox', function() {
     const Settings = common.require('settings/settings.js');
 
-    let settings = Settings.parse({
+    const settings = Settings.parse({
       selenium: {
         start_process: false
       }
@@ -162,7 +162,7 @@ describe('test Settings', function () {
   });
 
   it('testSetOptionsCredentials', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       username: 'test-user',
       accessKey: 'test-access-key'
     });
@@ -174,7 +174,7 @@ describe('test Settings', function () {
   });
 
   it('testEnableFailFast', function () {
-    let client = Nightwatch.createClient({}, null, {
+    const client = Nightwatch.createClient({}, null, {
       'fail-fast': true
     });
 
@@ -182,7 +182,7 @@ describe('test Settings', function () {
   });
 
   it('testSetOptionsScreenshots – path empty', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       screenshots: {
         enabled: true,
         path: ''
@@ -197,7 +197,7 @@ describe('test Settings', function () {
   });
 
   it('testSetOptionsScreenshots – path not empty', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       screenshots: {
         enabled: true,
         path: '/tmp/'
@@ -207,7 +207,7 @@ describe('test Settings', function () {
 
     eq(client.api.options.log_screenshot_data, true);
     eq(client.options.screenshots.on_error, true);
-
+    
     if (process.platform === 'win32') {
       assert.ok(client.settings.screenshots.path.endsWith('\\tmp'));
     } else {
@@ -217,7 +217,7 @@ describe('test Settings', function () {
 
 
   it('testSetOptionsScreenshotsOnError', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       screenshots: {
         enabled: true,
         on_error: true,
@@ -230,7 +230,7 @@ describe('test Settings', function () {
   });
 
   it('testSetOptionsScreenshotsThrows', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       screenshots: true
     });
 
@@ -242,7 +242,7 @@ describe('test Settings', function () {
   });
 
   it('testEndSessionOnFail', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       end_session_on_fail: true
     });
 
@@ -253,7 +253,7 @@ describe('test Settings', function () {
   });
 
   it('testSetRequestTimeoutOptions', function () {
-    let client = Nightwatch.createClient({
+    const client = Nightwatch.createClient({
       webdriver: {
         timeout_options: {
           timeout: 10000,
@@ -268,15 +268,15 @@ describe('test Settings', function () {
     });
 
 
-    let HttpRequest = common.require('http/request.js');
-    let request = new HttpRequest({});
+    const HttpRequest = common.require('http/request.js');
+    const request = new HttpRequest({});
 
     assert.strictEqual(request.httpOpts.timeout, 10000);
     assert.strictEqual(request.retryAttempts, 5);
   });
 
   it('Test initialize with parallel cli argument', function () {
-    let settings = Settings.parse({
+    const settings = Settings.parse({
       selenium_port: 10195,
       silent: false,
       output: false
diff --git a/test/src/index/transport/testBrowserstackTransport.js b/test/src/index/transport/testBrowserstackTransport.js
index bbb48adf..3837dfe9 100644
--- a/test/src/index/transport/testBrowserstackTransport.js
+++ b/test/src/index/transport/testBrowserstackTransport.js
@@ -41,6 +41,30 @@ describe('BrowserstackTransport', function () {
 
   });
 
+  it('test create Transport for Browserstack - env variable credentials', function() {
+    process.env['USER'] = 'test-access-user';
+    process.env['KEY'] = 'test-access-key';
+
+    const client = NightwatchClient.client({
+      webdriver: {
+        host: 'hub-cloud.browserstack.com',
+        port: 443
+      },
+      desiredCapabilities: {
+        'browserstack.user': '${USER}',
+        'browserstack.key': '${KEY}'
+      }
+    });
+
+    const {transport} = client;
+    assert.ok(transport instanceof SeleniumRemote);
+    assert.strictEqual(transport.username, 'test-access-user');
+    assert.strictEqual(transport.accessKey, 'test-access-key');
+
+    delete process.env['USER'];
+    delete process.env['KEY'];
+  });
+
   it('test create Transport for Browserstack', function(done) {
     const client = NightwatchClient.client({
       webdriver: {
diff --git a/test/src/runner/cucumber-integration/testCucumberWithBrowserstack.js b/test/src/runner/cucumber-integration/testCucumberWithBrowserstack.js
new file mode 100644
index 00000000..e99ac769
--- /dev/null
+++ b/test/src/runner/cucumber-integration/testCucumberWithBrowserstack.js
@@ -0,0 +1,100 @@
+const path = require('path');
+const assert = require('assert');
+const nock = require('nock');
+const common = require('../../../common.js');
+const {runTests} = common.require('index.js');
+
+xdescribe('Cucumber  Browserstack integration', function() {
+  
+  before(function(done) {
+    try {
+      nock.activate();
+    // eslint-disable-next-line no-empty
+    } catch (err) {
+    }
+   
+    nock('https://hub-cloud.browserstack.com')
+      .post('/wd/hub/session')
+      .reply(201, {
+        status: 0,
+        sessionId: '1352110219202',
+        value: {
+          browserName: 'chrome'
+        }
+      });
+
+    nock('https://api.browserstack.com')
+      .get('/automate/builds.json')
+      .reply(200, []);
+
+    nock('https://hub-cloud.browserstack.com')
+      .post('/wd/hub/session/1352110219202/url')
+      .reply(200, {
+        value: null
+      });
+
+    nock('https://hub-cloud.browserstack.com')
+      .post('/wd/hub/session/1352110219202/elements')
+      .reply(200, {
+        status: 0,
+        sessionId: '1352110219202',
+        value: [{
+          'ELEMENT': '0'
+        }]
+      });
+
+    nock('https://hub-cloud.browserstack.com')
+      .delete('/wd/hub/session/1352110219202')
+      .reply(200, {
+        value: []
+      });
+
+    done();
+  });
+
+  after(function(done) {
+    nock.restore();
+    done();
+  });
+
+
+  it('testCucumberSampleTests - [Passed]', function() {
+   
+    nock('https://api.browserstack.com')
+      .put('/automate/sessions/1352110219202.json', body => body.status === 'passed')
+      .reply(200);
+
+    const source = [path.join(__dirname, '../../../cucumber-integration-tests/sample_cucumber_tests/integration/testSample.js')];
+    
+    return runTests({
+      source,
+      tags: ['@pass'],
+      verbose: false,
+      config: path.join(__dirname, '../../../extra/cucumber-config.js'),
+      env: 'browserstack'
+    }, {}).then(failures => {
+      assert.strictEqual(failures, false, 'Cucumber has test failures. Run with verbose to investigate.');
+    });
+  });
+
+  it('testCucumberSampleTests with failures', function() {
+
+
+    nock('https://api.browserstack.com')
+      .put('/automate/sessions/1352110219202.json', body => body.status === 'failed')
+      .reply(200);
+    
+    const source = [path.join(__dirname, '../../../cucumber-integration-tests/sample_cucumber_tests/integration/testWithFailures.js')];
+    
+    return runTests({
+      source,
+      env: 'browserstack',
+      tags: ['@fail'],
+      verbose: false,
+      config: path.join(__dirname, '../../../extra/cucumber-config.js')
+    }, {}).then(failures => {
+      assert.strictEqual(failures, true, 'Cucumber tests should have failed. Run with verbose to investigate.');
+    });
+  });
+});
+
diff --git a/test/src/runner/testRunTestcase.js b/test/src/runner/testRunTestcase.js
index bcec9f55..5e91f47d 100644
--- a/test/src/runner/testRunTestcase.js
+++ b/test/src/runner/testRunTestcase.js
@@ -170,7 +170,7 @@ describe('testRunTestcase', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       },
       retryAssertionTimeout: 0
@@ -193,7 +193,7 @@ describe('testRunTestcase', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       },
       retryAssertionTimeout: 0
@@ -216,7 +216,7 @@ describe('testRunTestcase', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       },
       retryAssertionTimeout: 0
diff --git a/test/src/runner/testRunTestsuiteWithSuiteRetries.js b/test/src/runner/testRunTestsuiteWithSuiteRetries.js
index e11d072e..b61faf26 100644
--- a/test/src/runner/testRunTestsuiteWithSuiteRetries.js
+++ b/test/src/runner/testRunTestsuiteWithSuiteRetries.js
@@ -43,7 +43,7 @@ describe('testRunTestsuiteWithSuiteRetries', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       }
     };
@@ -78,7 +78,7 @@ describe('testRunTestsuiteWithSuiteRetries', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       }
     };
@@ -103,7 +103,7 @@ describe('testRunTestsuiteWithSuiteRetries', function() {
         assert.strictEqual(results.passed, 1);
         assert.strictEqual(results.failed, 1);
         assert.strictEqual(results.errors, 0);
-        assert.strictEqual(results.skipped, 0);
+        assert.strictEqual(results.skipped, 1);
         cb();
       }
     };
diff --git a/test/src/runner/testRunWithGlobalReporter.js b/test/src/runner/testRunWithGlobalReporter.js
index ddde3938..8a7b8f38 100644
--- a/test/src/runner/testRunWithGlobalReporter.js
+++ b/test/src/runner/testRunWithGlobalReporter.js
@@ -80,4 +80,23 @@ describe('testRunWithGlobalReporter', function() {
       assert.strictEqual(err.message, 'Timeout while waiting (20s) for the custom global reporter callback to be called.');
     });
   });
+
+  it('to check skipped count in global reporter', function() {
+    let testsPath = path.join(__dirname, '../../sampletests/globalreporterskippedcount/sample.js');
+    let reporterCount = 0;
+
+    return runTests(testsPath, settings({
+      globals: {
+        reporter(results, cb) {
+          assert.strictEqual(results.skipped, 2);
+          assert.strictEqual(results.modules.sample.skippedCount, 2);
+          reporterCount++;
+          cb();
+        }
+      },
+      output_folder: false
+    })).then(_ => {
+      assert.strictEqual(reporterCount, 1);
+    }).catch(err => (err))
+  });
 });
diff --git a/test/src/runner/testRunnerEs6Async.js b/test/src/runner/testRunnerEs6Async.js
index ce8dd90a..b4940f54 100644
--- a/test/src/runner/testRunnerEs6Async.js
+++ b/test/src/runner/testRunnerEs6Async.js
@@ -272,4 +272,32 @@ describe('testRunner ES6 Async', function () {
       globals
     }));
   });
+
+  it('test runner with async expect failure', function() {
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219202/element/0/text',
+      method: 'GET',
+      response: JSON.stringify({
+        sessionId: '1352110219202',
+        status: 0,
+        value: 'Barn owl'
+      })
+    }, true);
+
+    const testsPath = path.join(__dirname, '../../sampletests/asyncwithexpectfailures');
+    const globals = {
+      waitForConditionPollInterval: 50,
+      waitForConditionTimeout: 100,
+      retryAssertionTimeout: 150,
+      reporter(results) {
+        assert.strictEqual(results.assertions, 2);
+        assert.strictEqual(results.failed, 1);
+        assert.strictEqual(results.passed, 1);
+      }
+    };
+
+    return runTests(testsPath, settings({
+      globals
+    }));
+  });
 });
diff --git a/test/src/utils/testUtils.js b/test/src/utils/testUtils.js
index d2728323..e0606e8a 100644
--- a/test/src/utils/testUtils.js
+++ b/test/src/utils/testUtils.js
@@ -149,4 +149,13 @@ describe('test Utils', function() {
     assert.deepStrictEqual(absPath, [path.join(__dirname, '../../extra/commands/typescript/tsWait.js')]);
   });
 
+  it('SafeJSON.stringify for circurlar reference objects', function() {
+    const obj = {
+      value: 1
+    };
+    obj.cirRef = obj;
+
+    assert.strictEqual(Utils.SafeJSON.stringify(obj), '{"value":1,"cirRef":"[Circular]"}');
+  });
+
 });