Repository: nightwatchjs/nightwatch
Pull request #3705 (2023-04-21 08:59:44)

chore: Upgrade selenium to 4.9.0
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`);
- [ ] 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;
- [x] 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.



Patch:
diff --git a/lib/api/_loaders/element-api.js b/lib/api/_loaders/element-api.js
index 0ed9e1da..6b431444 100644
--- a/lib/api/_loaders/element-api.js
+++ b/lib/api/_loaders/element-api.js
@@ -20,6 +20,12 @@ class ScopedElementAPILoader {
     ];
   }
 
+  static isScopedElementCommand(commandName) {
+    return ScopedElementAPILoader.availableElementCommands.some(
+      commands => commands.includes(commandName)
+    );
+  }
+
   constructor(nightwatchInstance) {
     this.nightwatchInstance = nightwatchInstance;
   }
diff --git a/lib/api/assertions/hasAttribute.js b/lib/api/assertions/hasAttribute.js
index 802c9e34..54b3ac13 100644
--- a/lib/api/assertions/hasAttribute.js
+++ b/lib/api/assertions/hasAttribute.js
@@ -19,7 +19,7 @@
  * @api assertions
  */
 
-const {setElementSelectorProps, containsMultiple} = require('../../utils');
+const {setElementSelectorProps, isString} = require('../../utils');
 
 exports.assertion = function(definition, expectedAttribute, msg) {
   this.options = {
@@ -32,35 +32,31 @@ exports.assertion = function(definition, expectedAttribute, msg) {
  
 
   this.formatMessage = function() {
+    if (!isString(expectedAttribute)) {
+      throw new Error('Expected attribute must be a string');
+    }
+
     let message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have attribute %s' : 'has attribute %s'}`;
  
     return {
       message,
-      args: [this.elementSelector, `'${Array.isArray(expectedAttribute) ? expectedAttribute.join(' ') : expectedAttribute}'`]
+      args: [this.elementSelector, `'${expectedAttribute}'`]
     };
   };
 
   this.evaluate = function() {
-    if (!this.attributeList) {
-      return false;
-    }
- 
-    return containsMultiple(this.attributeList, expectedAttribute, ' ');
-  };
+    const {result} = this;
 
-  this.value = function(result) {
     if (!result || !result.value) {
-      return '';
+      return false;
     }
-
-    this.attributeList = result.value.map(attribute =>  attribute.name);
-
-    return this.attributeList;
+ 
+    return true;
   };
 
   this.command = function(callback) {
     this.api.getAttribute(setElementSelectorProps(definition, {
       suppressNotFoundErrors: true
-    }), 'attributes', callback);
+    }), expectedAttribute, callback);
   };
 };
diff --git a/lib/api/protocol/alerts/accept.js b/lib/api/client-commands/alerts/accept.js
similarity index 80%
rename from lib/api/protocol/alerts/accept.js
rename to lib/api/client-commands/alerts/accept.js
index b9b55db5..e7f91ab1 100644
--- a/lib/api/protocol/alerts/accept.js
+++ b/lib/api/client-commands/alerts/accept.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Accepts the currently displayed alert dialog. Usually, this is equivalent to clicking on the 'OK' button in the dialog.
@@ -27,12 +27,14 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#accept-alert
  * @api protocol.userprompts
  */
-module.exports = class Session extends ProtocolAction {
+class AcceptAlert extends ClientCommand {
   static get isTraceable() {
     return true;
   }
 
-  command(callback) {
-    return this.transportActions.acceptAlert(callback);
+  performAction(callback) {
+    this.transportActions.acceptAlert(callback);
   }
-};
+}
+
+module.exports = AcceptAlert;
diff --git a/lib/api/protocol/alerts/dismiss.js b/lib/api/client-commands/alerts/dismiss.js
similarity index 81%
rename from lib/api/protocol/alerts/dismiss.js
rename to lib/api/client-commands/alerts/dismiss.js
index 49f52709..f7eac52d 100644
--- a/lib/api/protocol/alerts/dismiss.js
+++ b/lib/api/client-commands/alerts/dismiss.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Dismisses the currently displayed alert dialog.
@@ -29,12 +29,14 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#dismiss-alert
  * @api protocol.userprompts
  */
-module.exports = class Session extends ProtocolAction {
+class DismissAlert extends ClientCommand {
   static get isTraceable() {
     return true;
   }
 
-  command(callback) {
-    return this.transportActions.dismissAlert(callback);
+  performAction(callback) {
+    this.transportActions.dismissAlert(callback);
   }
-};
+}
+
+module.exports = DismissAlert;
diff --git a/lib/api/protocol/alerts/getText.js b/lib/api/client-commands/alerts/getText.js
similarity index 80%
rename from lib/api/protocol/alerts/getText.js
rename to lib/api/client-commands/alerts/getText.js
index fce11050..a9c0e1e8 100644
--- a/lib/api/protocol/alerts/getText.js
+++ b/lib/api/client-commands/alerts/getText.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Get the text of the currently displayed JavaScript alert(), confirm(), or prompt() dialog.
@@ -28,8 +28,10 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#get-alert-text
  * @api protocol.userprompts
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getAlertText(callback);
+class GetAlertText extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getAlertText(callback);
   }
-};
+}
+
+module.exports = GetAlertText;
diff --git a/lib/api/protocol/alerts/setText.js b/lib/api/client-commands/alerts/setText.js
similarity index 66%
rename from lib/api/protocol/alerts/setText.js
rename to lib/api/client-commands/alerts/setText.js
index 1e460500..5b2e06b7 100644
--- a/lib/api/protocol/alerts/setText.js
+++ b/lib/api/client-commands/alerts/setText.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Send keystrokes to a JavaScript prompt() dialog.
@@ -27,12 +27,26 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#send-alert-text
  * @api protocol.userprompts
  */
-module.exports = class Session extends ProtocolAction {
+class SetAlertText extends ClientCommand {
   static get isTraceable() {
     return true;
   }
 
+  performAction(callback) {
+    const {alertText} = this;
+
+    this.transportActions.setAlertText(alertText, callback);
+  }
+
   command(value, callback) {
-    return this.transportActions.setAlertText(value, callback);
+    if (typeof value !== 'string') {
+      throw new Error('First argument passed to .alerts.setText() must be a string.');
+    }
+
+    this.alertText = value;
+  
+    return super.command(callback);
   }
-};
+}
+
+module.exports = SetAlertText;
diff --git a/lib/api/client-commands/captureBrowserConsoleLogs.js b/lib/api/client-commands/captureBrowserConsoleLogs.js
index bdb07148..df9344b9 100644
--- a/lib/api/client-commands/captureBrowserConsoleLogs.js
+++ b/lib/api/client-commands/captureBrowserConsoleLogs.js
@@ -44,12 +44,7 @@ class StartCapturingLogs extends ClientCommand {
       return callback(error);
     }
     
-    this.transportActions
-      .startLogsCapture(userCallback, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.startLogsCapture(userCallback, callback);
   }
 
   command(userCallback, callback) {
diff --git a/lib/api/client-commands/captureBrowserExceptions.js b/lib/api/client-commands/captureBrowserExceptions.js
index 54e54233..f7f6a2c5 100644
--- a/lib/api/client-commands/captureBrowserExceptions.js
+++ b/lib/api/client-commands/captureBrowserExceptions.js
@@ -48,12 +48,7 @@ class CatchJsExceptions extends ClientCommand {
       return callback(error);
     }
 
-    this.transportActions
-      .catchJsExceptions(userCallback, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.catchJsExceptions(userCallback, callback);
   }
 
   command(userCallback, callback) {
diff --git a/lib/api/client-commands/captureNetworkRequests.js b/lib/api/client-commands/captureNetworkRequests.js
index a6be7044..d71794cd 100644
--- a/lib/api/client-commands/captureNetworkRequests.js
+++ b/lib/api/client-commands/captureNetworkRequests.js
@@ -45,12 +45,7 @@ class CaptureNetworkCalls extends ClientCommand {
       return callback(error);
     }
     
-    this.transportActions
-      .interceptNetworkCalls(userCallback, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.interceptNetworkCalls(userCallback, callback);
   }
 
   command(userCallback, callback) {
diff --git a/lib/api/client-commands/cookies/delete.js b/lib/api/client-commands/cookies/delete.js
index 43c02fe1..3cd5a8e6 100644
--- a/lib/api/client-commands/cookies/delete.js
+++ b/lib/api/client-commands/cookies/delete.js
@@ -1,5 +1,4 @@
 const ClientCommand = require('../_base-command.js');
-const {Logger} = require('../../../utils');
 
 /**
  * Delete the cookie with the given name. This command is a no-op if there is no such cookie visible to the current page.
@@ -36,12 +35,7 @@ class DeleteCookie extends ClientCommand {
   performAction(callback) {
     const {cookieName} = this;
 
-    this.transportActions
-      .deleteCookie(cookieName, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.deleteCookie(cookieName, callback);
   }
 
   command(cookieName, callback) {
diff --git a/lib/api/client-commands/cookies/deleteAll.js b/lib/api/client-commands/cookies/deleteAll.js
index 0a953b97..f4dfcf5e 100644
--- a/lib/api/client-commands/cookies/deleteAll.js
+++ b/lib/api/client-commands/cookies/deleteAll.js
@@ -1,5 +1,4 @@
 const ClientCommand = require('../_base-command.js');
-const {Logger} = require('../../../utils');
 
 /**
  * Delete all cookies visible to the current page.
@@ -33,12 +32,7 @@ class DeleteCookies extends ClientCommand {
   }
 
   performAction(callback) {
-    this.transportActions
-      .deleteAllCookies(callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.deleteAllCookies(callback);
   }
 }
 
diff --git a/lib/api/client-commands/cookies/get.js b/lib/api/client-commands/cookies/get.js
index 81a77ac1..e89db66c 100644
--- a/lib/api/client-commands/cookies/get.js
+++ b/lib/api/client-commands/cookies/get.js
@@ -1,5 +1,4 @@
 const ClientCommand = require('../_base-command.js');
-const {Logger} = require('../../../utils');
 
 /**
  * Retrieve a single cookie visible to the current page.
@@ -39,12 +38,7 @@ class GetCookie extends ClientCommand {
   performAction(callback) {
     const {cookieName} = this;
 
-    this.transportActions
-      .getCookie(cookieName, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.getCookie(cookieName, callback);
   }
 
   command(name, callback) {
diff --git a/lib/api/client-commands/cookies/getAll.js b/lib/api/client-commands/cookies/getAll.js
index e8eb0ab8..d975e56e 100644
--- a/lib/api/client-commands/cookies/getAll.js
+++ b/lib/api/client-commands/cookies/getAll.js
@@ -1,5 +1,4 @@
 const ClientCommand = require('../_base-command.js');
-const {Logger} = require('../../../utils');
 
 /**
  * Retrieve all cookies visible to the current page.
@@ -36,12 +35,7 @@ const {Logger} = require('../../../utils');
 
 class GetCookies extends ClientCommand {
   performAction(callback) {
-    this.transportActions
-      .getCookies(callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.getCookies(callback);
   }
 }
 
diff --git a/lib/api/client-commands/cookies/set.js b/lib/api/client-commands/cookies/set.js
index 3cd8ab1d..37d9d315 100644
--- a/lib/api/client-commands/cookies/set.js
+++ b/lib/api/client-commands/cookies/set.js
@@ -1,6 +1,5 @@
 const ClientCommand = require('../_base-command.js');
 const Utils = require('../../../utils/index.js');
-const {Logger} = require('../../../utils');
 
 /**
  * Set a cookie, specified as a cookie JSON object, with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion).
@@ -48,12 +47,7 @@ class SetCookie extends ClientCommand {
   performAction(callback) {
     const {cookie} = this;
 
-    this.transportActions
-      .addCookie(cookie, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.addCookie(cookie, callback);
   }
 
   command(cookie, callback) {
diff --git a/lib/api/protocol/document/injectScript.js b/lib/api/client-commands/document/injectScript.js
similarity index 74%
rename from lib/api/protocol/document/injectScript.js
rename to lib/api/client-commands/document/injectScript.js
index 91e2d229..9954cf28 100644
--- a/lib/api/protocol/document/injectScript.js
+++ b/lib/api/client-commands/document/injectScript.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Utility command to load an external script into the page specified by url.
@@ -25,11 +25,17 @@ const ProtocolAction = require('../_base-action.js');
  * @returns {HTMLScriptElement} The newly created script tag.
  * @api protocol.document
  */
-module.exports = class InjectScript extends ProtocolAction {
+class InjectScript extends ClientCommand {
   static get isTraceable() {
     return true;
   }
 
+  performAction(callback) {
+    const {scriptFn, scriptArgs} = this;
+
+    this.transportActions.executeScript(scriptFn, scriptArgs, callback);
+  }
+
   command(scriptUrl, id, callback) {
     const args = [scriptUrl];
 
@@ -42,6 +48,12 @@ module.exports = class InjectScript extends ProtocolAction {
     // eslint-disable-next-line
     const script = function(u,i) {return (function(d){var e=d.createElement('script');var m=d.getElementsByTagName('head')[0];e.src=u;if(i){e.id=i;}m.appendChild(e);return e;})(document);};
 
-    return this.executeScriptHandler('executeScript', script, args, callback);
+    this.scriptFn = 'var passedArgs = Array.prototype.slice.call(arguments,0); return (' +
+      script.toString() + ').apply(window, passedArgs);';
+    this.scriptArgs = args; 
+
+    return super.command(callback);
   }
 };
+
+module.exports = InjectScript;
diff --git a/lib/api/protocol/document/source.js b/lib/api/client-commands/document/source.js
similarity index 80%
rename from lib/api/protocol/document/source.js
rename to lib/api/client-commands/document/source.js
index 845b9934..9aea0b14 100644
--- a/lib/api/protocol/document/source.js
+++ b/lib/api/client-commands/document/source.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Get the string serialized source of the current page.
@@ -24,12 +24,14 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#get-page-source
  * @api protocol.document
  */
-module.exports = class Source extends ProtocolAction {
+class Source extends ClientCommand {
   static get AliasName() {
     return 'pageSource';
   }
 
-  command(callback) {
-    return this.transportActions.getPageSource(callback);
+  performAction(callback) {
+    this.transportActions.getPageSource(callback);
   }
-};
+}
+
+module.exports = Source;
diff --git a/lib/api/client-commands/enablePerformanceMetrics.js b/lib/api/client-commands/enablePerformanceMetrics.js
index 61169d47..14941885 100644
--- a/lib/api/client-commands/enablePerformanceMetrics.js
+++ b/lib/api/client-commands/enablePerformanceMetrics.js
@@ -41,12 +41,7 @@ class EnablePerformanceMetrics extends ClientCommand {
 
     const {enable = true} = this;
     
-    this.transportActions
-      .enablePerformanceMetrics(enable, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.enablePerformanceMetrics(enable, callback);
   }
 
   command(enable, callback) {
diff --git a/lib/api/client-commands/getPerformanceMetrics.js b/lib/api/client-commands/getPerformanceMetrics.js
index 70d8f750..be396c96 100644
--- a/lib/api/client-commands/getPerformanceMetrics.js
+++ b/lib/api/client-commands/getPerformanceMetrics.js
@@ -39,12 +39,7 @@ class GetPerformanceMetrics extends ClientCommand {
       return callback(error);
     }
     
-    this.transportActions
-      .getPerformanceMetrics(callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.getPerformanceMetrics(callback);
   }
 
   command(callback) {
diff --git a/lib/api/client-commands/mockNetworkResponse.js b/lib/api/client-commands/mockNetworkResponse.js
index 8c31a9be..e564b46a 100644
--- a/lib/api/client-commands/mockNetworkResponse.js
+++ b/lib/api/client-commands/mockNetworkResponse.js
@@ -50,12 +50,7 @@ class MockNetworkResponse extends ClientCommand {
       urlToIntercept = `${launchUrl}${slashDel}${urlToIntercept}`;
     }
     
-    this.transportActions
-      .mockNetworkResponse(urlToIntercept, response, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.mockNetworkResponse(urlToIntercept, response, callback);
   }
 
   command(urlToIntercept, response, callback) {
diff --git a/lib/api/client-commands/setDeviceDimensions.js b/lib/api/client-commands/setDeviceDimensions.js
index d4666880..e5f16c15 100644
--- a/lib/api/client-commands/setDeviceDimensions.js
+++ b/lib/api/client-commands/setDeviceDimensions.js
@@ -49,12 +49,7 @@ class SetDeviceDimensions extends ClientCommand {
     const {width = 0, height = 0, deviceScaleFactor = 0, mobile = false} = this.metrics;
     const metrics = {width, height, deviceScaleFactor, mobile};
 
-    this.transportActions
-      .setDeviceMetrics(metrics, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.setDeviceMetrics(metrics, callback);
   }
 
   command(metrics, callback) {
diff --git a/lib/api/client-commands/setGeolocation.js b/lib/api/client-commands/setGeolocation.js
index 4b7dfab9..a128ee09 100644
--- a/lib/api/client-commands/setGeolocation.js
+++ b/lib/api/client-commands/setGeolocation.js
@@ -51,24 +51,14 @@ class SetGeolocation extends ClientCommand {
       // Set accuracy as 100 if not provided.
       if (accuracy === undefined) {coordinates.accuracy = 100}
 
-      this.transportActions
-        .setGeolocation(coordinates, callback)
-        .catch(err => {
-          Logger.error(err);
-          callback(err);
-        });
+      this.transportActions.setGeolocation(coordinates, callback);
 
       return;
     }
 
     if (latitude === undefined && longitude === undefined) {
       // Clear geolocation override.
-      this.transportActions
-        .clearGeolocation(callback)
-        .catch(err => {
-          Logger.error(err);
-          callback(err);
-        });
+      this.transportActions.clearGeolocation(callback);
 
       return;
     }
diff --git a/lib/api/client-commands/setNetworkConditions.js b/lib/api/client-commands/setNetworkConditions.js
index 86095122..6a932c70 100644
--- a/lib/api/client-commands/setNetworkConditions.js
+++ b/lib/api/client-commands/setNetworkConditions.js
@@ -24,7 +24,6 @@ const {Logger} = require('../../utils');
  */
 class SetNetworkConditions extends ClientCommand {
   performAction(callback) {
-
     if (!this.api.isChrome() && !this.api.isEdge()) {
       const error = new Error('SetNetworkConditions is not supported while using this driver');
       Logger.error(error);
@@ -34,14 +33,7 @@ class SetNetworkConditions extends ClientCommand {
 
     const {spec} = this;
 
-    this.transportActions
-      .setNetworkConditions(spec, callback)
-      .then((result) => {
-        callback(result);
-      })
-      .catch((err) => {
-        return err;
-      });
+    this.transportActions.setNetworkConditions(spec, callback);
   }
 
   command(spec, callback) {
diff --git a/lib/api/client-commands/takeHeapSnapshot.js b/lib/api/client-commands/takeHeapSnapshot.js
index 0f4c2f4d..a5e6348d 100644
--- a/lib/api/client-commands/takeHeapSnapshot.js
+++ b/lib/api/client-commands/takeHeapSnapshot.js
@@ -39,12 +39,7 @@ class TakeHeapSnapshot extends ClientCommand {
 
     const {heapSnapshotLocation} = this;
     
-    this.transportActions
-      .takeHeapSnapshot(heapSnapshotLocation, callback)
-      .catch(err => {
-        Logger.error(err);
-        callback(err);
-      });
+    this.transportActions.takeHeapSnapshot(heapSnapshotLocation, callback);
   }
 
   command(heapSnapshotLocation, callback) {
diff --git a/lib/api/protocol/window/close.js b/lib/api/client-commands/window/close.js
similarity index 81%
rename from lib/api/protocol/window/close.js
rename to lib/api/client-commands/window/close.js
index d07ebc1a..d443b840 100644
--- a/lib/api/protocol/window/close.js
+++ b/lib/api/client-commands/window/close.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Close the current window or tab. This can be useful when you're working with multiple windows/tabs open (e.g. an OAuth login).
@@ -26,8 +26,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.open
  * @api protocol.window
  */
-module.exports = class Action extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.closeWindow(callback);
+class CloseWindow extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.closeWindow(callback);
   }
-};
+}
+
+module.exports = CloseWindow;
diff --git a/lib/api/protocol/window/fullscreen.js b/lib/api/client-commands/window/fullscreen.js
similarity index 76%
rename from lib/api/protocol/window/fullscreen.js
rename to lib/api/client-commands/window/fullscreen.js
index da5e7d07..c4b828cc 100644
--- a/lib/api/protocol/window/fullscreen.js
+++ b/lib/api/client-commands/window/fullscreen.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Set the current window state to fullscreen, similar to pressing F11 in most browsers.
@@ -24,8 +24,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.minimize
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.fullscreenWindow(callback);
+class FullscreenWindow extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.fullscreenWindow(callback);
   }
-};
+}
+
+module.exports = FullscreenWindow;
diff --git a/lib/api/protocol/window/getAllHandles.js b/lib/api/client-commands/window/getAllHandles.js
similarity index 79%
rename from lib/api/protocol/window/getAllHandles.js
rename to lib/api/client-commands/window/getAllHandles.js
index abbfc7e4..b3f0e77f 100644
--- a/lib/api/protocol/window/getAllHandles.js
+++ b/lib/api/client-commands/window/getAllHandles.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Retrieve the list of all window handles available to the session.
@@ -26,8 +26,10 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#get-window-handles
  * @api protocol.window
  */
-module.exports = class Action extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getAllWindowHandles(callback);
+class GetAllWindowHandles extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getAllWindowHandles(callback);
   }
-};
+}
+
+module.exports = GetAllWindowHandles;
diff --git a/lib/api/protocol/window/getHandle.js b/lib/api/client-commands/window/getHandle.js
similarity index 82%
rename from lib/api/protocol/window/getHandle.js
rename to lib/api/client-commands/window/getHandle.js
index 27e9b607..57d763a3 100644
--- a/lib/api/protocol/window/getHandle.js
+++ b/lib/api/client-commands/window/getHandle.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Retrieve the current window handle.
@@ -28,8 +28,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.switchTo
  * @api protocol.window
  */
-module.exports = class Action extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getWindowHandle(callback);
+class GetWindowHandle extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getWindowHandle(callback);
   }
-};
+}
+
+module.exports = GetWindowHandle;
diff --git a/lib/api/protocol/window/getPosition.js b/lib/api/client-commands/window/getPosition.js
similarity index 79%
rename from lib/api/protocol/window/getPosition.js
rename to lib/api/client-commands/window/getPosition.js
index 75aa65ee..f8cc82d9 100644
--- a/lib/api/protocol/window/getPosition.js
+++ b/lib/api/client-commands/window/getPosition.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Get the coordinates of the top left corner of the current window.
@@ -25,8 +25,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.getRect
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getWindowPosition(callback);
+class GetWindowPosition extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getWindowPosition(callback);
   }
-};
+}
+
+module.exports = GetWindowPosition;
diff --git a/lib/api/protocol/window/getRect.js b/lib/api/client-commands/window/getRect.js
similarity index 86%
rename from lib/api/protocol/window/getRect.js
rename to lib/api/client-commands/window/getRect.js
index 679979e6..3af47bc0 100644
--- a/lib/api/protocol/window/getRect.js
+++ b/lib/api/client-commands/window/getRect.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Fetches the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect) - size and position of the current window.
@@ -36,8 +36,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.getSize
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getWindowRect(callback);
+class GetWindowRect extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getWindowRect(callback);
   }
-};
+}
+
+module.exports = GetWindowRect;
diff --git a/lib/api/protocol/window/getSize.js b/lib/api/client-commands/window/getSize.js
similarity index 79%
rename from lib/api/protocol/window/getSize.js
rename to lib/api/client-commands/window/getSize.js
index 841ec708..47d39a6a 100644
--- a/lib/api/protocol/window/getSize.js
+++ b/lib/api/client-commands/window/getSize.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Get the size of the current window in pixels.
@@ -25,8 +25,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.getRect
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.getWindowSize(callback);
+class GetWindowSize extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.getWindowSize(callback);
   }
-};
+}
+
+module.exports = GetWindowSize;
diff --git a/lib/api/protocol/window/maximize.js b/lib/api/client-commands/window/maximize.js
similarity index 76%
rename from lib/api/protocol/window/maximize.js
rename to lib/api/client-commands/window/maximize.js
index 1eccdb70..1e7f2542 100644
--- a/lib/api/protocol/window/maximize.js
+++ b/lib/api/client-commands/window/maximize.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Increases the window to the maximum available size without going full-screen.
@@ -24,8 +24,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.fullscreen
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.maximizeWindow(callback);
+class MaximizeWindow extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.maximizeWindow(callback);
   }
-};
+}
+
+module.exports = MaximizeWindow;
diff --git a/lib/api/protocol/window/minimize.js b/lib/api/client-commands/window/minimize.js
similarity index 79%
rename from lib/api/protocol/window/minimize.js
rename to lib/api/client-commands/window/minimize.js
index 1b3f82a4..2432f982 100644
--- a/lib/api/protocol/window/minimize.js
+++ b/lib/api/client-commands/window/minimize.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Hides the window in the system tray. If the window happens to be in fullscreen mode, it is restored the normal state then it will be "iconified" - minimize or hide the window from the visible screen.
@@ -24,8 +24,10 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.fullscreen
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
-  command(callback) {
-    return this.transportActions.minimizeWindow(callback);
+class MinimizeWindow extends ClientCommand {
+  performAction(callback) {
+    this.transportActions.minimizeWindow(callback);
   }
-};
+}
+
+module.exports = MinimizeWindow;
diff --git a/lib/api/protocol/window/open.js b/lib/api/client-commands/window/open.js
similarity index 81%
rename from lib/api/protocol/window/open.js
rename to lib/api/client-commands/window/open.js
index 134c0317..754e00b9 100644
--- a/lib/api/protocol/window/open.js
+++ b/lib/api/client-commands/window/open.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Opens a new tab (default) or a separate new window, and changes focus to the newly opened tab/window.
@@ -38,7 +38,7 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.switchTo
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
+class OpenNewWindow extends ClientCommand {
   static get isTraceable() {
     return true;
   }
@@ -47,12 +47,22 @@ module.exports = class Session extends ProtocolAction {
     return 'openNew';
   }
 
+  performAction(callback) {
+    const {windowType} = this;
+
+    this.transportActions.openNewWindow(windowType, callback);
+  }
+
   command(type = 'tab', callback) {
     if (typeof type == 'function' && callback === undefined) {
       callback = arguments[0];
       type = 'tab';
     }
 
-    return this.transportActions.openNewWindow(type, callback);
+    this.windowType = type;
+
+    return super.command(callback);
   }
-};
+}
+
+module.exports = OpenNewWindow;
diff --git a/lib/api/protocol/window/setPosition.js b/lib/api/client-commands/window/setPosition.js
similarity index 76%
rename from lib/api/protocol/window/setPosition.js
rename to lib/api/client-commands/window/setPosition.js
index e140d04b..92ba88f2 100644
--- a/lib/api/protocol/window/setPosition.js
+++ b/lib/api/client-commands/window/setPosition.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Set the position of the current window - move the window to the chosen position.
@@ -27,12 +27,23 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.setRect
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
+class SetWIndowPosition extends ClientCommand {
+  performAction(callback) {
+    const {xOffset, yOffset} = this;
+
+    this.transportActions.setWindowPosition(xOffset, yOffset, callback);
+  }
+
   command(x, y, callback) {
     if (typeof x !== 'number' || typeof y !== 'number') {
       throw new Error('Coordinates passed to .window.getPosition() must be of type number.');
     }
 
-    return this.transportActions.setWindowPosition(x, y, callback);
+    this.xOffset = x;
+    this.yOffset = y;
+
+    return super.command(callback);
   }
-};
+}
+
+module.exports = SetWIndowPosition;
diff --git a/lib/api/protocol/window/setRect.js b/lib/api/client-commands/window/setRect.js
similarity index 88%
rename from lib/api/protocol/window/setRect.js
rename to lib/api/client-commands/window/setRect.js
index 3eab494b..70dfaeb9 100644
--- a/lib/api/protocol/window/setRect.js
+++ b/lib/api/client-commands/window/setRect.js
@@ -1,5 +1,5 @@
-const ProtocolAction = require('../_base-action.js');
-const Utils = require('../../../utils');
+const ClientCommand = require('../_base-command.js');
+const Utils = require('../../../utils/index.js');
 
 /**
  * Change the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect) - size and position of the current window.
@@ -44,7 +44,13 @@ const Utils = require('../../../utils');
  * @see window.setSize
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
+class SetWindowRect extends ClientCommand {
+  performAction(callback) {
+    const {windowOptions} = this;
+
+    this.transportActions.setWindowRect(windowOptions, callback);
+  }
+
   command(options, callback) {
     const {width, height, x, y} = options;
 
@@ -78,6 +84,10 @@ module.exports = class Session extends ProtocolAction {
       }
     }
 
-    return this.transportActions.setWindowRect(options, callback);
+    this.windowOptions = options;
+
+    return super.command(callback);
   }
-};
+}
+
+module.exports = SetWindowRect;
diff --git a/lib/api/protocol/window/setSize.js b/lib/api/client-commands/window/setSize.js
similarity index 75%
rename from lib/api/protocol/window/setSize.js
rename to lib/api/client-commands/window/setSize.js
index 122bfa24..a518491f 100644
--- a/lib/api/protocol/window/setSize.js
+++ b/lib/api/client-commands/window/setSize.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Set the size of the current window in CSS pixels.
@@ -25,16 +25,27 @@ const ProtocolAction = require('../_base-action.js');
  * @see window.setRect
  * @api protocol.window
  */
-module.exports = class Session extends ProtocolAction {
+class SetWindowSize extends ClientCommand {
   static get AliasName() {
     return 'resize';
   }
 
+  performAction(callback) {
+    const {width, height} = this;
+
+    this.transportActions.setWindowSize(width, height, callback);
+  }
+
   command(width, height, callback) {
     if (typeof width !== 'number' || typeof height !== 'number') {
       throw new Error('First two arguments passed to .window.getPosition() must be of type number.');
     }
 
-    return this.transportActions.setWindowSize(width, height, callback);
+    this.width = width;
+    this.height = height;
+
+    return super.command(callback);
   }
-};
+}
+
+module.exports = SetWindowSize;
diff --git a/lib/api/protocol/window/switchTo.js b/lib/api/client-commands/window/switchTo.js
similarity index 90%
rename from lib/api/protocol/window/switchTo.js
rename to lib/api/client-commands/window/switchTo.js
index 99f972fb..7a2b6309 100644
--- a/lib/api/protocol/window/switchTo.js
+++ b/lib/api/client-commands/window/switchTo.js
@@ -1,4 +1,4 @@
-const ProtocolAction = require('../_base-action.js');
+const ClientCommand = require('../_base-command.js');
 
 /**
  * Change focus to another window.
@@ -72,7 +72,7 @@ const ProtocolAction = require('../_base-action.js');
  * @link /#switch-to-window
  * @api protocol.window
  */
-module.exports = class Action extends ProtocolAction {
+class SwitchToWindow extends ClientCommand {
   static get isTraceable() {
     return true;
   }
@@ -81,11 +81,21 @@ module.exports = class Action extends ProtocolAction {
     return 'switch';
   }
 
+  performAction(callback) {
+    const {windowHandle} = this;
+
+    this.transportActions.switchToWindow(windowHandle, callback);
+  }
+
   command(windowHandle, callback) {
     if (typeof windowHandle !== 'string') {
       throw new Error(`windowHandle argument passed to .window.switchTo() must be a string; received: ${typeof windowHandle} (${windowHandle}).`);
     }
 
-    return this.transportActions.switchToWindow(windowHandle, callback);
+    this.windowHandle = windowHandle;
+
+    return super.command(callback);
   }
-};
+}
+
+module.exports = SwitchToWindow;
diff --git a/lib/api/protocol/windowRect.js b/lib/api/protocol/windowRect.js
index 391750b3..663478b3 100644
--- a/lib/api/protocol/windowRect.js
+++ b/lib/api/protocol/windowRect.js
@@ -22,13 +22,13 @@ const ProtocolAction = require('./_base-action.js');
  *      browser.windowRect({width: 600, height: 300});
  *
  *      // Retrieve the attributes
- *      browser.windowRect(function(result) {
+ *      browser.windowRect(null, function(result) {
  *        console.log(result.value);
  *      });
  *   },
  *
  *   'windowRect ES6 demo test': async function(browser) {
- *      const resultValue = await browser.windowRect();
+ *      const resultValue = await browser.windowRect(null);
  *      console.log('result value', resultValue);
  *   }
  * }
diff --git a/lib/api/web-element/commands/findByLabelText.js b/lib/api/web-element/commands/findByLabelText.js
index 60f82299..44a5d55c 100644
--- a/lib/api/web-element/commands/findByLabelText.js
+++ b/lib/api/web-element/commands/findByLabelText.js
@@ -45,12 +45,11 @@ module.exports.command = function (text, {exact = true, timeout, retryInterval,
       return null;
     }
 
-    const elements = await labelWebElement.findElement(By.css('input'));
-    if (!elements) {
+    try {
+      return await labelWebElement.findElement(By.css('input'));
+    } catch (err) {
       return null;
     }
-
-    return elements[0];
   };
 
   const findByDeepNesting = async (text, {exact}) => {
@@ -69,13 +68,7 @@ module.exports.command = function (text, {exact = true, timeout, retryInterval,
     }
 
     try {
-      const element = await labelElement.findElement(By.css('input'));
-
-      if (element.length === 0) {
-        return null;
-      }
-
-      return element[0];
+      return await labelElement.findElement(By.css('input'));
     } catch (err) {
       return null;
     }
diff --git a/lib/api/web-element/commands/sendKeys.js b/lib/api/web-element/commands/sendKeys.js
index 7c5fa292..eca11527 100644
--- a/lib/api/web-element/commands/sendKeys.js
+++ b/lib/api/web-element/commands/sendKeys.js
@@ -1,5 +1,12 @@
-module.exports.command = function(...keys) {
+module.exports.command = function(...args) {
+  const keys = args.reduce((prev, key) => {
+    const keyList = Array.isArray(key) ? key : [key];
+    prev.push(...keyList);
+
+    return prev;
+  }, []);
+
   return this.runQueuedCommand('sendKeysToElement', {
-    args: keys
+    args: [keys]
   });
-};
\ No newline at end of file
+};
diff --git a/lib/api/web-element/commands/setValue.js b/lib/api/web-element/commands/setValue.js
new file mode 100644
index 00000000..d27c86ae
--- /dev/null
+++ b/lib/api/web-element/commands/setValue.js
@@ -0,0 +1,12 @@
+module.exports.command = function(...args) {
+  const keys = args.reduce((prev, key) => {
+    const keyList = Array.isArray(key) ? key : [key];
+    prev.push(...keyList);
+
+    return prev;
+  }, []);
+
+  return this.runQueuedCommand('setElementValue', {
+    args: [keys]
+  });
+};
diff --git a/lib/api/web-element/commands/update.js b/lib/api/web-element/commands/update.js
index 4916657e..d27c86ae 100644
--- a/lib/api/web-element/commands/update.js
+++ b/lib/api/web-element/commands/update.js
@@ -1,5 +1,12 @@
-module.exports.command = function(...keys) {
+module.exports.command = function(...args) {
+  const keys = args.reduce((prev, key) => {
+    const keyList = Array.isArray(key) ? key : [key];
+    prev.push(...keyList);
+
+    return prev;
+  }, []);
+
   return this.runQueuedCommand('setElementValue', {
-    args: keys
+    args: [keys]
   });
-};
\ No newline at end of file
+};
diff --git a/lib/api/web-element/element-locator.js b/lib/api/web-element/element-locator.js
index 7e1bcf02..46ceb965 100644
--- a/lib/api/web-element/element-locator.js
+++ b/lib/api/web-element/element-locator.js
@@ -7,7 +7,7 @@ const NightwatchLocator = require('../../element/locator-factory');
 
 class ElementLocator {
   constructor(selector, options = {}) {
-    this.index = ElementLocator.getOrDefault(selector, 'index', 0);
+    this.index = ElementLocator.getOrDefault(selector, '__index', 0);
     this.timeout = ElementLocator.getOrDefault(selector, 'timeout', options.waitForConditionTimeout);
     this.retryInterval = ElementLocator.getOrDefault(selector, 'retryInterval', options.waitForConditionPollInterval);
     this.locateStrategy = ElementLocator.getLocateStrategy(selector, options.locateStrategy);
@@ -80,6 +80,10 @@ class ElementLocator {
     }
 
     if (element instanceof Element) {
+      if (element.usingRecursion) {
+        return element;
+      }
+
       return NightwatchLocator.create(element);
     }
 
diff --git a/lib/api/web-element/factory.js b/lib/api/web-element/factory.js
index ec05375c..e44ce740 100644
--- a/lib/api/web-element/factory.js
+++ b/lib/api/web-element/factory.js
@@ -1,6 +1,7 @@
 const ScopedWebElements = require('./scoped-elements.js');
 const ScopedWebElement = require('./scoped-element.js');
 const ScopedElementAssertions = require('./assert/element-assertions.js');
+const WaitUntil = require('./waitUntil.js');
 
 const Element = require('../../element');
 const ScopedValueAssertions = require('./assert/value-assertions.js');
@@ -43,6 +44,30 @@ const createScopedWebElement = function(selector, parentElement, nightwatchInsta
     configurable: true
   });
 
+  Object.defineProperty(exported, 'waitUntil', {
+    value(args) {
+
+
+      return createScopedWebElement(new Promise(function (resolve, reject) {
+
+        if (typeof args === 'string') {
+          args = {action: args};
+        }
+
+        const waitUntil = new WaitUntil(instance, {
+          selector,
+          nightwatchInstance,
+          ...args
+        });
+  
+        return waitUntil.wait()
+          .then(element => resolve(element));
+        
+      }.bind(instance)), parentElement, nightwatchInstance);
+    }
+    
+  });
+
   Object.defineProperty(exported, 'then', {
     value(onFulfilled, onRejected) {
       return instance.then(onFulfilled, onRejected);
diff --git a/lib/api/web-element/scoped-element.js b/lib/api/web-element/scoped-element.js
index 2b934a9b..44c145c7 100644
--- a/lib/api/web-element/scoped-element.js
+++ b/lib/api/web-element/scoped-element.js
@@ -103,29 +103,73 @@ class ScopedWebElement {
     return webElements;
   }
 
-  async findElement({parentElement, condition, index, timeout, retryInterval}) {
+  async findElementUsingRecursion({parentElement, recursiveElement, timeout, retryInterval}) {
+    const allElements = Array.isArray(recursiveElement.selector) ? recursiveElement.selector.slice() : [
+      recursiveElement
+    ];
+
+    let result = null;
+    while (allElements.length > 0) {
+      const nextElement = allElements.shift();
+      if (result) {
+        parentElement = {webElement: result};
+      }
+
+      const {condition, index} = ScopedElementLocator.create(nextElement, this.nightwatchInstance);
+      result = await this.findElement({parentElement, selector: condition, index, timeout, retryInterval});
+
+      if (!result) {
+        break;
+      }
+    }
+
+    return result;
+  }
+
+  async findElement({parentElement, selector, index, timeout, retryInterval}) {
+    const webElements = await this.locateElements({parentElement, selector, timeout, retryInterval});
+
+    if (webElements.length === 0) {
+      const err = new Error(`Cannot find element with "${selector}" selector in ${timeout} milliseconds.`);
+      this.reporter.registerTestError(err);
 
+      return;
+    }
+
+    if (index > webElements.length) {
+      const err = new Error(`The index "${index}" is out of bounds for selector "${selector}".`);
+      this.reporter.registerTestError(err);
+
+      return;
+    }
+
+    return webElements[index];
+  }
+
+  async findElementAction({parentElement, condition, index, timeout, retryInterval, abortOnFailure, suppressNotFoundErrors}) {
     const createAction = () => async ({args}) => {
       const parentElement = args[0];
 
       try {
-        const webElements = await this.locateElements({parentElement, selector: condition, timeout, retryInterval});
-        if (webElements.length === 0) {
-          const err = new Error(`Cannot find element with "${condition}" selector in ${timeout} milliseconds.`);
-          this.reporter.registerTestError(err);
+        if (condition.usingRecursion) {
+          return await this.findElementUsingRecursion({parentElement, recursiveElement: condition, timeout, retryInterval});
+        }
 
-          return;
+        return await this.findElement({parentElement, selector: condition, index, timeout, retryInterval});
+      } catch (error) {
+        if (suppressNotFoundErrors) {
+          return null;
         }
 
-        if (index > webElements.length) {
-          const err = new Error(`The index "${index}" is out of bounds for selector "${condition}".`);
-          this.reporter.registerTestError(err);
+        const narrowedError = createNarrowedError({error, condition, timeout});
+        Logger.error(narrowedError);
 
-          return;
+        if (abortOnFailure) {
+          this.reporter.registerTestError(narrowedError);
+          // TODO: find a way to reject here without unhandled promise rejection
+          // reject(narrowedError);
         }
 
-        return webElements[index];
-      } catch (error) {
         return null;
       }
     };
@@ -181,25 +225,10 @@ class ScopedWebElement {
         return resolve(condition);
       }
 
-      try {
-        const webElement = await this.findElement({parentElement, condition, index, timeout, retryInterval});
-
-        resolve(webElement);
-      } catch (error) {
-        if (suppressNotFoundErrors) {
-          return resolve(null);
-        }
-
-        const narrowedError = createNarrowedError({error, condition, timeout});
-        Logger.error(narrowedError);
-
-        if (abortOnFailure) {
-          this.reporter.registerTestError(narrowedError);
-          // TODO: find a way to reject here without unhandled promise rejection
-          // reject(narrowedError);
-        }
-        resolve(null);
-      }
+      const webElement = await this.findElementAction(
+        {parentElement, condition, index, timeout, retryInterval, abortOnFailure, suppressNotFoundErrors}
+      );
+      resolve(webElement);
     }));
   }
 
diff --git a/lib/api/web-element/scoped-elements.js b/lib/api/web-element/scoped-elements.js
index bb057529..615725ae 100644
--- a/lib/api/web-element/scoped-elements.js
+++ b/lib/api/web-element/scoped-elements.js
@@ -21,7 +21,7 @@ class ScopedElements {
         resolve(selector);
       } else {
         try {
-          const webElements = await this.findElements(selector);
+          const webElements = await this.findElementsAction(selector);
 
           resolve(webElements);
         } catch (error) {
@@ -31,7 +31,39 @@ class ScopedElements {
     });
   }
 
-  async findElements(descriptor) {
+  async findElementsUsingRecursion({parentElement, recursiveElement, timeout, retryInterval}) {
+    const allElements = Array.isArray(recursiveElement.selector) ? recursiveElement.selector.slice() : [
+      recursiveElement
+    ];
+
+    let result = [];
+    while (allElements.length > 0) {
+      const nextElement = allElements.shift();
+      if (result.length) {
+        parentElement = {webElement: result[0]};
+      }
+
+      const {condition} = ScopedElementLocator.create(nextElement, this.nightwatchInstance);
+      result = await this.findElements({parentElement, selector: condition, timeout, retryInterval});
+
+      if (!result || result.length === 0) {
+        result = [];
+        break;
+      }
+    }
+
+    return result;
+  }
+
+  async findElements({parentElement, selector, timeout, retryInterval}) {
+    const webElements = parentElement && parentElement.webElement
+      ? await parentElement.webElement.findElements(selector)
+      : await this.nightwatchInstance.transport.driver.wait(until.elementsLocated(selector), timeout,  null, retryInterval);
+
+    return webElements;
+  }
+
+  async findElementsAction(descriptor) {
     const {
       timeout,
       condition,
@@ -44,11 +76,11 @@ class ScopedElements {
       const parentElement = args[0];
 
       try {
-        const webElements = parentElement && parentElement.webElement
-          ? await parentElement.webElement.findElements(condition)
-          : await this.nightwatchInstance.transport.driver.wait(until.elementsLocated(condition), timeout,  null, retryInterval);
+        if (condition.usingRecursion) {
+          return await this.findElementsUsingRecursion({parentElement, recursiveElement: condition, timeout, retryInterval});
+        }
 
-        return webElements;
+        return await this.findElements({parentElement, selector: condition, timeout, retryInterval});
       } catch (error) {
         if (suppressNotFoundErrors) {
           return [];
diff --git a/lib/api/web-element/waitUntil.js b/lib/api/web-element/waitUntil.js
new file mode 100644
index 00000000..32a99ed4
--- /dev/null
+++ b/lib/api/web-element/waitUntil.js
@@ -0,0 +1,127 @@
+const until = require('selenium-webdriver/lib/until');
+const {AssertionRunner} = require('../../assertion');
+const {isDefined, format} = require('../../utils');
+
+const mapToSeleniumFunction = {
+  'selected': until.elementIsSelected,
+  'not.selected': until.elementIsNotSelected,
+  'visible': until.elementIsVisible,
+  'not.visible': until.elementIsNotVisible,
+  'enabled': until.elementIsEnabled,
+  'not.enabled': until.elementIsDisabled,
+  'disabled': until.elementIsDisabled
+};
+
+
+
+class WaitUntil {
+  constructor(scopedElement, {action, timeout, retryInterval, message, nightwatchInstance, selector, abortOnFailure}) {
+    this.scopedElement = scopedElement;
+    this.conditionFn = mapToSeleniumFunction[action];
+    this.nightwatchInstance = nightwatchInstance;
+    this.action = action;
+    this.message = message;
+    this.selector = selector;
+    this.timeout = timeout || this.nightwatchInstance.settings.globals.waitForConditionTimeout;
+    this.retryInterval = retryInterval || this.nightwatchInstance.settings.globals.waitForConditionPollInterval;
+    this.abortOnFailure = isDefined(abortOnFailure) ? abortOnFailure : this.nightwatchInstance.settings.globals.abortOnAssertionFailure;
+ 
+  }
+
+  createNode() {
+    const createAction = (actions, webElement) =>  function() {
+      if (!this.conditionFn) {
+        throw new Error(`Invalid action ${this.action} for WaitUntil Command. Possible actions: ${Object.keys(mapToSeleniumFunction).toString()}`);
+      }
+
+      return actions['wait'](this.conditionFn(webElement), this.timeout, this.retryInterval, this.message, ()=>{})
+        .then(result => {
+          const elapsedTime = new Date().getTime() - node.startTime;
+          if (result.error) {
+            const actual = result.error.name === 'NightwatchElementError' ? 'not found' : null;
+
+            return this.fail(result, actual, elapsedTime);
+          }
+
+          return this.pass(result, elapsedTime);
+        })
+        .catch(err=> err)
+        .then(result => {
+          node.deferred.resolve(result);
+
+          return result;
+        });
+    }.bind(this);
+    
+    const node = this.scopedElement.queueAction({name: 'waitUntil', createAction});
+
+    return node;
+  }
+
+
+  async wait() {
+    const assertApi = this.nightwatchInstance.api.assert;
+    try {
+      const node =  this.createNode();
+     
+      return this.scopedElement.waitFor(node.deferred.promise);
+    } catch (err) {
+      assertApi.ok(false, err.message);
+    }
+  }
+
+  formatMsg(message, timeMs) {
+    const defaultMsg = this.message || message;
+    
+    return format(defaultMsg, this.selector, timeMs);
+  }
+
+  assert({result, passed, err, message, elapsedTime}) {
+    const {reporter} = this.scopedElement;
+    
+    const runner = new AssertionRunner({abortOnFailure: this.abortOnFailure, passed, err, message, reporter, elapsedTime});
+
+    return runner.run(result);
+  }
+
+  pass(result, elapsedTime) {
+    const sliceAction = this.action.split('.');
+    const expected = sliceAction[0] === 'not' ? `not ${sliceAction[1]}` : this.action;
+
+    const message = this.formatMsg(`Element <%s> was ${expected} in %d milliseconds`, elapsedTime);
+
+    return this.assert({
+      result,
+      passed: true,
+      err: {
+        expected
+      },
+      elapsedTime,
+      message
+    });
+  }
+
+  fail(result, actual, elapsedTime) {
+
+    const sliceAction = this.action.split('.');
+    
+    actual = actual ? actual : sliceAction[0] === 'not' ? sliceAction[1] : `not ${this.action}`;
+    const expected = sliceAction[0] === 'not' ? `not ${sliceAction[1]}` : this.action;
+    const message = this.formatMsg(`Timed out while waiting for element <%s> to be ${expected} for %d milliseconds`, this.timeout);
+
+    return this.assert({
+      result,
+      passed: false,
+      message,
+      err: {
+        actual,
+        expected
+      },
+      elapsedTime
+    });
+  }
+
+
+}
+
+module.exports = WaitUntil;
\ No newline at end of file
diff --git a/lib/core/asynctree.js b/lib/core/asynctree.js
index 3abd4101..317310c4 100644
--- a/lib/core/asynctree.js
+++ b/lib/core/asynctree.js
@@ -47,6 +47,10 @@ class AsyncTree extends EventEmitter{
     if (childNode) {
       const result = await this.runChildNode(childNode);
 
+      if (result instanceof Error && result.namespace === 'verify') {
+        return null;
+      }
+
       return result;
     }
 
@@ -123,6 +127,7 @@ class AsyncTree extends EventEmitter{
 
     if (result instanceof Error) {
       err = node.handleError(result);
+      err.namespace = node.namespace;
 
       abortOnFailure = err.abortOnFailure || Utils.isUndefined(err.abortOnFailure);
       if (this.foreignRunner) {
diff --git a/lib/core/client.js b/lib/core/client.js
index 64d2296c..41ce5ea9 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -310,6 +310,10 @@ class NightwatchClient extends EventEmitter {
                 }
 
                 return newResult;
+              })
+              .catch(err => {
+                Logger.error(err);
+                callback(err);
               });
           }
 
diff --git a/lib/page-object/base-object.js b/lib/page-object/base-object.js
index c586ef3c..8c8e60a6 100644
--- a/lib/page-object/base-object.js
+++ b/lib/page-object/base-object.js
@@ -5,12 +5,12 @@ const Element = require('../element');
 class BaseObject {
   static get WrappedProtocolCommands() {
     return [
-      'element',
       'elements',
       'elementIdElement',
       'elementIdElements'
     ];
   }
+
   /**
    * Returns the properties object passed as an argument (or null if no arguments are passed).
    *  If the supplied properties argument is a function, it invokes that function with the page as its context.
diff --git a/lib/page-object/command-wrapper.js b/lib/page-object/command-wrapper.js
index b1975a9c..ed6d92d9 100644
--- a/lib/page-object/command-wrapper.js
+++ b/lib/page-object/command-wrapper.js
@@ -1,8 +1,13 @@
 const Element = require('../element');
 const Utils = require('../utils');
 
-function isValidAssertion(commandName) {
-  return ['assert', 'verify', 'expect'].indexOf(commandName) > -1;
+const ALLOWED_NAMESPACES = [
+  'alerts', 'cookies', 'document',
+  'assert', 'verify', 'expect'
+];
+
+function isAllowedNamespace(commandName) {
+  return ALLOWED_NAMESPACES.indexOf(commandName) > -1;
 }
 
 class Command {
@@ -29,8 +34,9 @@ class Command {
     }
 
     const Api = require('../api');
+    const ScopedElementApi = require('../api/_loaders/element-api');
 
-    return Utils.isString(item) && (item.startsWith('@') || Api.isElementCommand(commandName));
+    return Utils.isString(item) && (item.startsWith('@') || Api.isElementCommand(commandName) || ScopedElementApi.isScopedElementCommand(commandName));
   }
 
   static isUserDefinedElementCommand(commandName) {
@@ -220,10 +226,20 @@ class Command {
       //  then we need to retrieve the element using recursion
       const Section = require('./section.js');
       if (this.parent instanceof Section) {
-        args[0] = Element.createFromSelector({
+        inputElement.parent = this.parent;
+        inputElement = inputElement.getRecursiveLookupElement() || Element.createFromSelector({
           locateStrategy: 'recursion',
           selector: [this.parent, inputElement]
         });
+
+        const ScopedElementApi = require('../api/_loaders/element-api');
+        if (ScopedElementApi.isScopedElementCommand(this.commandName)) {
+          // only locate the parent sections recursively and then find the main element using
+          // original method and arguments.
+          this.onlyLocateSectionsRecursively = true;
+        }
+
+        args[0] = inputElement;
       }
     }
   }
@@ -292,22 +308,27 @@ class CommandLoader {
    */
   static applyCommandsToTarget(parent, target, commands) {
     Object.keys(commands).forEach(function(commandName) {
-      if (isValidAssertion(commandName)) {
+      if (isAllowedNamespace(commandName)) {
         target[commandName] = target[commandName] || {};
 
         const isChaiAssertion = commandName === 'expect';
-        const assertions = commands[commandName];
+        const namespace = commands[commandName];
 
-        Object.keys(assertions).forEach(function(assertionName) {
-          target[commandName][assertionName] = CommandLoader.addCommand({
+        Object.keys(namespace).forEach(function(nsCommandName) {
+          target[commandName][nsCommandName] = CommandLoader.addCommand({
             target: target[commandName],
-            commandFn: assertions[assertionName],
-            commandName: assertionName,
+            commandFn: namespace[nsCommandName],
+            commandName: nsCommandName,
             parent,
             isChaiAssertion
           });
         });
       } else {
+        // don't load namespaces here, otherwise they'll be wrapped by a function.
+        if (Utils.isObject(commands[commandName])) {
+          return;
+        }
+
         target[commandName] = CommandLoader.addCommand({
           target,
           commandFn: commands[commandName],
@@ -322,22 +343,65 @@ class CommandLoader {
 
   /**
    * @param parent
-   * @param target
+   * @param originalApi
+   * @param targetApi
+   * @param {string} commandName
+   * @returns {function}
+   */
+  static wrapElementCommand(parent, originalApi, targetApi, commandName) {
+    const originalFn = originalApi[commandName];
+
+    const command = new Command(parent, commandName, false);
+
+    return function(...args) {
+      const origArgs = args.slice();
+
+      command.parseElementSelector(args);
+
+      if (command.onlyLocateSectionsRecursively) {
+        // only happens in case of new scoped element api, when non @-referenced selector is passed
+        // to element api methods for sections. Doing this is not really necessary for `find` and `findAll`
+        // methods but only for testing-library methods (which only accepts string as first argument),
+        // but we do it for them anyways.
+
+        // transfer n-1 selectors to `element()` and the main selector (origArgs) to the actual method.
+        args[0].selector.pop();
+
+        const parentSectionElement = targetApi(args[0]);
+
+        return parentSectionElement[commandName](...origArgs);
+      }
+
+      return originalFn.apply(targetApi, args);
+    }; 
+  }
+
+  /**
+   * @param parent
+   * @param api
    * @param {Array} commands
    */
-  static wrapProtocolCommands(parent, target, commands) {
+  static wrapProtocolCommands(parent, api, commands) {
     commands.forEach(commandName => {
-      const originalFn = target[commandName];
-      target[commandName] = (function () {
-        const command = new Command(parent, commandName, false);
+      api[commandName] = CommandLoader.wrapElementCommand(parent, api, api, commandName);
+    });
+  }
 
-        return function(...args) {
-          command.parseElementSelector(args);
+  static wrapScopedElementApi(parent, api, elementCommands) {
+    const wrappedElementFn = CommandLoader.wrapElementCommand(parent, api, api, 'element');
 
-          return originalFn.apply(target, args);
-        };
-      })();
+    elementCommands.forEach(commandName => {
+      let names = commandName;
+      if (!Array.isArray(names)) {
+        names = [names];
+      }
+
+      names.forEach(commandName => {
+        wrappedElementFn[commandName] = CommandLoader.wrapElementCommand(parent, api.element, wrappedElementFn, commandName);
+      });
     });
+
+    api.element = wrappedElementFn;
   }
 
   static addCommand({target, commandFn, commandName, parent, isChaiAssertion, isES6Async = false, overwrite = false}) {
diff --git a/lib/page-object/index.js b/lib/page-object/index.js
index 11157c9f..b9d802d6 100644
--- a/lib/page-object/index.js
+++ b/lib/page-object/index.js
@@ -1,6 +1,7 @@
 const Utils = require('../utils');
 const CommandWrapper = require('./command-wrapper.js');
 const BaseObject = require('./base-object.js');
+const ScopedElementAPILoader = require('../api/_loaders/element-api');
 
 const shouldReturnPromise = function() {
   return this.client.isES6AsyncTestcase || this.client.isES6AsyncCommand;
@@ -55,17 +56,15 @@ class Page extends BaseObject {
 
     CommandWrapper.addWrappedCommands(this, this.commandLoader);
     CommandWrapper.wrapProtocolCommands(this, this.api, BaseObject.WrappedProtocolCommands);
-
-    ['element', 'alerts', 'document'].forEach((command) => {
-      Object.defineProperty(this, command, {
-        get() {
-          return this.api[command];
-        },
-        enumerable: true,
-        configurable: false
-      });
+    CommandWrapper.wrapScopedElementApi(this, this.api, ScopedElementAPILoader.availableElementCommands);
+
+    Object.defineProperty(this, 'element', {
+      get() {
+        return this.api.element;
+      },
+      enumerable: true,
+      configurable: false
     });
-
   }
 
   get api() {
diff --git a/lib/page-object/section.js b/lib/page-object/section.js
index af2a3576..8220c18c 100644
--- a/lib/page-object/section.js
+++ b/lib/page-object/section.js
@@ -1,5 +1,6 @@
 const Element = require('../element');
 const CommandWrapper = require('./command-wrapper.js');
+const ScopedElementAPILoader = require('../api/_loaders/element-api');
 
 /**
  * Class that all sections subclass from
@@ -37,15 +38,15 @@ class Section extends Element {
 
     CommandWrapper.addWrappedCommands(this, this.commandLoader);
     CommandWrapper.wrapProtocolCommands(this, this.api, BaseObject.WrappedProtocolCommands);
-
-    // TODO: implement support for new element() api on sections
-    // Object.defineProperty(this, 'element', {
-    //   get() {
-    //     return this.api.element;
-    //   },
-    //   enumerable: true,
-    //   configurable: false
-    // });
+    CommandWrapper.wrapScopedElementApi(this, this.api, ScopedElementAPILoader.availableElementCommands);
+
+    Object.defineProperty(this, 'element', {
+      get() {
+        return this.api.element;
+      },
+      enumerable: true,
+      configurable: false
+    });
   }
 }
 
diff --git a/lib/reporter/results.js b/lib/reporter/results.js
index 15a9807b..3776b985 100644
--- a/lib/reporter/results.js
+++ b/lib/reporter/results.js
@@ -49,6 +49,7 @@ module.exports = class Results {
     this.buildName = capabilities.buildName || desiredCapabilities.buildName || '';
     const {webdriver = {}} = settings;
     this.host = webdriver.host || '';
+    this.__retryTest = false;
 
     this.initCount(tests);
   }
@@ -271,12 +272,20 @@ module.exports = class Results {
     return this.currentTestName;
   }
 
+  set retryTest(value) {
+    this.__retryTest = value;
+  }
+
+  get retryTest() {
+    return this.__retryTest;
+  }
+  
   /**
    * @param {TestCase} testcase
    * @return {Object}
    */
   createTestCaseResults(testcase) {
-    return {
+    const result = {
       time: 0,
       assertions: [],
       commands: [],
@@ -288,6 +297,22 @@ module.exports = class Results {
       tests: 0,
       status: Results.TEST_PASS
     };
+
+    if (this.retryTest && this.testSections[testcase.testName]) {
+      const retryTestData = this.testSections[testcase.testName];
+
+      result['retryTestData'] = [retryTestData];
+
+      if (retryTestData['retryTestData']) {
+        result['retryTestData'].push(...retryTestData['retryTestData']);
+
+        delete retryTestData['retryTestData'];
+      }
+
+      this.retryTest = false;
+    }
+
+    return result;
   }
 
   resetLastError() {
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index cbe454ea..52b18a19 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -9,8 +9,8 @@ const Utils = require('../../utils');
 const Runner = require('../runner.js');
 const ProcessListener = require('../process-listener.js');
 const analyticsCollector = require('../../utils/analytics.js');
+const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator, isAndroid} = require('../../utils/mobile.js');
 const {Logger, singleSourceFile, isSafari, isLocalhost} = Utils;
-const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator} = require('../../utils/mobile.js');
 
 class CliRunner {
   static get CONFIG_FILE_JS() {
@@ -115,6 +115,7 @@ class CliRunner {
   initTestSettings(userSettings = {}, baseSettings = null, argv = null, testEnv = '', asyncLoading) {
     this.test_settings = Settings.parse(userSettings, baseSettings, argv, testEnv);
 
+    this.setMobileOptions(argv);
     this.setLoggingOptions();
     this.setupGlobalHooks();
 
@@ -127,7 +128,6 @@ class CliRunner {
     }
 
     this.globalsSetup(argv);
-    this.setMobileOptions(argv);
 
     return this;
   }
@@ -148,16 +148,22 @@ class CliRunner {
   }
   
   setMobileOptions(argv) {
-    const {desiredCapabilities} = this.test_settings;
+    const {desiredCapabilities, selenium = {}} = this.test_settings;
 
-    if (!isRealIos(desiredCapabilities)) {
-      return;
+    if (isRealIos(desiredCapabilities) && !selenium.use_appium) {
+      if (argv.deviceId) {
+        this.test_settings.desiredCapabilities['safari:deviceUDID'] = iosRealDeviceUDID(argv.deviceId);
+      } else if (!desiredCapabilities['safari:deviceUDID']) {
+        throw new RealIosDeviceIdError();
+      }
     }
 
-    if (argv.deviceId) {
-      this.test_settings.desiredCapabilities['safari:deviceUDID'] = iosRealDeviceUDID(argv.deviceId);
-    } else if (!desiredCapabilities['safari:deviceUDID']) {
-      throw new RealIosDeviceIdError();
+    if (isAndroid(desiredCapabilities) && argv.deviceId && !selenium.use_appium) {
+      if (desiredCapabilities['goog:chromeOptions']) {
+        this.test_settings.desiredCapabilities['goog:chromeOptions'].androidDeviceSerial = argv.deviceId;
+      } else if (desiredCapabilities['moz:firefoxOptions']) {
+        this.test_settings.desiredCapabilities['moz:firefoxOptions'].androidDeviceSerial = argv.deviceId;
+      }
     }
   }
 
@@ -386,6 +392,11 @@ class CliRunner {
   isTestWorkersEnabled() {
     const testWorkers =  this.test_settings.testWorkersEnabled && !singleSourceFile(this.argv);
     if (!testWorkers) {
+
+      if (this.argv.debug) {
+        Logger.info('Disabling parallelism while running in debug mode');
+      }
+
       return false;
     }
 
diff --git a/lib/runner/process-listener.js b/lib/runner/process-listener.js
index 2bcf4166..07c0fcb6 100644
--- a/lib/runner/process-listener.js
+++ b/lib/runner/process-listener.js
@@ -80,6 +80,9 @@ module.exports = class {
   }
 
   uncaught(err, {type = 'uncaughtException'} = {}) {
+    Logger.setOutputEnabled(true); // force log for uncaught exception
+    Logger.enable();
+
     Logger.error(`${type}: ${err.message}\n${err.stack}`);
     analyticsCollector.exception(err);
 
diff --git a/lib/settings/settings.js b/lib/settings/settings.js
index e7828e70..cbaaef6e 100644
--- a/lib/settings/settings.js
+++ b/lib/settings/settings.js
@@ -102,8 +102,21 @@ class Settings {
 
   get testWorkersEnabled() {
     const {test_workers} = this.settings;
+    const {serial, debug, parallel} = this.argv;
 
-    return  this.argv.serial ? false : (this.argv.parallel || test_workers === true || (test_workers && test_workers.enabled && test_workers.workers !== 1));
+    if (serial || debug) {
+      return false;
+    }
+
+    if (parallel || test_workers === true) {
+      return true;
+    }
+
+    if (test_workers && test_workers.enabled && (test_workers.workers === 'auto' || test_workers.workers > 1)) {
+      return true;
+    }
+
+    return false;
   }
 
   /**
@@ -304,6 +317,10 @@ class Settings {
       this.settings.parallel_mode = true;
     }
 
+    if (isObject(this.settings.test_workers)) {
+      this.settings.test_workers.workers = this.settings.test_workers.workers || 'auto';
+    }
+
     if (this.argv.parallel === true && !this.settings.test_workers) {
       this.settings.test_workers = true;
     } else if (isNumber(this.argv.parallel)) {
diff --git a/lib/transport/errors/index.js b/lib/transport/errors/index.js
index 5c539ee2..90308435 100644
--- a/lib/transport/errors/index.js
+++ b/lib/transport/errors/index.js
@@ -133,6 +133,11 @@ const Errors = {
   }
 };
 
+const IosSessionErrors = {
+  SessionNotCreatedError: ErrorCode.SESSION_NOT_CREATED_EXCEPTION,
+  UnsupportedOperationError: ErrorCode.UNSUPPORTED_OPERATION
+};
+
 const SeleniumNightwatchErrorCodeMap = {
   NoSuchElementError: ErrorCode.NO_SUCH_ELEMENT
 };
@@ -176,5 +181,6 @@ module.exports = {
   },
 
   StatusCode: ErrorCode,
-  Response: Errors
+  Response: Errors,
+  IosSessionErrors: IosSessionErrors
 };
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index 4a8824c1..71231c32 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -3,14 +3,15 @@ const {Builder, Browser, error} = require('selenium-webdriver');
 
 const Actions = require('./actions.js');
 const SeleniumCapabilities = require('./options.js');
-const {Logger, isObject, IosSessionNotCreatedError, AndroidConnectionError} = require('../../utils');
+const {Logger, isObject} = require('../../utils');
+const {IosSessionNotCreatedError, AndroidConnectionError} = require('../../utils/mobile');
 const httpClient = require('./httpclient.js');
 const Session = require('./session.js');
 const BaseTransport = require('../');
 const CDP = require('./cdp.js');
 const {colors} = Logger;
 const {isErrorResponse, checkLegacyResponse, throwDecodedError, WebDriverError} = error;
-
+const {IosSessionErrors} = require('../errors');
 
 let _driverService = null;
 let _driver = null;
@@ -436,7 +437,7 @@ class Transport extends BaseTransport {
         return new AndroidConnectionError(err);
       }
 
-      if (err.name === 'SessionNotCreatedError' && this.api.isSafari() && this.api.isIOS()) {
+      if (IosSessionErrors[err.name] && this.api.isSafari() && this.api.isIOS()) {
         return new IosSessionNotCreatedError(err, this.desiredCapabilities);
       }
     }
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index 11801f69..736bfc7d 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -1,4 +1,4 @@
-const {WebElement, WebDriver, Origin, By, until} = require('selenium-webdriver');
+const {WebElement, WebDriver, Origin, By, until, Condition} = require('selenium-webdriver');
 const {Locator} = require('../../element');
 const NightwatchLocator = require('../../element/locator-factory.js');
 const {isString} = require('../../utils');
@@ -1036,7 +1036,7 @@ module.exports = class MethodMappings {
         ///////////////////////////////////////////////////////////////////////////
         async wait(conditionFn, timeMs, message, retryInterval) {
           const driver = new WebDriver(Promise.resolve());
-          await driver.wait(conditionFn(), timeMs, message, retryInterval);
+          await driver.wait(conditionFn instanceof Condition ? conditionFn : conditionFn(), timeMs, message, retryInterval);
 
           return {
             value: null
diff --git a/lib/utils/mobile.js b/lib/utils/mobile.js
index 9a8bca04..9d74c08c 100644
--- a/lib/utils/mobile.js
+++ b/lib/utils/mobile.js
@@ -294,6 +294,19 @@ class IosSessionNotCreatedError extends Error {
           `For more help, run: ${Logger.colors.cyan('npx run @nightwatch/mobile-helper ios')}\n`
         ];
       }
+    } else if (this.desiredCapabilities['safari:deviceUDID']) {
+      help = [
+        `Verify the UDID of the device set in Nightwatch configuration (look for ${Logger.colors.cyan('safari:deviceUDID')} capability) or pass the correct UDID using ${Logger.colors.cyan('--deviceId')} flag in the test command.`,
+        'Re-run the test command',
+        `If it doesn't work, try running: ${Logger.colors.cyan('npx run @nightwatch/mobile-helper ios')}`
+      ];
+
+      if (isRealIos(this.desiredCapabilities)) {
+        help = [
+          `Check device connection by running command: ${Logger.colors.cyan('system_profiler SPUSBDataType | sed -n \'/iPhone/,/Serial/p\' | grep \'Serial Number:\' | awk -F \': \' \'{print $2}')}`,
+          ...help
+        ];
+      }
     }
 
     return help;
diff --git a/package-lock.json b/package-lock.json
index ae3116a0..96940980 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,7 @@
         "open": "8.4.0",
         "ora": "5.4.1",
         "piscina": "^3.2.0",
-        "selenium-webdriver": "4.8.0",
+        "selenium-webdriver": "4.9.2",
         "semver": "7.3.5",
         "stacktrace-parser": "0.1.10",
         "strip-ansi": "6.0.1",
@@ -7038,13 +7038,13 @@
       "dev": true
     },
     "node_modules/selenium-webdriver": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.0.tgz",
-      "integrity": "sha512-s/HL8WNwy1ggHR244+tAhjhyKMJnZLt1HKJ6Gn7nQgVjB/ybDF+46Uui0qI2J7AjPNJzlUmTncdC/jg/kKkn0A==",
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.9.2.tgz",
+      "integrity": "sha512-0gDswAgVn6qbCYckZetQQvQK9tWW1r7LaumhiqY1/Wl/7JEWG0JANsTbZKnmGc3+cUU76zAi5/p0P8LUweXlig==",
       "dependencies": {
-        "jszip": "^3.10.0",
+        "jszip": "^3.10.1",
         "tmp": "^0.2.1",
-        "ws": ">=8.11.0"
+        "ws": ">=8.13.0"
       },
       "engines": {
         "node": ">= 14.20.0"
@@ -8342,15 +8342,15 @@
       }
     },
     "node_modules/ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
+      "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
       "engines": {
         "node": ">=10.0.0"
       },
       "peerDependencies": {
         "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
+        "utf-8-validate": ">=5.0.2"
       },
       "peerDependenciesMeta": {
         "bufferutil": {
@@ -13912,13 +13912,13 @@
       "dev": true
     },
     "selenium-webdriver": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.0.tgz",
-      "integrity": "sha512-s/HL8WNwy1ggHR244+tAhjhyKMJnZLt1HKJ6Gn7nQgVjB/ybDF+46Uui0qI2J7AjPNJzlUmTncdC/jg/kKkn0A==",
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.9.2.tgz",
+      "integrity": "sha512-0gDswAgVn6qbCYckZetQQvQK9tWW1r7LaumhiqY1/Wl/7JEWG0JANsTbZKnmGc3+cUU76zAi5/p0P8LUweXlig==",
       "requires": {
-        "jszip": "^3.10.0",
+        "jszip": "^3.10.1",
         "tmp": "^0.2.1",
-        "ws": ">=8.11.0"
+        "ws": ">=8.13.0"
       }
     },
     "semver": {
@@ -14876,9 +14876,9 @@
       }
     },
     "ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
+      "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
       "requires": {}
     },
     "xml": {
diff --git a/package.json b/package.json
index 3d8df596..9985d43d 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
     "open": "8.4.0",
     "ora": "5.4.1",
     "piscina": "^3.2.0",
-    "selenium-webdriver": "4.8.0",
+    "selenium-webdriver": "4.9.2",
     "semver": "7.3.5",
     "stacktrace-parser": "0.1.10",
     "strip-ansi": "6.0.1",


Test patch:
diff --git a/examples/tests/chromeCDP_example.js b/examples/tests/chromeCDP_example.js
index a62abf63..ea08a330 100644
--- a/examples/tests/chromeCDP_example.js
+++ b/examples/tests/chromeCDP_example.js
@@ -1,5 +1,7 @@
 describe('Chrome DevTools Example', function() {
 
+  this.disabled = this.argv.env !== 'chrome';
+  
   it ('using CDP DOM Snapshot', async function(browser) {
     await browser.navigateTo('https://nightwatchjs.org');
 
diff --git a/examples/tests/google.js b/examples/tests/google.js
index fbfed00c..1a665c13 100644
--- a/examples/tests/google.js
+++ b/examples/tests/google.js
@@ -27,9 +27,16 @@ describe('sample google search', function() {
         .pause(1000);
     }
 
+    let locator;
+    if (browser.isMobile()) {
+      locator = 'form[action="/search"] input[type=search]';
+    } else {
+      locator = 'form[action="/search"] input[type=text]';
+    }
+
     await browser
-      .waitForElementVisible('form[action="/search"] input[type=text]')
-      .sendKeys('form[action="/search"] input[type=text]', ['Nightwatch.js', browser.Keys.ENTER])
+      .waitForElementVisible(locator)
+      .sendKeys(locator, ['Nightwatch.js', browser.Keys.ENTER])
       .assert.textContains('#rso>:first-child', 'Nightwatch.js')
       .end();
   });
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 89eb0416..72ebd0bc 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -828,6 +828,7 @@ class TestSuite {
     let currentTestName = this.testcase.testName;
     this.suiteRetries.incrementTestRetriesCount(currentTestName);
     this.reporter.resetCurrentTestPassedCount();
+    this.reporter.testResults.retryTest = true;
     this.commandQueue.clearScheduled();
 
     return this.runCurrentTest(currentTestName);
diff --git a/test/apidemos/web-elements/elementApiWithPageObjects.js b/test/apidemos/web-elements/elementApiWithPageObjects.js
new file mode 100644
index 00000000..42b3e089
--- /dev/null
+++ b/test/apidemos/web-elements/elementApiWithPageObjects.js
@@ -0,0 +1,54 @@
+const assert = require('assert');
+const {WebElement} = require('selenium-webdriver');
+
+describe('demo tests with new element api in page objects', function () {
+  after(browser => browser.end());
+
+  const pageObject = this.page.simplePageObj();
+
+  it('element api on page objects', async function() {
+    const elementResult = await pageObject.element('@loginAsString');
+    assert.strictEqual(elementResult instanceof WebElement, true);
+    assert.strictEqual(await elementResult.getId(), '5cc459b8-36a8-3042-8b4a-258883ea642b');
+
+    const elementFindResult = await pageObject.element.find('@loginIndexed');
+    assert.strictEqual(elementFindResult instanceof WebElement, true);
+    assert.strictEqual(await elementFindResult.getId(), '3783b042-7001-0740-a2c0-afdaac732e9f');
+
+    const elementFindAllResult = await pageObject.element.findAll('@loginXpath');
+    assert.strictEqual(elementFindAllResult.length, 2);
+    assert.strictEqual(elementFindAllResult[0] instanceof WebElement, true);
+    assert.strictEqual(await elementFindAllResult[1].getId(), '3783b042-7001-0740-a2c0-afdaac732e9f');
+
+    const findByTextResult = await pageObject.element.findByText('Web Login');
+    assert.strictEqual(findByTextResult instanceof WebElement, true);
+    assert.strictEqual(await findByTextResult.getId(), '5cc459b8-36a8-3042-8b4a-258883ea642b');
+  });
+
+  it('element api on sections', async function() {
+    const signupSection = pageObject.section.signUp;
+
+    const elementResult = await signupSection.element('@help');
+    assert.strictEqual(elementResult instanceof WebElement, true);
+    assert.strictEqual(await elementResult.getId(), '1');
+
+    const elementFindResult = await signupSection.element.find('@help');
+    assert.strictEqual(elementFindResult instanceof WebElement, true);
+    assert.strictEqual(await elementFindResult.getId(), '1');
+
+    const findByTextResult = await signupSection.element.findByText('Help');
+    assert.strictEqual(findByTextResult instanceof WebElement, true);
+    assert.strictEqual(await findByTextResult.getId(), '2');
+
+    const getStartedSection = signupSection.section.getStarted;
+
+    const elementFindResult2 = await getStartedSection.element.find('#getStartedStart');
+    assert.strictEqual(elementFindResult2 instanceof WebElement, true);
+    assert.strictEqual(await elementFindResult2.getId(), '4');
+
+    const elementFindAllResult = await getStartedSection.element.findAll('@start');
+    assert.strictEqual(elementFindAllResult.length, 3);
+    assert.strictEqual(elementFindAllResult[0] instanceof WebElement, true);
+    assert.strictEqual(await elementFindAllResult[2].getId(), '6');
+  });
+});
diff --git a/test/apidemos/web-elements/waitUntilFailureTest.js b/test/apidemos/web-elements/waitUntilFailureTest.js
new file mode 100644
index 00000000..93005c5f
--- /dev/null
+++ b/test/apidemos/web-elements/waitUntilFailureTest.js
@@ -0,0 +1,5 @@
+describe('demo of failure of waitUntil element commands', function() {
+  it('waitUntil element is visible and but not selected', function({element}) {
+    element.find('#weblogin').waitUntil('visible').waitUntil('not.selected');
+  });
+});
\ No newline at end of file
diff --git a/test/apidemos/web-elements/waitUntilTest.js b/test/apidemos/web-elements/waitUntilTest.js
new file mode 100644
index 00000000..c56d4520
--- /dev/null
+++ b/test/apidemos/web-elements/waitUntilTest.js
@@ -0,0 +1,14 @@
+describe('demo tests using waitUntil element APIs', function() {
+  it('wait until element is visible', function({element}) {
+    element('#weblogin').waitUntil('visible');
+  });
+
+  it('wait until element is selected', function({element, state}) {
+    element('#weblogin').waitUntil('selected');
+  });
+
+  it('wait until element is enabled', function({element}) {
+    element('#weblogin').waitUntil('enabled');
+  });
+});
+
diff --git a/test/lib/command-mocks.js b/test/lib/command-mocks.js
index 63ffd65a..8c08da65 100644
--- a/test/lib/command-mocks.js
+++ b/test/lib/command-mocks.js
@@ -267,7 +267,27 @@ module.exports = {
         value
       })
     }, true);
-  }, 
+  },
+  
+  w3cSelected(elementId ='5cc459b8-36a8-3042-8b4a-258883ea642b', value = true) {
+    MockServer.addMock({
+      url: `/session/13521-10219-202/element/${elementId}/selected`,
+      method: 'GET',
+      response: JSON.stringify({
+        value
+      })
+    }, true);
+  },
+  
+  w3cEnabled(elementId ='5cc459b8-36a8-3042-8b4a-258883ea642b', value = true) {
+    MockServer.addMock({
+      url: `/session/13521-10219-202/element/${elementId}/enabled`,
+      method: 'GET',
+      response: JSON.stringify({
+        value
+      })
+    });
+  },
 
   findElements({using = 'css selector', value = '#container', response = null, times = 0}) {
     const mockOpts = {
diff --git a/test/sampletests/withverify/verifySampleFailures.js b/test/sampletests/withverify/verifySampleFailures.js
index 2164d7d6..9df9a259 100644
--- a/test/sampletests/withverify/verifySampleFailures.js
+++ b/test/sampletests/withverify/verifySampleFailures.js
@@ -1,17 +1,21 @@
 module.exports = {
   before(client) {
     client.globals.calls++;
+    client.url('http://localhost');
   },
 
   demoTest(client) {
-    client.url('http://localhost')
-      .verify.equal(0, 1, 'custom message')
-      .verify.elementPresent('#badElement')
+    client.verify.equal(0, 1, 'custom message')
       .verify.elementPresent('#weblogin')
-      .end();
+      .verify.elementPresent('#badElement');
+  },
+
+  demoTest1(client) {
+    client.verify.equal(0, 0, 'custom message');
   },
 
   after(client, callback) {
+    client.end();
     client.globals.calls++;
     callback();
   }
diff --git a/test/src/api/assertions/testHasAttribute.js b/test/src/api/assertions/testHasAttribute.js
index 390aa7ed..8037314b 100644
--- a/test/src/api/assertions/testHasAttribute.js
+++ b/test/src/api/assertions/testHasAttribute.js
@@ -13,7 +13,7 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test', 'Test message'],
       commandResult: {
-        value: [{name: 'data-track'}, {name: 'data-test'}]
+        value: 'data-track'
       },
       assertMessage: true,
       assertion({instance, failure}) {
@@ -28,7 +28,7 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test'],
       commandResult: {
-        value: [{name: 'data-track'}]
+        value: 'data-track'
       },
       negate: true,
       assertion({instance, queueOpts, message}) {
@@ -36,8 +36,8 @@ describe('assert.hasAttribute', function () {
         assert.strictEqual(queueOpts.negate, true);
 
         assert.strictEqual(instance.hasFailure(), false);
-        assert.deepStrictEqual(instance.getValue(), ['data-track']);
-        assert.deepStrictEqual(instance.getActual(), ['data-track']);
+        assert.deepStrictEqual(instance.getValue(), 'data-track');
+        assert.deepStrictEqual(instance.getActual(), 'data-track');
         assert.strictEqual(instance.message, 'Testing if element <.test_element> doesn\'t have attribute \'data-test\'');
         assert.ok(message.startsWith('Testing if element <.test_element> doesn\'t have attribute \'data-test\''), message);
       }
@@ -48,15 +48,15 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test'],
       commandResult: {
-        value: [{name: 'data-test'}]
+        value: 'data-test'
       },
       negate: true,
       assertError: true,
       assertion({instance, queueOpts, err}) {
         assert.strictEqual(queueOpts.negate, true);
         assert.strictEqual(instance.hasFailure(), false);
-        assert.deepStrictEqual(instance.getValue(), ['data-test']);
-        assert.deepStrictEqual(instance.getActual(), ['data-test']);
+        assert.deepStrictEqual(instance.getValue(), 'data-test');
+        assert.deepStrictEqual(instance.getActual(), 'data-test');
         assert.strictEqual(err.message, `Testing if element <.test_element> doesn't have attribute 'data-test' in 5ms - expected "has not data-test" but got: "data-test" (${instance.elapsedTime}ms)`);
       }
     });
@@ -66,13 +66,13 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: [{selector: '.test_element'}, 'data-test'],
       commandResult: {
-        value: [{name: 'data-test'}]
+        value: 'data-test'
       },
       assertResult: true,
       assertion({instance, failure, message, err}) {
         assert.strictEqual(err, undefined);
         assert.deepStrictEqual(instance.options, {elementSelector: true});
-        assert.deepStrictEqual(instance.getActual(), ['data-test']);
+        assert.deepStrictEqual(instance.getActual(), 'data-test');
         assert.strictEqual(instance.hasFailure(), false);
         assert.ok(message.startsWith('Testing if element <.test_element> has attribute \'data-test\''), message);
         assert.strictEqual(failure, false);
@@ -84,33 +84,13 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test', 'Test message'],
       commandResult: {
-        value: [{name: 'not_expected'}]
+        value: null
       },
       assertError: true,
       assertResult: true,
       assertion({instance, failure}) {
-        assert.deepStrictEqual(instance.getActual(), ['not_expected']);
-        assert.strictEqual(failure, 'Expected "has data-test" but got: "not_expected"');
-      }
-    });
-  });
-
-  it('hasAttribute assertion - element not found', function() {
-    return assertionTest({
-      args: ['.test_element', 'data-test', 'Test attribute %s from element "%s" == %s'],
-      commandResult: {
-        status: -1,
-        value: []
-      },
-      assertError: true,
-      assertFailure: true,
-      assertResult: true,
-      assertion({instance, failure, err}) {
-        assert.strictEqual(instance.getActual(), 'element could not be located');
-        assert.strictEqual(instance.expected(), 'has data-test');
-        assert.strictEqual(instance.getValue(), null);
-        assert.strictEqual(failure, 'Expected "has data-test" but got: "element could not be located"');
-        assert.strictEqual(err.message, `Test attribute <.test_element> from element "'data-test'" == %s in 5ms - expected "has data-test" but got: "element could not be located" (${instance.elapsedTime}ms)`);
+        assert.deepStrictEqual(instance.getActual(), null);
+        assert.strictEqual(failure, 'Expected "has data-test" but got: "null"');
       }
     });
   });
@@ -132,4 +112,17 @@ describe('assert.hasAttribute', function () {
       }
     });
   });
+
+  it('hasAttribute assertion failed for wrong parameters', function () {
+    return assertionTest({
+      args: ['.test_element', ['data-test', 'dummy'], 'Test message'],
+      commandResult: {
+        value: 'data-track'
+      },
+      assertMessage: true
+    }).catch((err) => {
+      assert.ok(err instanceof Error);
+      assert.strictEqual(err.message, 'Error while running "assert.hasAttribute" command: Expected attribute must be a string');
+    });
+  });
 });
diff --git a/test/src/api/commands/element/testSubmitForm.js b/test/src/api/commands/element/testSubmitForm.js
index 225f033f..dfe6e792 100644
--- a/test/src/api/commands/element/testSubmitForm.js
+++ b/test/src/api/commands/element/testSubmitForm.js
@@ -1,19 +1,19 @@
 const assert = require('assert');
-const MockServer  = require('../../../../lib/mockserver.js');
+const MockServer = require('../../../../lib/mockserver.js');
 const CommandGlobals = require('../../../../lib/globals/commands.js');
 
-describe('submitForm', function() {
-  before(function(done) {
+describe('submitForm', function () {
+  before(function (done) {
     CommandGlobals.beforeEach.call(this, done);
   });
 
-  after(function(done) {
+  after(function (done) {
     CommandGlobals.afterEach.call(this, done);
   });
 
-  it('client.submitForm()', function(done) {
+  it('client.submitForm()', function (done) {
     MockServer.addMock({
-      url: '/wd/hub/session/1352110219202/element/0/submit',
+      url: '/wd/hub/session/1352110219202/execute/sync',
       method: 'POST',
       response: JSON.stringify({
         sessionId: '1352110219202',
diff --git a/test/src/api/commands/web-element/testFindByLabelText.js b/test/src/api/commands/web-element/testFindByLabelText.js
index 8751a133..e9ec85cd 100644
--- a/test/src/api/commands/web-element/testFindByLabelText.js
+++ b/test/src/api/commands/web-element/testFindByLabelText.js
@@ -184,7 +184,7 @@ describe('.findByLabelText() commands', function () {
         },
         method: 'POST',
         response: JSON.stringify({
-          value: [{'element-6066-11e4-a52e-4f735466cecf': '2'}]
+          value: {'element-6066-11e4-a52e-4f735466cecf': '2'}
         })
       }, true);
 
@@ -234,7 +234,7 @@ describe('.findByLabelText() commands', function () {
         },
         method: 'POST',
         response: JSON.stringify({
-          value: [{'element-6066-11e4-a52e-4f735466cecf': '2'}]
+          value: {'element-6066-11e4-a52e-4f735466cecf': '2'}
         })
       }, true);
 
diff --git a/test/src/api/commands/web-element/testSendKeys.js b/test/src/api/commands/web-element/testSendKeys.js
index 6bff24be..b69b9937 100644
--- a/test/src/api/commands/web-element/testSendKeys.js
+++ b/test/src/api/commands/web-element/testSendKeys.js
@@ -78,7 +78,7 @@ describe('element().sendKeys() command', function () {
         })
       }, true);
 
-    const resultPromise = this.client.api.element('#signupSection').find('input[name=q]').sendKeys('nightwatch');
+    const resultPromise = this.client.api.element('#signupSection').find('input[name=q]').sendKeys('night', 'watch');
     // neither an instance of Element or Promise, but an instance of ScopedWebElement.
     assert.strictEqual(resultPromise instanceof Element, false);
     assert.strictEqual(typeof resultPromise.find, 'undefined');
@@ -116,7 +116,7 @@ describe('element().sendKeys() command', function () {
         })
       }, true);
 
-    const resultPromise = this.client.api.element.find('#signupSection').find('input[name=q]').sendKeys('nightwatch');
+    const resultPromise = this.client.api.element.find('#signupSection').find('input[name=q]').sendKeys(['night', 'watch']);
     // neither an instance of Element or Promise, but an instance of ScopedWebElement.
     assert.strictEqual(resultPromise instanceof Element, false);
     assert.strictEqual(typeof resultPromise.find, 'undefined');
diff --git a/test/src/api/commands/web-element/testSetValue.js b/test/src/api/commands/web-element/testSetValue.js
new file mode 100644
index 00000000..fa05494b
--- /dev/null
+++ b/test/src/api/commands/web-element/testSetValue.js
@@ -0,0 +1,153 @@
+const assert = require('assert');
+const {WebElement} = require('selenium-webdriver');
+const MockServer  = require('../../../../lib/mockserver.js');
+const CommandGlobals = require('../../../../lib/globals/commands-w3c.js');
+const Element = require('../../../../../lib/element/index.js');
+
+describe('element().setValue() command', function () {
+  before(function (done) {
+    CommandGlobals.beforeEach.call(this, done);
+  });
+
+  after(function (done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('test .element().setValue()', async function() {
+    MockServer
+      .addMock({
+        url: '/session/13521-10219-202/elements',
+        postdata: {
+          using: 'css selector',
+          value: 'input[name=q]'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [{'element-6066-11e4-a52e-4f735466cecf': '9'}]
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/clear',
+        method: 'POST',
+        postdata: {},
+        response: JSON.stringify({
+          value: null
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/value',
+        method: 'POST',
+        postdata: {
+          text: 'nightwatch',
+          value: [
+            'n', 'i', 'g', 'h', 't', 'w', 'a', 't', 'c', 'h'
+          ]
+        },
+        response: JSON.stringify({
+          value: null
+        })
+      }, true);
+
+    const resultPromise = this.client.api.element('input[name=q]').setValue('nightwatch');
+    // neither an instance of Element or Promise, but an instance of ScopedWebElement.
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+    assert.strictEqual(resultPromise instanceof Promise, false);
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, true);
+    assert.strictEqual(await result.getId(), '9');
+  });
+
+  it('test .element().find().setValue()', async function() {
+    MockServer
+      .addMock({
+        url: '/session/13521-10219-202/element/0/elements',
+        postdata: {
+          using: 'css selector',
+          value: 'input[name=q]'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [{'element-6066-11e4-a52e-4f735466cecf': '9'}]
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/clear',
+        method: 'POST',
+        postdata: {},
+        response: JSON.stringify({
+          value: null
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/value',
+        method: 'POST',
+        postdata: {
+          text: 'nightwatch',
+          value: [
+            'n', 'i', 'g', 'h', 't', 'w', 'a', 't', 'c', 'h'
+          ]
+        },
+        response: JSON.stringify({
+          value: null
+        })
+      }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').find('input[name=q]').setValue('night', 'watch');
+    // neither an instance of Element or Promise, but an instance of ScopedWebElement.
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+    assert.strictEqual(resultPromise instanceof Promise, false);
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, true);
+    assert.strictEqual(await result.getId(), '9');
+  });
+
+  it('test .element.find().setValue()', async function() {
+    MockServer
+      .addMock({
+        url: '/session/13521-10219-202/element/0/elements',
+        postdata: {
+          using: 'css selector',
+          value: 'input[name=q]'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [{'element-6066-11e4-a52e-4f735466cecf': '9'}]
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/clear',
+        method: 'POST',
+        postdata: {},
+        response: JSON.stringify({
+          value: null
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/9/value',
+        method: 'POST',
+        postdata: {
+          text: 'nightwatch',
+          value: [
+            'n', 'i', 'g', 'h', 't', 'w', 'a', 't', 'c', 'h'
+          ]
+        },
+        response: JSON.stringify({
+          value: null
+        })
+      }, true);
+
+    const resultPromise = this.client.api.element.find('#signupSection').find('input[name=q]').setValue(['night', 'watch']);
+    // neither an instance of Element or Promise, but an instance of ScopedWebElement.
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+    assert.strictEqual(resultPromise instanceof Promise, false);
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, true);
+    assert.strictEqual(await result.getId(), '9');
+  });
+});
diff --git a/test/src/api/commands/web-element/testUpdate.js b/test/src/api/commands/web-element/testUpdate.js
index e31a23d7..441084d7 100644
--- a/test/src/api/commands/web-element/testUpdate.js
+++ b/test/src/api/commands/web-element/testUpdate.js
@@ -94,7 +94,7 @@ describe('element().update() command', function () {
         })
       }, true);
 
-    const resultPromise = this.client.api.element('#signupSection').find('input[name=q]').update('nightwatch');
+    const resultPromise = this.client.api.element('#signupSection').find('input[name=q]').update('night', 'watch');
     // neither an instance of Element or Promise, but an instance of ScopedWebElement.
     assert.strictEqual(resultPromise instanceof Element, false);
     assert.strictEqual(typeof resultPromise.find, 'undefined');
@@ -140,7 +140,7 @@ describe('element().update() command', function () {
         })
       }, true);
 
-    const resultPromise = this.client.api.element.find('#signupSection').find('input[name=q]').update('nightwatch');
+    const resultPromise = this.client.api.element.find('#signupSection').find('input[name=q]').update(['night', 'watch']);
     // neither an instance of Element or Promise, but an instance of ScopedWebElement.
     assert.strictEqual(resultPromise instanceof Element, false);
     assert.strictEqual(typeof resultPromise.find, 'undefined');
diff --git a/test/src/api/protocol/testAlerts.js b/test/src/api/protocol/testAlerts.js
index 54d2d15b..b3662fb5 100644
--- a/test/src/api/protocol/testAlerts.js
+++ b/test/src/api/protocol/testAlerts.js
@@ -120,7 +120,7 @@ describe('alert commands', function () {
       assertion: function (value) {
         assert.strictEqual(value, text);
       },
-      commandName: 'setAlertText',
+      commandName: 'alerts.setText',
       args: [text]
     }).then((result) => {
       assert.strictEqual(result.value, null);
@@ -131,4 +131,25 @@ describe('alert commands', function () {
       done(err);
     });
   });
+
+  it('test alerts.setText() with error', function (done) {
+    const text = 2;
+
+    Globals.protocolTest({
+      assertion: function (value) {
+        assert.strictEqual(value, text);
+      },
+      commandName: 'alerts.setText',
+      args: [text]
+    }).catch(err => {
+      assert.strictEqual(
+        err.message,
+        'Error while running "alerts.setText" command: First argument passed to .alerts.setText() must be a string.'
+      );
+    }).catch(err => {
+      return err;
+    }).then(err => {
+      done(err);
+    });
+  });
 });
diff --git a/test/src/apidemos/web-elements/testElementApiWithPageObjects.js b/test/src/apidemos/web-elements/testElementApiWithPageObjects.js
new file mode 100644
index 00000000..6a9d7ef7
--- /dev/null
+++ b/test/src/apidemos/web-elements/testElementApiWithPageObjects.js
@@ -0,0 +1,109 @@
+const path = require('path');
+const MockServer = require('../../../lib/mockserver.js');
+const common = require('../../../common.js');
+const {settings} = common;
+const NightwatchClient = common.require('index.js');
+
+describe('new element api with page objects', function() {
+  beforeEach(function(done) {
+    this.server = MockServer.init();
+    this.server.on('listening', () => {
+      done();
+    });
+  });
+
+  afterEach(function(done) {
+    this.server.close(function() {
+      done();
+    });
+  });
+
+  it('test custom command', function() {
+    const testsPath = path.join(__dirname, '../../../apidemos/web-elements/elementApiWithPageObjects.js');
+
+    MockServer
+      .addMock({
+        url: '/session/13521-10219-202/elements',
+        postdata: {
+          using: 'xpath',
+          value: './/*[text()="Web Login"]'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [
+            {'element-6066-11e4-a52e-4f735466cecf': '5cc459b8-36a8-3042-8b4a-258883ea642b'},
+            {'element-6066-11e4-a52e-4f735466cecf': '3783b042-7001-0740-a2c0-afdaac732e9f'}
+          ]
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/0/elements',
+        postdata: {
+          using: 'xpath',
+          value: './/*[text()="Help"]'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [{'element-6066-11e4-a52e-4f735466cecf': '2'}]
+        })
+      }, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/0/elements',
+        postdata: {
+          using: 'css selector',
+          value: '#getStarted'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [
+            {'element-6066-11e4-a52e-4f735466cecf': '2'},
+            {'element-6066-11e4-a52e-4f735466cecf': '3'}
+          ]
+        })
+      }, true, true)
+      .addMock({
+        url: '/session/13521-10219-202/element/2/elements',
+        postdata: {
+          using: 'css selector',
+          value: '#getStartedStart'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          value: [
+            {'element-6066-11e4-a52e-4f735466cecf': '4'},
+            {'element-6066-11e4-a52e-4f735466cecf': '5'},
+            {'element-6066-11e4-a52e-4f735466cecf': '6'}
+          ]
+        })
+      }, true, true);
+
+    const globals = {
+      calls: 0,
+      waitForConditionPollInterval: 50,
+      waitForConditionTimeout: 120,
+      retryAssertionTimeout: 100,
+
+      reporter(results) {
+        if (results.lastError) {
+          throw results.lastError;
+        }
+      }
+    };
+
+    return NightwatchClient.runTests(testsPath, settings({
+      selenium: {
+        port: null,
+        start_process: false
+      },
+      selenium_host: null,
+      webdriver: {
+        port: 10195,
+        start_process: false
+      },
+      output: false,
+      skip_testcases_on_fail: false,
+      page_objects_path: [path.join(__dirname, '../../../extra/pageobjects/pages')],
+      globals
+    }));
+  });
+});
diff --git a/test/src/apidemos/web-elements/testWaitUntil.js b/test/src/apidemos/web-elements/testWaitUntil.js
new file mode 100644
index 00000000..f833d6b5
--- /dev/null
+++ b/test/src/apidemos/web-elements/testWaitUntil.js
@@ -0,0 +1,86 @@
+const path = require('path');
+const assert = require('assert');
+
+const commandMocks = require('../../../lib/command-mocks');
+const common = require('../../../common.js');
+const {settings} = common;
+const NightwatchClient = common.require('index.js');
+
+describe('waitUntil element command', function() {
+  beforeEach(function(done) {
+    commandMocks.start(done);
+  });
+
+  afterEach(function(done) {
+    commandMocks.stop(done);
+  });
+
+  it('test waitUntil element command - passed', function() {
+    const testsPath = path.join(__dirname, '../../../apidemos/web-elements/waitUntilTest.js');
+    commandMocks.w3cSelected();
+    commandMocks.w3cVisible();
+    commandMocks.w3cEnabled();
+  
+
+    const globals = {
+      waitForConditionPollInterval: 50,
+      waitForConditionTimeout: 120,
+      retryAssertionTimeout: 100,
+
+      reporter(results) {
+        if (results.lastError) {
+          throw results.lastError;
+        }
+      }
+    };
+
+    return NightwatchClient.runTests(testsPath, settings({
+      selenium: {
+        port: null,
+        start_process: false
+      },
+      selenium_host: null,
+      webdriver: {
+        port: 10195,
+        start_process: false
+      },
+      output: false,
+      skip_testcases_on_fail: false,
+      globals
+    }));
+  });
+
+  it('test waitUntil element command - failed', function() {
+    const testsPath = path.join(__dirname, '../../../apidemos/web-elements/waitUntilFailureTest.js');
+    commandMocks.w3cSelected();
+    commandMocks.w3cVisible();
+  
+
+    const globals = {
+      waitForConditionPollInterval: 50,
+      waitForConditionTimeout: 120,
+      retryAssertionTimeout: 100,
+
+      reporter(results) {
+        if (!results.lastError) {
+          assert.fail('Should result into failure');
+        }
+      }
+    };
+
+    return NightwatchClient.runTests(testsPath, settings({
+      selenium: {
+        port: null,
+        start_process: false
+      },
+      selenium_host: null,
+      webdriver: {
+        port: 10195,
+        start_process: false
+      },
+      output: false,
+      skip_testcases_on_fail: false,
+      globals
+    }));
+  });
+});
diff --git a/test/src/cli/testCliRunner.js b/test/src/cli/testCliRunner.js
index 72b3ce41..eb3ae274 100644
--- a/test/src/cli/testCliRunner.js
+++ b/test/src/cli/testCliRunner.js
@@ -1158,8 +1158,8 @@ describe('Test CLI Runner', function() {
         },
 
         'android.chrome': {
-          real_mobile: false,
           desiredCapabilities: {
+            real_mobile: false,
             avd: 'nightwatch-android-11',
             browserName: 'chrome',
             'goog:chromeOptions': {
@@ -1178,7 +1178,7 @@ describe('Test CLI Runner', function() {
     }).setup();
 
     assert.strictEqual(runner.argv.env, 'android.chrome');
-    assert.strictEqual(runner.test_settings.real_mobile, false);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, false);
     assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
     assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'chrome');
     assert.ok('goog:chromeOptions' in runner.test_settings.desiredCapabilities);
@@ -1194,8 +1194,8 @@ describe('Test CLI Runner', function() {
         },
 
         'android.firefox': {
-          real_mobile: false,
           desiredCapabilities: {
+            real_mobile: false,
             avd: 'nightwatch-android-11',
             browserName: 'firefox',
             'moz:firefoxOptions': {
@@ -1214,7 +1214,8 @@ describe('Test CLI Runner', function() {
     }).setup();
 
     assert.strictEqual(runner.argv.env, 'android.firefox');
-    assert.strictEqual(runner.test_settings.real_mobile, false);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, false);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
     assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'firefox');
     assert.ok('moz:firefoxOptions' in runner.test_settings.desiredCapabilities);
     assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidPackage, 'org.mozilla.firefox');
@@ -1229,8 +1230,8 @@ describe('Test CLI Runner', function() {
         },
 
         'android.chrome': {
-          real_mobile: true,
           desiredCapabilities: {
+            real_mobile: true,
             avd: 'nightwatch-android-11',
             browserName: 'chrome',
             'goog:chromeOptions': {
@@ -1250,7 +1251,7 @@ describe('Test CLI Runner', function() {
     }).setup();
 
     assert.strictEqual(runner.argv.env, 'android.chrome');
-    assert.strictEqual(runner.test_settings.real_mobile, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, true);
     assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
     assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'chrome');
     assert.ok('goog:chromeOptions' in runner.test_settings.desiredCapabilities);
@@ -1267,8 +1268,8 @@ describe('Test CLI Runner', function() {
         },
 
         'android.firefox': {
-          real_mobile: true,
           desiredCapabilities: {
+            real_mobile: true,
             avd: 'nightwatch-android-11',
             browserName: 'firefox',
             'moz:firefoxOptions': {
@@ -1288,7 +1289,84 @@ describe('Test CLI Runner', function() {
     }).setup();
 
     assert.strictEqual(runner.argv.env, 'android.firefox');
-    assert.strictEqual(runner.test_settings.real_mobile, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
+    assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'firefox');
+    assert.ok('moz:firefoxOptions' in runner.test_settings.desiredCapabilities);
+    assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidPackage, 'org.mozilla.firefox');
+    assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidDeviceSerial, 'ZD2222W62Y');
+  });
+
+  it('android config setup on real device - for chrome - deviceId passed in args', function() {
+    mockery.registerMock('./android_config.json', {
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false
+        },
+
+        'android.chrome': {
+          desiredCapabilities: {
+            real_mobile: true,
+            avd: 'nightwatch-android-11',
+            browserName: 'chrome',
+            'goog:chromeOptions': {
+              androidPackage: 'com.android.chrome'
+            }
+          }
+        }
+      }
+    });
+
+    const CliRunner = common.require('runner/cli/cli.js');
+
+    const runner = new CliRunner({
+      config: './android_config.json',
+      env: 'android.chrome',
+      deviceId: 'ZD2222W62Y'
+    }).setup();
+
+    assert.strictEqual(runner.argv.env, 'android.chrome');
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
+    assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'chrome');
+    assert.ok('goog:chromeOptions' in runner.test_settings.desiredCapabilities);
+    assert.strictEqual(runner.test_settings.desiredCapabilities['goog:chromeOptions'].androidPackage, 'com.android.chrome');
+    assert.strictEqual(runner.test_settings.desiredCapabilities['goog:chromeOptions'].androidDeviceSerial, 'ZD2222W62Y');
+  });
+
+  it('android config setup on real device - for firefox - deviceId passed in args', function() {
+    mockery.registerMock('./android_config.json', {
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false
+        },
+
+        'android.firefox': {
+          desiredCapabilities: {
+            real_mobile: true,
+            avd: 'nightwatch-android-11',
+            browserName: 'firefox',
+            'moz:firefoxOptions': {
+              androidPackage: 'org.mozilla.firefox'
+            }
+          }
+        }
+      }
+    });
+
+    const CliRunner = common.require('runner/cli/cli.js');
+
+    const runner = new CliRunner({
+      config: './android_config.json',
+      env: 'android.firefox',
+      deviceId: 'ZD2222W62Y'
+    }).setup();
+
+    assert.strictEqual(runner.argv.env, 'android.firefox');
+    assert.strictEqual(runner.test_settings.desiredCapabilities.real_mobile, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.avd, 'nightwatch-android-11');
     assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'firefox');
     assert.ok('moz:firefoxOptions' in runner.test_settings.desiredCapabilities);
     assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidPackage, 'org.mozilla.firefox');
@@ -1349,4 +1427,64 @@ describe('Test CLI Runner', function() {
   });
 
 
+  it('Nightwatch Inspector - By default Chrome in debug mode run serially', function() {
+    mockery.registerMock('./nightwatch_inspector.json', {
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false
+        },
+
+        chrome: {
+          desiredCapabilities: {
+            browserName: 'chrome'
+          }
+        }
+      }
+    });
+
+    const CliRunner = common.require('runner/cli/cli.js');
+
+    const runner = new CliRunner({
+      config: './nightwatch_inspector.json',
+      env: 'chrome',
+      debug: true
+    }).setup();
+
+    assert.strictEqual(runner.argv.env, 'chrome');
+    assert.strictEqual(runner.test_settings.parallel_mode, false);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'chrome');
+  });
+
+  it('Nightwatch Inspector - parallel argument enables running Chrome in debug mode parallelly', function() {
+    mockery.registerMock('./nightwatch_inspector.json', {
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false
+        },
+
+        chrome: {
+          desiredCapabilities: {
+            browserName: 'chrome'
+          }
+        }
+      }
+    });
+
+    const CliRunner = common.require('runner/cli/cli.js');
+
+    const runner = new CliRunner({
+      config: './nightwatch_inspector.json',
+      env: 'chrome',
+      debug: true,
+      parallel: true
+    }).setup();
+
+    assert.strictEqual(runner.argv.env, 'chrome');
+    assert.strictEqual(runner.argv.serial, undefined);
+    assert.strictEqual(runner.argv.parallel, true);
+    assert.strictEqual(runner.test_settings.desiredCapabilities.browserName, 'chrome');
+  });
+
 });
diff --git a/test/src/core/testPageObjectWaitForElementNotPresent.js b/test/src/core/testPageObjectWaitForElementNotPresent.js
index f13aa823..4e7628a3 100644
--- a/test/src/core/testPageObjectWaitForElementNotPresent.js
+++ b/test/src/core/testPageObjectWaitForElementNotPresent.js
@@ -18,16 +18,17 @@ xdescribe('test PageObject WaitForElementNotPresent', function () {
     Nocks.deleteSession().disable();
   });
 
-  it('WaitForElementNotPresent with section', function(done) {
+  it('WaitForElementNotPresent with section', async function() {
     Nocks.elementFound().click().elementFound().childElementsFound('#badElement').elementFound().childElementsNotFound()
 
     const page = this.client.api.page.waitForElementNotPresentPageObj();
 
+    let res;
     page.waitForElementNotPresentDemo(function(result) {
-      assert.equal(result.status, 0);
-      done();
+      res = result;
     })
-
-    this.client.start()
+  
+    await this.client.start();
+    assert.equal(res.status, 0);
   });
 });
diff --git a/test/src/index/transport/testMobileWebSupport.js b/test/src/index/transport/testMobileWebSupport.js
new file mode 100644
index 00000000..904fdf5a
--- /dev/null
+++ b/test/src/index/transport/testMobileWebSupport.js
@@ -0,0 +1,241 @@
+const assert = require('assert');
+const common = require('../../../common.js');
+const path = require('path');
+const CommandGlobals = require('../../../lib/globals/commands.js');
+const MockServer = require('../../../lib/mockserver.js');
+const {settings} = common;
+const {runTests} = common.require('index.js');
+const Transport = common.require('transport/selenium-webdriver/index.js');
+const {IosSessionNotCreatedError, AndroidConnectionError} = common.require('utils/mobile.js');
+const mockery = require('mockery');
+
+describe('MobileSupport', function () {
+  before(function () {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+  })
+
+  beforeEach(function (done) {
+    this.getDriverBackup = Transport.prototype.getDriver;
+    this.createDriverServiceBackup = Transport.prototype.createDriverService;
+    process.removeAllListeners('exit');
+    process.removeAllListeners('uncaughtException');
+    process.removeAllListeners('unhandledRejection');
+
+    this.server = MockServer.init();
+
+    this.server.on('listening', () => {
+      done()
+    });
+  });
+
+  afterEach(function (done) {
+    mockery.deregisterAll();
+    mockery.resetCache();
+    mockery.disable();
+    Transport.prototype.getDriver = this.getDriverBackup;
+    Transport.prototype.createDriverService = this.createDriverServiceBackup;
+
+    CommandGlobals.afterEach.call(this, function () {
+      done();
+    });
+  });
+
+  it('error classes for mobile-web support - RealIosDeviceIdError', function () {
+    let src_folders = [
+      path.join(__dirname, '../../../sampletests/withsubfolders')
+    ];
+
+    return runTests({}, settings({
+      output: false,
+      src_folders,
+      desiredCapabilities: {
+        browserName: 'safari',
+        platformName: 'iOS',
+        alwaysMatch: {
+          acceptInsecureCerts: false
+        }
+      },
+
+      webdriver: {
+        start_process: true,
+        server_path: '',
+        cli_args: [
+          // --verbose
+        ]
+      }
+    })).catch(err => {
+      assert.ok(err instanceof Error);
+      assert.strictEqual(err.name, 'RealIosDeviceIdError');
+      assert.strictEqual(err.message, 'Real Device ID is neither configured nor passed');
+      assert.strictEqual(err.help.length, 4)
+    })
+  });
+
+  it('error classes for mobile-web support - IosSessionNotCreatedError', function () {
+    Transport.prototype.getDriver = function() {
+      const err = new Error('An error occurred while creating a new SafariDriver session');
+      err.name = 'SessionNotCreatedError';
+
+      throw err;
+    };
+
+    Transport.prototype.createDriverService = async function() { 
+      this.driverService = {
+        getOutputFilePath(){},
+        getSettingsFormatted(){}
+      };
+    };
+
+    let src_folders = [
+      path.join(__dirname, '../../../sampletests/withfailures'),
+      path.join(__dirname, '../../../sampletests/withsubfolders')
+    ];
+
+    let globals = {
+      calls: 0,
+      retryAssertionTimeout: 0,
+      reporter(results, cb) {
+        assert.ok(results.lastError instanceof Error);
+        assert.ok(results.lastError instanceof IosSessionNotCreatedError);
+        assert.ok(Object.prototype.hasOwnProperty.call(results.lastError, 'name'));
+        assert.ok(Object.prototype.hasOwnProperty.call(results.lastError, 'message'));
+        assert.ok(results.lastError.help.length, 3);
+        cb();
+      }
+    };
+
+    return runTests({
+    }, settings({
+      output: false,
+      src_folders,
+      globals,
+      desiredCapabilities: {
+        browserName: 'safari',
+        platformName: 'iOS',
+        'safari:deviceUDID': '00008030-00024C2C3453402E',
+        alwaysMatch: {
+          acceptInsecureCerts: false
+        }
+      },
+    
+      webdriver: {
+        start_process: true,
+        server_path: '',
+        cli_args: [
+          // --verbose
+        ]
+      }
+    }))
+  });
+
+  it('deviceId passed as argument - real device', async function () {
+    const CliRunner = common.require('runner/cli/cli.js');
+    
+    const runner = new CliRunner({
+      reporter: 'junit',
+      env: 'mixed',
+      deviceId: '00008030-00024C2C3453402E'
+    });
+
+    const globals = {
+      calls: 0,
+      retryAssertionTimeout: 0
+    };
+
+    await runner.setupAsync(settings({
+      output: false,
+      globals,
+      desiredCapabilities: {
+        browserName: 'safari',
+        platformName: 'iOS',
+        alwaysMatch: {
+          acceptInsecureCerts: false
+        }
+      },
+
+      webdriver: {
+        start_process: true,
+        server_path: '',
+        cli_args: [
+          // --verbose
+        ]
+      }
+    }));
+
+    assert.strictEqual(runner.test_settings.desiredCapabilities['safari:deviceUDID'], '00008030-00024C2C3453402E');
+    assert.strictEqual(runner.test_settings.desiredCapabilities.platformName, 'iOS');
+    assert.ok(!runner.test_settings.desiredCapabilities['safari:useSimulator']);
+  });
+
+  it('error classes for mobile-web support - AndroidConnectionError', function () {
+    this.timeout(10000);
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+
+    mockery.registerMock('../androidEmulator.js', class AndroidServer {
+      constructor() {}
+
+      launchEmulator() {
+        return Promise.resolve();
+      }
+
+      killEmulator() {}
+    });
+
+    Transport.prototype.getDriver = function() {
+      const err = new Error('unknown error: There are no devices online');
+      err.name = 'SessionNotCreatedError';
+
+      throw err;
+    };
+
+    Transport.prototype.createDriverService = async function() { 
+      this.driverService = {
+        getOutputFilePath(){},
+        getSettingsFormatted(){}
+      };
+    };
+
+    mockery.registerMock('./', Transport);
+    mockery.registerMock('@nightwatch/mobile-helper', {
+      getBinaryLocation(){}, getPlatformName(){}
+    });
+
+    let src_folders = [
+      path.join(__dirname, '../../../sampletests/withsubfolders/simple')
+    ];
+
+    let globals = {
+      calls: 0,
+      retryAssertionTimeout: 0,
+      reporter(results, cb) {
+        assert.ok(results.lastError instanceof Error);
+        assert.ok(results.lastError instanceof AndroidConnectionError);
+        assert.ok(results.lastError.message.includes('no devices online'));
+        assert.ok(Object.prototype.hasOwnProperty.call(results.lastError, 'message'));
+        assert.ok(results.lastError.help.length, 4);
+        cb();
+      }
+    };
+
+    return runTests({}, settings({
+      output: false,
+      src_folders,
+      globals,
+      desiredCapabilities: {
+        real_mobile: false,
+        browserName: 'chrome',
+        avd: 'dummy',
+        'goog:chromeOptions': {
+          androidPackage: 'com.android.chrome'
+        }
+      },
+      webdriver: {
+        start_process: true,
+        server_path: '',
+        cli_args: [
+          // --verbose
+        ]
+      }
+    }))
+  });
+});
diff --git a/test/src/runner/testReporter.js b/test/src/runner/testReporter.js
index 4f5a15ea..1a69b234 100644
--- a/test/src/runner/testReporter.js
+++ b/test/src/runner/testReporter.js
@@ -394,4 +394,30 @@ describe('testReporter', function() {
       assert.deepStrictEqual(result, ['nightwatch_reporter_output', 'html_reporter_output']);
     });
   });
+
+  it('test to check retry data logging', function() {
+    this.timeout(100000);
+
+    const testsPath = path.join(__dirname, '../../sampletests/withfailures');
+    const globals = {
+      calls: 0,
+      reporter(results, cb) {
+        assert.ok('sample' in results.modules);
+        assert.ok('completedSections' in results.modules['sample']);
+        assert.ok('demoTest' in results.modules['sample']['completedSections']);
+        assert.ok('retryTestData' in results.modules['sample']['completedSections']['demoTest']);
+        assert.ok(results.modules['sample']['completedSections']['demoTest']['retryTestData'].length <= 3);
+        cb();
+      },
+      retryAssertionTimeout: 0
+    };
+
+    return runTests({
+      retries: 3,
+      _source: [testsPath]
+    }, settings({
+      skip_testcases_on_fail: false,
+      globals
+    }));
+  });
 });
diff --git a/test/src/runner/testRunWithVerify.js b/test/src/runner/testRunWithVerify.js
index e65d5275..a21872da 100644
--- a/test/src/runner/testRunWithVerify.js
+++ b/test/src/runner/testRunWithVerify.js
@@ -37,9 +37,10 @@ describe('testRunWithVerify', function() {
         retryAssertionTimeout: 50,
         reporter(results, cb) {
           assert.ok('verifySampleFailures' in results.modules);
-          assert.strictEqual(results.passed, 1);
+          assert.strictEqual(results.passed, 2);
           assert.strictEqual(results.failed, 2);
-          assert.strictEqual(results.assertions, 3);
+          assert.strictEqual(results.assertions, 4);
+          assert.strictEqual(results.skipped, 0);
 
           cb();
         }
diff --git a/test/src/runner/testRunnerSessionCreate.js b/test/src/runner/testRunnerSessionCreate.js
index 1096dd43..735874fe 100644
--- a/test/src/runner/testRunnerSessionCreate.js
+++ b/test/src/runner/testRunnerSessionCreate.js
@@ -6,8 +6,8 @@ const CommandGlobals = require('../../lib/globals/commands.js');
 const {settings} = common;
 const {runTests} = common.require('index.js');
 
-describe('testRunnerSessionCreate', function() {
-  before(function(done) {
+describe('testRunnerSessionCreate', function () {
+  before(function (done) {
     this.server = MockServer.init();
 
     this.server.on('listening', () => {
@@ -15,17 +15,17 @@ describe('testRunnerSessionCreate', function() {
     });
   });
 
-  after(function(done) {
+  after(function (done) {
     CommandGlobals.afterEach.call(this, done);
   });
 
-  beforeEach(function() {
+  beforeEach(function () {
     process.removeAllListeners('exit');
     process.removeAllListeners('uncaughtException');
     process.removeAllListeners('unhandledRejection');
   });
 
-  it('testRunner with session create error using webdriver', function() {
+  it('testRunner with session create error using webdriver', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple'),
       path.join(__dirname, '../../sampletests/async')
@@ -49,7 +49,7 @@ describe('testRunnerSessionCreate', function() {
       url: '/session',
       statusCode: 500,
       postdata: JSON.stringify({
-        capabilities: {firstMatch: [{}], alwaysMatch: {browserName: 'firefox',  'nightwatch:options': {name: 'test-Name'}}}
+        capabilities: {firstMatch: [{}], alwaysMatch: {browserName: 'firefox', 'nightwatch:options': {name: 'test-Name'}}}
       }),
       response: JSON.stringify({
         value: {
@@ -88,7 +88,7 @@ describe('testRunnerSessionCreate', function() {
     }));
   });
 
-  it('testRunner with session create error using webdriver with --fail-fast argv', function() {
+  it('testRunner with session create error using webdriver with --fail-fast argv', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple'),
       path.join(__dirname, '../../sampletests/async')
@@ -154,7 +154,7 @@ describe('testRunnerSessionCreate', function() {
 
   });
 
-  it('testRunner with session create error using webdriver with enable_fail_fast setting', function() {
+  it('testRunner with session create error using webdriver with enable_fail_fast setting', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple'),
       path.join(__dirname, '../../sampletests/async')
@@ -220,7 +220,7 @@ describe('testRunnerSessionCreate', function() {
 
   });
 
-  it('testRunner with session ECONNREFUSED error using webdriver', function() {
+  it('testRunner with session ECONNREFUSED error using webdriver', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple'),
       path.join(__dirname, '../../sampletests/async')
@@ -259,7 +259,7 @@ describe('testRunnerSessionCreate', function() {
     });
   });
 
-  it('testRunner with not found server_path error', function() {
+  it('testRunner with not found server_path error', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple')
     ];
@@ -273,8 +273,8 @@ describe('testRunnerSessionCreate', function() {
         assert.strictEqual(results.lastError.sessionCreate, true);
         assert.strictEqual(results.lastError.showTrace, false);
         assert.ok(results.lastError instanceof Error);
-        assert.ok(results.lastError.detailedErr.includes('verify if webdriver is configured correctly; using:'));
-        assert.strictEqual(results.lastError.message, 'Unable to create the GeckoDriver process: The specified executable path does not exist: /bin/xxxxx');
+        assert.ok(results.lastError.detailedErr.includes(' Verify if GeckoDriver is configured correctly; using:'));
+        assert.strictEqual(results.lastError.message, 'An error occurred while creating a new GeckoDriver session: [Error] spawn /bin/xxxxx ENOENT');
       }
     };
 
@@ -298,7 +298,7 @@ describe('testRunnerSessionCreate', function() {
     });
   });
 
-  it('testRunner with incorrect server_path error', function() {
+  it('testRunner with incorrect server_path error', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/simple')
     ];
@@ -337,7 +337,7 @@ describe('testRunnerSessionCreate', function() {
     });
   });
 
-  it('test runner with reusing browser sessions', function() {
+  it('test runner with reusing browser sessions', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/reusebrowser')];
 
@@ -364,7 +364,7 @@ describe('testRunnerSessionCreate', function() {
     }, true);
 
 
-   
+
 
     const globals = {
       reporter(results) {
@@ -387,10 +387,10 @@ describe('testRunnerSessionCreate', function() {
       globals,
       output: false,
       output_folder: false
-    })); 
+    }));
   });
 
-  it('test runner with reusing browser sessions - using globals', function() {
+  it('test runner with reusing browser sessions - using globals', function () {
     const testsPath = [
       path.join(__dirname, '../../sampletests/reusebrowser')];
 
@@ -417,7 +417,7 @@ describe('testRunnerSessionCreate', function() {
     }, true);
 
 
-   
+
 
     const globals = {
       reuseBrowserSession: true,
@@ -440,6 +440,6 @@ describe('testRunnerSessionCreate', function() {
       globals,
       output: false,
       output_folder: false
-    })); 
+    }));
   });
 });