Repository: nightwatchjs/nightwatch
Pull request #3486 (2022-11-17 08:20:47)

Make appium context api more intuitive and update jsdoc.
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.

- [ ] 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;
- [ ] 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/client-commands/setGeolocation.js b/lib/api/client-commands/setGeolocation.js
index 1db83a4e..4b7dfab9 100644
--- a/lib/api/client-commands/setGeolocation.js
+++ b/lib/api/client-commands/setGeolocation.js
@@ -35,7 +35,6 @@ class SetGeolocation extends ClientCommand {
   }
 
   performAction(callback) {
-
     if (!this.api.isChrome()  && !this.api.isEdge()) {
       const error =  new Error('The command .setGeolocation() is only supported in Chrome and Edge drivers');
       Logger.error(error);
diff --git a/lib/api/protocol/appium/getContext.js b/lib/api/protocol/appium/getContext.js
new file mode 100644
index 00000000..2cf19362
--- /dev/null
+++ b/lib/api/protocol/appium/getContext.js
@@ -0,0 +1,36 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get the current context in which Appium is running. Used when testing hybrid mobile apps using Appium.
+ *
+ * More info here: https://appium.io/docs/en/commands/context/get-context/
+ *
+ * @example
+ * module.exports = {
+ *   'get current context': function (app) {
+ *     app
+ *       .appium.getContext(function (result) {
+ *         console.log('the current context is:', result.value);
+ *       });
+ *   },
+ *
+ *   'get current context with ES6 async/await': async function (app) {
+ *     const context = await app.appium.getContext();
+ *     console.log('the current context is:', context);
+ *   }
+ * };
+ *
+ * @syntax .appium.getContext([callback])
+ * @method getContext
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {string|null} A string representing the current context, or `null` representing "no context".
+ * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html
+ * @see appium.setContext
+ * @see appium.getContexts
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getCurrentContext(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/getContexts.js b/lib/api/protocol/appium/getContexts.js
new file mode 100644
index 00000000..29dbba32
--- /dev/null
+++ b/lib/api/protocol/appium/getContexts.js
@@ -0,0 +1,36 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get a list of the available contexts. Used when testing hybrid mobile apps using Appium.
+ *
+ * More info here: https://appium.io/docs/en/commands/context/get-contexts/
+ *
+ * @example
+ * module.exports = {
+ *   'get available contexts': function (app) {
+ *     app
+ *       .appium.getContexts(function (result) {
+ *         console.log('the available contexts are:', result.value);
+ *       });
+ *   },
+ *
+ *   'get available contexts with ES6 async/await': async function (app) {
+ *     const contexts = await app.appium.getContexts();
+ *     console.log('the available contexts are:', contexts);
+ *   }
+ * };
+ *
+ * @syntax .appium.getContexts([callback])
+ * @method getContexts
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {Array<string>} An array of strings representing available contexts, e.g 'WEBVIEW_<id>', or 'NATIVE_APP'
+ * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html
+ * @see appium.getContext
+ * @see appium.setContext
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getAvailableContexts(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/getCurrentActivity.js b/lib/api/protocol/appium/getCurrentActivity.js
new file mode 100644
index 00000000..48fb296b
--- /dev/null
+++ b/lib/api/protocol/appium/getCurrentActivity.js
@@ -0,0 +1,33 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get the name of the current Android activity.
+ *
+ * @example
+ * module.exports = {
+ *   'get current activity name': function (app) {
+ *     app
+ *       .appium.getCurrentActivity(function (result) {
+ *         console.log('current android activity is:', result.value);
+ *       });
+ *   },
+ *
+ *   'get current activity name with ES6 async/await': async function (app) {
+ *     const activity = await app.appium.getCurrentActivity();
+ *     console.log('current android activity is:', activity);
+ *   }
+ * };
+ *
+ * @syntax .appium.getCurrentActivity([callback])
+ * @method getCurrentActivity
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {string} Name of the current activity.
+ * @see appium.getCurrentPackage
+ * @see appium.startActivity
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getCurrentActivity(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/getCurrentPackage.js b/lib/api/protocol/appium/getCurrentPackage.js
new file mode 100644
index 00000000..70d578c1
--- /dev/null
+++ b/lib/api/protocol/appium/getCurrentPackage.js
@@ -0,0 +1,33 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get the name of the current Android package.
+ *
+ * @example
+ * module.exports = {
+ *   'get current package name': function (app) {
+ *     app
+ *       .appium.getCurrentPackage(function (result) {
+ *         console.log('current android package is:', result.value);
+ *       });
+ *   },
+ *
+ *   'get current package name with ES6 async/await': async function (app) {
+ *     const packageName = await app.appium.getCurrentPackage();
+ *     console.log('current android package is:', packageName);
+ *   }
+ * };
+ *
+ * @syntax .appium.getCurrentPackage([callback])
+ * @method getCurrentPackage
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {string} Name of the current package.
+ * @see appium.getCurrentActivity
+ * @see appium.startActivity
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getCurrentPackage(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/getGeolocation.js b/lib/api/protocol/appium/getGeolocation.js
new file mode 100644
index 00000000..171a9a08
--- /dev/null
+++ b/lib/api/protocol/appium/getGeolocation.js
@@ -0,0 +1,32 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get the current geolocation of the mobile device.
+ *
+ * @example
+ * module.exports = {
+ *   'get device geolocation': function (app) {
+ *     app
+ *       .appium.getGeolocation(function (result) {
+ *         console.log('current device geolocation is:', result.value);
+ *       });
+ *   },
+ *
+ *   'get device geolocation with ES6 async/await': async function (app) {
+ *     const location = await app.appium.getGeolocation();
+ *     console.log('current device geolocation is:', location);
+ *   }
+ * };
+ *
+ * @syntax .appium.getGeolocation([callback])
+ * @method getGeolocation
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {object} The current geolocation: `{latitude: number, longitude: number, altitude: number}`.
+ * @see appium.setGeolocation
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getDeviceGeolocation(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/getOrientation.js b/lib/api/protocol/appium/getOrientation.js
new file mode 100644
index 00000000..1cd986a8
--- /dev/null
+++ b/lib/api/protocol/appium/getOrientation.js
@@ -0,0 +1,32 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Get the current device orientation.
+ *
+ * @example
+ * module.exports = {
+ *   'get current device orientation': function (app) {
+ *     app
+ *       .appium.getOrientation(function (result) {
+ *         console.log('current device orientation is:', result.value);
+ *       });
+ *   },
+ *
+ *   'get current device orientation with ES6 async/await': async function (app) {
+ *     const orientation = await app.appium.getOrientation();
+ *     console.log('current device orientation is:', orientation);
+ *   }
+ * };
+ *
+ * @syntax .appium.getOrientation([callback])
+ * @method getOrientation
+ * @param {function} callback Callback function which is called with the result value.
+ * @returns {string} The current device orientation: `LANDSCAPE` or `PORTRAIT`.
+ * @see appium.setOrientation
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.getScreenOrientation(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/hideKeyboard.js b/lib/api/protocol/appium/hideKeyboard.js
new file mode 100644
index 00000000..25233ec1
--- /dev/null
+++ b/lib/api/protocol/appium/hideKeyboard.js
@@ -0,0 +1,32 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Hide soft keyboard.
+ *
+ * @example
+ * module.exports = {
+ *   'hide device soft keyboard': function (app) {
+ *     app
+ *       .appium.hideKeyboard();
+ *   },
+ *
+ *   'hide device soft keyboard with ES6 async/await': async function (app) {
+ *     await app.appium.hideKeyboard();
+ *   }
+ * };
+ *
+ * @syntax .appium.hideKeyboard([callback])
+ * @method hideKeyboard
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.isKeyboardShown
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(callback) {
+    return this.transportActions.hideDeviceKeyboard({}, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/isKeyboardShown.js b/lib/api/protocol/appium/isKeyboardShown.js
new file mode 100644
index 00000000..b4b03d97
--- /dev/null
+++ b/lib/api/protocol/appium/isKeyboardShown.js
@@ -0,0 +1,32 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Whether or not the soft keyboard is shown.
+ *
+ * @example
+ * module.exports = {
+ *   'whether keyboard is shown': function (app) {
+ *     app
+ *       .appium.isKeyboardShown(function (result) {
+ *         console.log('result value of whether keyboard is shown:', result.value);
+ *       });
+ *   },
+ *
+ *   'whether keyboard is shown with ES6 async/await': async function (app) {
+ *     const result = await app.appium.isKeyboardShown();
+ *     console.log('result value of whether keyboard is shown:', result);
+ *   }
+ * };
+ *
+ * @syntax .appium.isKeyboardShown([callback])
+ * @method isKeyboardShown
+ * @param {function} [callback] Callback function which is called with the result value.
+ * @returns {boolean} True if the keyboard is shown.
+ * @see appium.hideKeyboard
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  command(callback) {
+    return this.transportActions.isDeviceKeyboardShown(callback);
+  }
+};
diff --git a/lib/api/protocol/appium/longPressKeyCode.js b/lib/api/protocol/appium/longPressKeyCode.js
new file mode 100644
index 00000000..4c74f7ab
--- /dev/null
+++ b/lib/api/protocol/appium/longPressKeyCode.js
@@ -0,0 +1,58 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Press and hold a particular key on an Android Device.
+ *
+ * See [official Android Developers docs](https://developer.android.com/reference/android/view/KeyEvent.html) for reference of available Android key code values.
+ *
+ * @example
+ * module.exports = {
+ *   'long press e with caps lock on (keycode 33 and metastate 1048576)': function (app) {
+ *     app
+ *       .appium.longPressKeyCode(33, 1048576);
+ *   },
+ *
+ *   'long press g (keycode 35) with ES6 async/await': async function (app) {
+ *     await app.appium.longPressKeyCode(35);
+ *   }
+ * };
+ *
+ * @syntax .appium.longPressKeyCode(keycode, [callback])
+ * @syntax .appium.longPressKeyCode(keycode, metastate, flags, [callback])
+ * @method longPressKeyCode
+ * @param {number} keycode Key code to press on the device.
+ * @param {number} [metastate] Meta state to press the keycode with.
+ * @param {number} [flags] Flags for the keypress.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.pressKeyCode
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(keycode, ...args) {
+    let metastate;
+    let flags;
+    let callback;
+
+    if (typeof keycode !== 'number') {
+      throw new Error('The first argument to longPressKeyCode is mandatory and must be a number.');
+    }
+
+    if (typeof args[0] === 'function') {
+      callback = args[0];
+    } else {
+      [metastate, flags, callback] = args;
+    }
+
+    const opts = {
+      keycode,
+      ...(metastate && {metastate}),
+      ...(flags && {flags})
+    };
+
+    return this.transportActions.longPressDeviceKeyCode(opts, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/pressKeyCode.js b/lib/api/protocol/appium/pressKeyCode.js
new file mode 100644
index 00000000..8945da8f
--- /dev/null
+++ b/lib/api/protocol/appium/pressKeyCode.js
@@ -0,0 +1,58 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Press a particular key on an Android Device.
+ *
+ * See [official Android Developers docs](https://developer.android.com/reference/android/view/KeyEvent.html) for reference of available Android key code values.
+ *
+ * @example
+ * module.exports = {
+ *   'press e with caps lock on (keycode 33 and metastate 1048576)': function (app) {
+ *     app
+ *       .appium.pressKeyCode(33, 1048576);
+ *   },
+ *
+ *   'press g (keycode 35) with ES6 async/await': async function (app) {
+ *     await app.appium.pressKeyCode(35);
+ *   }
+ * };
+ *
+ * @syntax .appium.pressKeyCode(keycode, [callback])
+ * @syntax .appium.pressKeyCode(keycode, metastate, flags, [callback])
+ * @method pressKeyCode
+ * @param {number} keycode Key code to press on the device.
+ * @param {number} [metastate] Meta state to press the keycode with.
+ * @param {number} [flags] Flags for the keypress.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.longPressKeyCode
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(keycode, ...args) {
+    let metastate;
+    let flags;
+    let callback;
+
+    if (typeof keycode !== 'number') {
+      throw new Error('The first argument to pressKeyCode is mandatory and must be a number.');
+    }
+
+    if (typeof args[0] === 'function') {
+      callback = args[0];
+    } else {
+      [metastate, flags, callback] = args;
+    }
+
+    const opts = {
+      keycode,
+      ...(metastate && {metastate}),
+      ...(flags && {flags})
+    };
+
+    return this.transportActions.pressDeviceKeyCode(opts, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/setContext.js b/lib/api/protocol/appium/setContext.js
new file mode 100644
index 00000000..198173c4
--- /dev/null
+++ b/lib/api/protocol/appium/setContext.js
@@ -0,0 +1,48 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Set the context to be automated. Used when testing hybrid mobile apps using Appium.
+ *
+ * More info here: https://appium.io/docs/en/commands/context/set-context/
+ *
+ * @example
+ * module.exports = {
+ *   'switch to webview context': async function (app) {
+ *     app
+ *       .waitUntil(async function() {
+ *         // wait for webview context to be available
+ *         // initially, this.getContexts() only returns ['NATIVE_APP']
+ *         const contexts = await this.appium.getContexts();
+ *
+ *         return contexts.length > 1;
+ *       })
+ *       .perform(async function() {
+ *         // switch to webview context
+ *         const contexts = await this.appium.getContexts();  // contexts: ['NATIVE_APP', 'WEBVIEW_<id>']
+ *         await this.appium.setContext(contexts[1]);
+ *       });
+ *   },
+ *
+ *   'switch to native context': function (app) {
+ *     app.appium.setContext('NATIVE_APP');
+ *   }
+ * };
+ *
+ * @syntax .appium.setContext(context, [callback])
+ * @method setContext
+ * @param {string} context context name to switch to - a string representing an available context.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html
+ * @see appium.getContext
+ * @see appium.getContexts
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(context, callback) {
+    return this.transportActions.setCurrentContext(context, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/setGeolocation.js b/lib/api/protocol/appium/setGeolocation.js
new file mode 100644
index 00000000..5983a476
--- /dev/null
+++ b/lib/api/protocol/appium/setGeolocation.js
@@ -0,0 +1,37 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Set the current geolocation of the mobile device.
+ *
+ * @example
+ * module.exports = {
+ *   'set geolocation to Tokyo, Japan': function (app) {
+ *     app
+ *       .appium.setGeolocation({latitude: 35.689487, longitude: 139.691706, altitude: 5});
+ *   },
+ *
+ *   'set geolocation to Tokyo, Japan with ES6 async/await': async function (app) {
+ *     await app.appium.setGeolocation({latitude: 35.689487, longitude: 139.691706});
+ *   }
+ * };
+ *
+ * @syntax .appium.setGeolocation({latitude, longitude, altitude}, [callback])
+ * @method setGeolocation
+ * @param {object} [coordinates] `latitude` and `longitude` are required; `altitude` is optional. All should be of type `number`.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.getGeolocation
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(coordinates = {}, callback) {
+    if (!('latitude' in coordinates && 'longitude' in coordinates)) {
+      throw new Error('Please provide both latitude and longitude while using setGeolocation.');
+    }
+
+    return this.transportActions.setDeviceGeolocation(coordinates, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/setOrientation.js b/lib/api/protocol/appium/setOrientation.js
new file mode 100644
index 00000000..b080f60f
--- /dev/null
+++ b/lib/api/protocol/appium/setOrientation.js
@@ -0,0 +1,39 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Set the current device orientation.
+ *
+ * @example
+ * module.exports = {
+ *   'set orientation to LANDSCAPE': function (app) {
+ *     app
+ *       .appium.setOrientation('LANDSCAPE');
+ *   },
+ *
+ *   'set orientation to PORTRAIT with ES6 async/await': async function (app) {
+ *     await app.appium.setOrientation('PORTRAIT');
+ *   }
+ * };
+ *
+ * @syntax .appium.setOrientation(orientation, [callback])
+ * @method setOrientation
+ * @param {string} orientation The new device orientation: `LANDSCAPE` or `PORTRAIT`.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.getOrientation
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(orientation, callback) {
+    orientation = orientation.toUpperCase();
+
+    if (!ProtocolAction.ScreenOrientation.includes(orientation)) {
+      throw new Error('Invalid screen orientation value specified. Accepted values are: ' + ProtocolAction.ScreenOrientation.join(', '));
+    }
+
+    return this.transportActions.setScreenOrientation(orientation, callback);
+  }
+};
diff --git a/lib/api/protocol/appium/startActivity.js b/lib/api/protocol/appium/startActivity.js
new file mode 100644
index 00000000..a218bb24
--- /dev/null
+++ b/lib/api/protocol/appium/startActivity.js
@@ -0,0 +1,48 @@
+const ProtocolAction = require('../_base-action.js');
+
+/**
+ * Start an Android activity by providing package name, activity name and other optional parameters.
+ *
+ * More info here: https://appium.io/docs/en/commands/device/activity/start-activity/
+ *
+ * @example
+ * module.exports = {
+ *   'start an android activity': function (app) {
+ *     app
+ *       .appium.startActivity({
+ *         appPackage: 'com.android.chrome',
+ *         appActivity: 'com.google.android.apps.chrome.Main'
+ *       });
+ *   },
+ *
+ *   'start the main Android activity and wait for onboarding activity to start': function (app) {
+ *     app
+ *       .appium.startActivity({
+ *         appPackage: 'org.wikipedia',
+ *         appActivity: 'org.wikipedia.main.MainActivity',
+ *         appWaitActivity: 'org.wikipedia.onboarding.InitialOnboardingActivity'
+ *       });
+ *   }
+ * };
+ *
+ * @syntax .appium.startActivity(opts, [callback])
+ * @method startActivity
+ * @param {string} opts Options to start the activity with. `appPackage` and `appActivity` are required, [others](https://appium.io/docs/en/commands/device/activity/start-activity/#json-parameters) are optional.
+ * @param {function} [callback] Optional callback function to be called when the command finishes.
+ * @see appium.getCurrentActivity
+ * @see appium.getCurrentPackage
+ * @api protocol.appium
+ */
+module.exports = class Session extends ProtocolAction {
+  static get isTraceable() {
+    return true;
+  }
+
+  command(opts = {}, callback) {
+    if (!('appPackage' in opts && 'appActivity' in opts)) {
+      throw new Error('Please provide both appPackage and appActivity options while using startActivity.');
+    }
+
+    return this.transportActions.startActivity(opts, callback);
+  }
+};
diff --git a/lib/api/protocol/contexts.js b/lib/api/protocol/contexts.js
index 7ac7b8fc..bf36cdb6 100644
--- a/lib/api/protocol/contexts.js
+++ b/lib/api/protocol/contexts.js
@@ -9,6 +9,7 @@ const ProtocolAction = require('./_base-action.js');
  * @param {function} callback Callback function to be called when the command finishes.
  * @returns {Array} an array of strings representing available contexts, e.g 'WEBVIEW', or 'NATIVE'
  * @api protocol.mobile
+ * @deprecated In favour of `.appium.getContexts()`
  */
 module.exports = class Session extends ProtocolAction {
   command(callback) {
diff --git a/lib/api/protocol/currentContext.js b/lib/api/protocol/currentContext.js
index a30d8a09..3d21537d 100644
--- a/lib/api/protocol/currentContext.js
+++ b/lib/api/protocol/currentContext.js
@@ -6,6 +6,7 @@ const ProtocolAction = require('./_base-action.js');
  * @param {function} callback Callback function to be called when the command finishes.
  * @returns {string|null} a string representing the current context or `null`, representing "no context"
  * @api protocol.mobile
+ * @deprecated In favour of `.appium.getContext()`
  */
 module.exports = class Session extends ProtocolAction {
   command(callback) {
diff --git a/lib/api/protocol/getOrientation.js b/lib/api/protocol/getOrientation.js
index 9bfa72f1..28364bc6 100644
--- a/lib/api/protocol/getOrientation.js
+++ b/lib/api/protocol/getOrientation.js
@@ -6,6 +6,7 @@ const ProtocolAction = require('./_base-action.js');
  * @param {function} callback Callback function which is called with the result value.
  * @returns {string} The current browser orientation: {LANDSCAPE|PORTRAIT}
  * @api protocol.mobile
+ * @deprecated In favour of `.appium.getOrientation()`
  */
 module.exports = class Session extends ProtocolAction {
   command(callback) {
diff --git a/lib/api/protocol/setContext.js b/lib/api/protocol/setContext.js
index bc4c37f2..15e6fa08 100644
--- a/lib/api/protocol/setContext.js
+++ b/lib/api/protocol/setContext.js
@@ -6,6 +6,7 @@ const ProtocolAction = require('./_base-action.js');
  * @param {string} context context name to switch to - a string representing an available context.
  * @param {function} [callback] Optional callback function to be called when the command finishes.
  * @api protocol.mobile
+ * @deprecated In favour of `.appium.setContext()`
  */
 module.exports = class Session extends ProtocolAction {
   static get isTraceable() {
diff --git a/lib/api/protocol/setOrientation.js b/lib/api/protocol/setOrientation.js
index 6780195d..0aa61035 100644
--- a/lib/api/protocol/setOrientation.js
+++ b/lib/api/protocol/setOrientation.js
@@ -6,6 +6,7 @@ const ProtocolAction = require('./_base-action.js');
  * @param {string} orientation The new browser orientation: {LANDSCAPE|PORTRAIT}
  * @param {function} [callback] Optional callback function to be called when the command finishes.
  * @api protocol.mobile
+ * @deprecated In favour of `.appium.setOrientation()`
  */
 module.exports = class Session extends ProtocolAction {
   static get isTraceable() {
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index 94427052..cf0df4a3 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -3,6 +3,7 @@ const lodashClone = require('lodash.clone');
 const ArgvSetup = require('./argv-setup.js');
 const Settings = require('../../settings/settings.js');
 const Globals = require('../../testsuite/globals.js');
+const Factory = require('../../transport/factory.js');
 const Concurrency = require('../concurrency');
 const Utils = require('../../utils');
 const Runner = require('../runner.js');
@@ -472,10 +473,7 @@ class CliRunner {
         let promise = Promise.resolve();
 
         if (this.test_settings.selenium && this.test_settings.selenium.start_process) {
-          const SeleniumServer = require('../../transport/selenium-webdriver/selenium.js');
-          this.seleniumService = SeleniumServer.startServer(this.test_settings);
-          promise = this.seleniumService.init();
-          this.test_settings.selenium['[_started]'] = true;
+          promise = Factory.createSeleniumService(this);
         }
 
         const {real_mobile, avd} = this.test_settings.desiredCapabilities;
diff --git a/lib/runner/concurrency/index.js b/lib/runner/concurrency/index.js
index b2924b3e..6e0ffb0b 100644
--- a/lib/runner/concurrency/index.js
+++ b/lib/runner/concurrency/index.js
@@ -231,8 +231,8 @@ class Concurrency extends EventEmitter {
    * @param {function} fn
    */
   static buildProcessQueue(maxWorkers, modules, fn) {
-    const queue = modules;
-    
+    const queue = modules.slice(0);
+
     let workers = 0;
     let index = 0;
 
diff --git a/lib/runner/matchers/tags.js b/lib/runner/matchers/tags.js
index 53a04561..3556614a 100644
--- a/lib/runner/matchers/tags.js
+++ b/lib/runner/matchers/tags.js
@@ -44,6 +44,15 @@ class TagsMatcher {
 
     const context = new Context({modulePath, settings});
     context.setReloadModuleCache();
+    
+    // Defining global browser object to make it available before nightwatch client created.
+    // To avoid errors like browser is not defined if testsuits has tags
+    Object.defineProperty(global, 'browser', {
+      configurable: true,
+      get: function() {
+        return {};
+      }
+    });
 
     try {
       if (this.usingMocha) {
diff --git a/lib/settings/defaults.js b/lib/settings/defaults.js
index 68cc9ecf..b9423adc 100644
--- a/lib/settings/defaults.js
+++ b/lib/settings/defaults.js
@@ -166,7 +166,7 @@ module.exports = {
     start_process: false,
     cli_args: {},
     server_path: null,
-    log_path: '',
+    log_path: './logs',
     port: undefined,
     check_process_delay: 500,
     max_status_poll_tries: 15,
diff --git a/lib/transport/factory.js b/lib/transport/factory.js
index 6bd0164a..0f77bc0a 100644
--- a/lib/transport/factory.js
+++ b/lib/transport/factory.js
@@ -12,6 +12,19 @@ const BrowsersLowerCase = {
 };
 
 module.exports = class TransportFactory {
+  static createSeleniumService(cliRunner) {
+    const usingAppium = cliRunner.test_settings.selenium.use_appium;
+
+    const Server = usingAppium
+      ? require('./selenium-webdriver/appium.js')
+      : require('./selenium-webdriver/selenium.js');
+
+    cliRunner.seleniumService = Server.createService(cliRunner.test_settings);
+    cliRunner.test_settings.selenium['[_started]'] = true;
+
+    return cliRunner.seleniumService.init();
+  }
+
   static usingSeleniumServer(settings) {
     if (!settings.selenium) {
       return false;
@@ -48,8 +61,22 @@ module.exports = class TransportFactory {
 
     let {browserName} = settings.desiredCapabilities;
 
-    // Better support for Appium, if the browserName has explicitly been set to null we can skip all further checks
+    // Better support for app-testing, if the browserName is not present we can skip all further checks
+    const usingAppium = TransportFactory.usingSeleniumServer(settings) && settings.selenium.use_appium;
+    const usingBrowserStack = TransportFactory.usingBrowserstack(settings);
+    if ((usingAppium || usingBrowserStack) && !browserName) {
+      return browserName;
+    }
+
+    // for backward compatibility
     if (browserName === null) {
+      // eslint-disable-next-line no-console
+      console.warn('DEPRECATED: Setting browserName=null for running Appium tests has been deprecated ' +
+        'and will not be supported in future versions. Set `use_appium` property in `selenium` config to true ' +
+        'in your Nightwatch configuration file to run Appium tests.');
+
+      settings.selenium.use_appium = true;
+
       return browserName;
     }
 
@@ -86,21 +113,26 @@ module.exports = class TransportFactory {
 
   static createWebdriver(nightwatchInstance, {usingSeleniumServer, browserName}) {
     if (TransportFactory.usingBrowserstack(nightwatchInstance.settings)) {
-      const Browserstack = require('./selenium-webdriver/browserstack.js');
+      if (!browserName) {
+        const AppAutomate = require('./selenium-webdriver/browserstack/appAutomate.js');
+
+        return new AppAutomate(nightwatchInstance, browserName);
+      }
+
+      const Automate = require('./selenium-webdriver/browserstack/automate.js');
 
-      return new Browserstack(nightwatchInstance, browserName);
+      return new Automate(nightwatchInstance, browserName);
     }
 
     if (usingSeleniumServer) {
-      const Selenium = require('./selenium-webdriver/selenium.js');
-
-      // support for mobile native apps
-      if (browserName === null) {
-        const Mobile = require('./selenium-webdriver/mobile-webdriver.js');
+      if (nightwatchInstance.settings.selenium.use_appium) {
+        const Appium = require('./selenium-webdriver/appium.js');
 
-        return new Mobile(nightwatchInstance);
+        return new Appium(nightwatchInstance, browserName);
       }
 
+      const Selenium = require('./selenium-webdriver/selenium.js');
+
       return new Selenium(nightwatchInstance, browserName);
     }
 
diff --git a/lib/transport/selenium-webdriver/appium.js b/lib/transport/selenium-webdriver/appium.js
new file mode 100644
index 00000000..6caa1409
--- /dev/null
+++ b/lib/transport/selenium-webdriver/appium.js
@@ -0,0 +1,72 @@
+const AppiumBaseServer = require('./appiumBase.js');
+const AppiumServiceBuilder = require('./service-builders/appium');
+const {iosRealDeviceUDID} = require('../../utils/mobile.js');
+
+
+class AppiumServer extends AppiumBaseServer {
+  static createService(settings) {
+    const Options = require('./options.js');
+    const opts = new Options({settings});
+    opts.updateWebdriverPath();
+
+    const appiumService = new AppiumServiceBuilder(settings);
+
+    const outputFile = settings.webdriver.log_file_name || '';
+    appiumService.setOutputFile(outputFile);
+
+    return appiumService;
+  }
+
+  get defaultBrowser() {
+    return null;
+  }
+
+  get ServiceBuilder() {
+    return AppiumServiceBuilder;
+  }
+
+  get defaultServerUrl() {
+    return 'http://127.0.0.1:4723';
+  }
+
+  get defaultPort() {
+    return 4723;
+  }
+
+  get defaultPathPrefix() {
+    return '/wd/hub';
+  }
+
+  createSessionOptions(argv) {
+    this.extractAppiumOptions();
+
+    // set 'appium:udid' if deviceId present in argv
+    if (argv && argv.deviceId) {
+      const platformName = this.desiredCapabilities.platformName;
+
+      let udid = argv.deviceId;
+      if (platformName && platformName.toLowerCase() === 'ios') {
+        udid = iosRealDeviceUDID(udid);
+      }
+
+      this.desiredCapabilities['appium:udid'] = udid;
+    }
+
+    // if `appium:chromedriverExecutable` is present and left blank,
+    // assign the path of binary from `chromedriver` NPM package to it.
+    if (this.desiredCapabilities['appium:chromedriverExecutable'] === '') {
+      const chromedriver = this.seleniumCapabilities.getChromedriverPath();
+      if (chromedriver) {
+        this.desiredCapabilities['appium:chromedriverExecutable'] = chromedriver;
+      }
+    }
+
+    return super.createSessionOptions(argv) || this.desiredCapabilities;
+  }
+
+  createDriver({options}) {
+    return this.createAppiumDriver({options});
+  }
+};
+
+module.exports = AppiumServer;
diff --git a/lib/transport/selenium-webdriver/appiumBase.js b/lib/transport/selenium-webdriver/appiumBase.js
new file mode 100644
index 00000000..51ac5ffe
--- /dev/null
+++ b/lib/transport/selenium-webdriver/appiumBase.js
@@ -0,0 +1,33 @@
+const {WebDriver} = require('selenium-webdriver');
+const {Executor} = require('selenium-webdriver/http');
+const http = require('selenium-webdriver/http');
+const SeleniumServer = require('./selenium');
+const {isObject} = require('../../utils');
+
+
+class AppiumBaseServer extends SeleniumServer {
+  extractAppiumOptions() {
+    // break 'appium:options' to individual configs
+    if (isObject(this.desiredCapabilities['appium:options'])) {
+      const appiumOptions = this.desiredCapabilities['appium:options'];
+      for (let key of Object.keys(appiumOptions)) {
+        const value = appiumOptions[key];
+
+        if (!key.startsWith('appium:')) {
+          key = `appium:${key}`;
+        }
+        this.desiredCapabilities[key] = value;
+      }
+
+      delete this.desiredCapabilities['appium:options'];
+    }
+  }
+
+  createAppiumDriver({options}) {
+    const httpClient = new http.HttpClient(this.getServerUrl());
+
+    return WebDriver.createSession(new Executor(httpClient), options);
+  }
+};
+
+module.exports = AppiumBaseServer;
diff --git a/lib/transport/selenium-webdriver/browserstack/appAutomate.js b/lib/transport/selenium-webdriver/browserstack/appAutomate.js
new file mode 100644
index 00000000..79563ae3
--- /dev/null
+++ b/lib/transport/selenium-webdriver/browserstack/appAutomate.js
@@ -0,0 +1,23 @@
+const BrowserStack = require('./browserstack.js');
+
+class AppAutomate extends BrowserStack {
+  get ApiUrl() {
+    return `https://api.browserstack.com/${this.productNamespace}`;
+  }
+
+  get productNamespace() {
+    return 'app-automate';
+  }
+
+  createSessionOptions() {
+    this.extractAppiumOptions();
+
+    return this.desiredCapabilities;
+  }
+
+  createDriver({options}) {
+    return this.createAppiumDriver({options});
+  }
+}
+
+module.exports = AppAutomate;
diff --git a/lib/transport/selenium-webdriver/browserstack/automate.js b/lib/transport/selenium-webdriver/browserstack/automate.js
new file mode 100644
index 00000000..8415fb88
--- /dev/null
+++ b/lib/transport/selenium-webdriver/browserstack/automate.js
@@ -0,0 +1,22 @@
+const BrowserStack = require('./browserstack.js');
+const {Capabilities} = require('selenium-webdriver');
+
+class Automate extends BrowserStack {
+  get ApiUrl() {
+    return `https://api.browserstack.com/${this.productNamespace}`;
+  }
+
+  get productNamespace() {
+    return 'automate';
+  }
+
+  createDriver({options = this.desiredCapabilities}) {
+    if (options instanceof Capabilities) {
+      return super.createDriver({options});
+    }
+
+    return this.createAppiumDriver({options});
+  }
+}
+
+module.exports = Automate;
diff --git a/lib/transport/selenium-webdriver/browserstack.js b/lib/transport/selenium-webdriver/browserstack/browserstack.js
similarity index 90%
rename from lib/transport/selenium-webdriver/browserstack.js
rename to lib/transport/selenium-webdriver/browserstack/browserstack.js
index 1b262f49..ee7160ac 100644
--- a/lib/transport/selenium-webdriver/browserstack.js
+++ b/lib/transport/selenium-webdriver/browserstack/browserstack.js
@@ -1,13 +1,10 @@
 const stripAnsi = require('strip-ansi');
-const {Logger} = require('../../utils');
-const BaseDriver = require('./mobile-webdriver.js');
+const {Logger} = require('../../../utils');
+const AppiumBaseServer = require('../appiumBase.js');
 const defaultsDeep = require('lodash.defaultsdeep');
 
-class Browserstack extends BaseDriver {
-  get ApiUrl() {
-    return `https://api.browserstack.com/${this.isMobile ? 'app-automate' : 'automate'}`;
-  }
 
+class Browserstack extends AppiumBaseServer {
   bStackOptions() {
     return this.settings.desiredCapabilities['bstack:options'];
   }
@@ -31,7 +28,6 @@ class Browserstack extends BaseDriver {
   constructor(nightwatchInstance, browserName) {
     super(nightwatchInstance, browserName);
 
-    this.isMobile = this.api.isMobile();
     this.useLocal = false;
 
     this.nightwatchInstance.on('nightwatch:session.create', (data) => {
@@ -146,7 +142,7 @@ class Browserstack extends BaseDriver {
       await this.sendReasonToBrowserstack(!!failures, reason);
       // eslint-disable-next-line no-console
       console.log('\n  ' + 'See more info, video, & screenshots on Browserstack:\n' +
-        '  ' + Logger.colors.light_cyan(`https://${this.isMobile ? 'app-automate' : 'automate'}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
+        '  ' + Logger.colors.light_cyan(`https://${this.productNamespace}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
 
       this.sessionId = null;
 
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index b64a2e3f..f32c7a13 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -193,6 +193,7 @@ class Transport extends BaseTransport {
 
   async createDriverService({options, moduleKey, reuseBrowser = false}) {
     try {
+      moduleKey = this.settings.webdriver.log_file_name || moduleKey || '';
 
       if (!this.shouldReuseDriverService(reuseBrowser)) {
         Transport.driverService = new this.ServiceBuilder(this.settings);
@@ -200,10 +201,6 @@ class Transport extends BaseTransport {
       }
 
       this.driverService = Transport.driverService;
-
-      moduleKey = this.settings.webdriver.log_file_name || moduleKey || '';
-
-      Transport.driverService = this.driverService;
     } catch (err) {
       this.showConnectSpinner(colors.red(`Failed to start ${this.serviceName}.`), 'warn');
 
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index e9bc7849..961ab1e1 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -902,6 +902,10 @@ module.exports = class MethodMappings {
           };
         },
 
+        ///////////////////////////////////////////////////////////
+        // Appium
+        ///////////////////////////////////////////////////////////
+
         getAvailableContexts() {
           return '/contexts';
         },
@@ -920,6 +924,62 @@ module.exports = class MethodMappings {
           };
         },
 
+        startActivity(opts) {
+          return {
+            method: 'POST',
+            path: '/appium/device/start_activity',
+            data: opts
+          };
+        },
+
+        getCurrentActivity() {
+          return '/appium/device/current_activity';
+        },
+
+        getCurrentPackage() {
+          return '/appium/device/current_package';
+        },
+
+        getDeviceGeolocation() {
+          return '/location';
+        },
+
+        setDeviceGeolocation(location) {
+          return {
+            method: 'POST',
+            path: '/location',
+            data: {location}
+          };
+        },
+
+        pressDeviceKeyCode(opts) {
+          return {
+            method: 'POST',
+            path: '/appium/device/press_keycode',
+            data: opts
+          };
+        },
+
+        longPressDeviceKeyCode(opts) {
+          return {
+            method: 'POST',
+            path: '/appium/device/long_press_keycode',
+            data: opts
+          };
+        },
+
+        hideDeviceKeyboard(opts) {
+          return {
+            method: 'POST',
+            path: '/appium/device/hide_keyboard',
+            data: opts
+          };
+        },
+
+        isDeviceKeyboardShown() {
+          return '/appium/device/is_keyboard_shown';
+        },
+
         ///////////////////////////////////////////////////////////////////////////
         // Selenium Webdriver
         ///////////////////////////////////////////////////////////////////////////
diff --git a/lib/transport/selenium-webdriver/mobile-webdriver.js b/lib/transport/selenium-webdriver/mobile-webdriver.js
deleted file mode 100644
index b069232e..00000000
--- a/lib/transport/selenium-webdriver/mobile-webdriver.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const {WebDriver} = require('selenium-webdriver');
-const {Executor} = require('selenium-webdriver/http');
-const Selenium = require('./selenium');
-const http = require('selenium-webdriver/http');
-
-
-class Mobile extends Selenium {
-
-  get defaultServerUrl() {
-    return 'http://127.0.0.1:4723';
-  }
-
-  get defaultPathPrefix() {
-    return '/wd/hub';
-  }
-
-  createDriver({options = this.desiredCapabilities} = {}) {
-    const httpClient = new http.HttpClient(this.getServerUrl());
-
-    return WebDriver.createSession(new Executor(httpClient), options);
-  }
-
-}
-
-module.exports = Mobile;
diff --git a/lib/transport/selenium-webdriver/options.js b/lib/transport/selenium-webdriver/options.js
index 246a0ad0..aebb7c41 100644
--- a/lib/transport/selenium-webdriver/options.js
+++ b/lib/transport/selenium-webdriver/options.js
@@ -243,10 +243,20 @@ module.exports = class SeleniumCapabilities {
     }
   }
 
+  getAppiumPath() {
+    return require.resolve('appium');
+  }
+
   updateWebdriverPath() {
     if (this.shouldSetupWebdriver()) {
       try {
         if (this.usingSeleniumServer()) {
+          if (this.settings.selenium.use_appium) {
+            this.settings.selenium.server_path = this.settings.webdriver.server_path = this.getAppiumPath();
+
+            return this;
+          }
+
           this.settings.selenium.server_path = this.settings.webdriver.server_path = require('@nightwatch/selenium-server').path;
           this.settings.selenium.cli_args = this.settings.selenium.cli_args || {};
 
diff --git a/lib/transport/selenium-webdriver/selenium.js b/lib/transport/selenium-webdriver/selenium.js
index 95233e3c..cc24a2e6 100644
--- a/lib/transport/selenium-webdriver/selenium.js
+++ b/lib/transport/selenium-webdriver/selenium.js
@@ -1,22 +1,21 @@
 const DefaultSeleniumDriver = require('./');
-const SeleniumServerBuilder = require('./service-builders/selenium.js');
+const SeleniumServiceBuilder = require('./service-builders/selenium.js');
 
 module.exports = class SeleniumServer extends DefaultSeleniumDriver {
   /**
    * Used when running in parallel with start_process=true
    */
-  static startServer(settings) {
+  static createService(settings) {
     const Options = require('./options.js');
     const opts = new Options({settings});
     opts.updateWebdriverPath();
 
-    const seleniumService = new SeleniumServerBuilder(settings);
-    const moduleKey = settings.webdriver.log_file_name || '';
+    const seleniumService = new SeleniumServiceBuilder(settings);
 
-    seleniumService.setOutputFile(moduleKey);
+    const outputFile = settings.webdriver.log_file_name || '';
+    seleniumService.setOutputFile(outputFile);
 
     return seleniumService;
-
   }
 
   get defaultBrowser() {
@@ -24,7 +23,7 @@ module.exports = class SeleniumServer extends DefaultSeleniumDriver {
   }
 
   get ServiceBuilder() {
-    return SeleniumServerBuilder;
+    return SeleniumServiceBuilder;
   }
 
   get defaultServerUrl() {
diff --git a/lib/transport/selenium-webdriver/service-builders/appium.js b/lib/transport/selenium-webdriver/service-builders/appium.js
new file mode 100644
index 00000000..24e894a0
--- /dev/null
+++ b/lib/transport/selenium-webdriver/service-builders/appium.js
@@ -0,0 +1,98 @@
+const {SeleniumServer, DriverService} = require('selenium-webdriver/remote');
+const {getFreePort} = require('../../../utils');
+const BaseService = require('./base-service.js');
+
+class AppiumService extends DriverService {
+  constructor(server_path, opt_options) {
+    const options = opt_options || {};
+    const {args, default_path_prefix} = options;
+
+    const port = options.port;
+    if (port !== AppiumServiceBuilder.defaultPort && !args.includes('--port')) {
+      args.unshift('--port', port);
+    }
+
+    let cmd = 'node';
+    if (server_path.startsWith('appium')) {
+      cmd = server_path;
+    } else {
+      args.unshift(server_path);
+    }
+
+    super(cmd, {
+      loopback: options.loopback,
+      port,
+      args,
+      path: default_path_prefix,
+      env: options.env,
+      stdio: options.stdio
+    });
+  }
+}
+
+class AppiumServiceBuilder extends BaseService {
+  static get serviceName() {
+    return 'Appium Server';
+  }
+
+  static get defaultPort() {
+    return 4723;
+  }
+
+  get npmPackageName() {
+    return 'appium';
+  }
+
+  get outputFile() {
+    return this._outputFile + '_appium-server.log';
+  }
+
+  get defaultPort() {
+    return AppiumServiceBuilder.defaultPort;
+  }
+
+  get serviceName() {
+    return 'Appium Server';
+  }
+
+  get downloadMessage() {
+    return 'install Appium globally with "npm i -g appium" command, \n and set ' +
+      '"selenium.server_path" config option to "appium".';
+  }
+
+  /**
+   * @param {Capabilities} opts
+   * @returns {Promise<void>}
+   */
+  async createService(opts = {}) {
+    const {port} = this;
+    const options = new SeleniumServer.Options();
+    options.port = port || await getFreePort();
+    const {server_path, default_path_prefix = '/wd/hub'} = this.settings.webdriver;
+
+    const introMsg = `Starting Appium Server on port ${options.port}...`;
+
+    if (opts.showSpinner) {
+      opts.showSpinner(`${introMsg}\n\n`);
+    } else {
+      // eslint-disable-next-line
+      console.info(introMsg);
+    }
+
+    // TODO: read the log_path and add it to cliArgs
+    // above TODO is copied from ./selenium.js
+    options.args = this.cliArgs;
+    options.default_path_prefix = default_path_prefix;
+
+    if (this.hasSinkSupport() && this.needsSinkProcess()) {
+      this.createSinkProcess();
+      options.stdio = ['pipe', this.process.stdin, this.process.stdin];
+    }
+
+    this.service = new AppiumService(server_path, options);
+
+    return this.service.start();
+  }
+}
+
+module.exports = AppiumServiceBuilder;
diff --git a/lib/transport/selenium-webdriver/service-builders/base-service.js b/lib/transport/selenium-webdriver/service-builders/base-service.js
index f6c0890b..7cf7f991 100644
--- a/lib/transport/selenium-webdriver/service-builders/base-service.js
+++ b/lib/transport/selenium-webdriver/service-builders/base-service.js
@@ -375,7 +375,7 @@ class BaseService {
           return resolve();
         }
 
-        Logger.info(`Wrote log file to: ${filePath}.`);
+        Logger.info(`Wrote log file to: ${filePath}`);
         this.stopped = true;
         resolve();
       });


Test patch:
diff --git a/test/lib/mocks/mocks-w3c.yaml b/test/lib/mocks/mocks-w3c.yaml
index 530ad39d..07140138 100644
--- a/test/lib/mocks/mocks-w3c.yaml
+++ b/test/lib/mocks/mocks-w3c.yaml
@@ -103,7 +103,6 @@ mocks:
       firstMatch: [{}]
       alwaysMatch:
         appium:automationName: "XCUITest"
-        appium:app: "../samples/Wikipedia.app"
         appium:appPackage: "org.wikimedia.wikipedia"
         platformName: "iOS"
         appium:deviceName: "iPhone 13"
@@ -113,7 +112,6 @@ mocks:
       sessionId: '13521-10219-202'
       capabilities:
         appium:automationName: "XCUITest"
-        appium:app: "../samples/Wikipedia.app"
         appium:appPackage: "org.wikimedia.wikipedia"
         platformName: "iOS"
         appium:deviceName: "iPhone 13"
diff --git a/test/sampletests/tagswithbrowserobject/sample.js b/test/sampletests/tagswithbrowserobject/sample.js
new file mode 100644
index 00000000..018ead8b
--- /dev/null
+++ b/test/sampletests/tagswithbrowserobject/sample.js
@@ -0,0 +1,10 @@
+describe('Sample test with tags and global browser object', function() {
+  const globals = browser.globals;
+  this.tags = ['browser'];
+  it('demoTagTest', function(browser) {
+    browser
+      .url('http://localhost')
+      .assert.elementPresent('#weblogin')
+      .end();
+  });
+});
\ No newline at end of file
diff --git a/test/src/api/commands/client/testSetGeolocation.js b/test/src/api/commands/client/testSetGeolocation.js
index f164c9d8..d6658044 100644
--- a/test/src/api/commands/client/testSetGeolocation.js
+++ b/test/src/api/commands/client/testSetGeolocation.js
@@ -215,5 +215,4 @@ describe('.setGeolocation()', function () {
       client.start(done);
     });
   });
-
-});
\ No newline at end of file
+});
diff --git a/test/src/api/commands/element/testClick.js b/test/src/api/commands/element/testClick.js
index f1cc890c..1195f141 100644
--- a/test/src/api/commands/element/testClick.js
+++ b/test/src/api/commands/element/testClick.js
@@ -195,13 +195,13 @@ describe('.click()', function() {
       silent: false,
       selenium: {
         start_process: false,
+        use_appium: true,
         port: 10195,
         host: 'localhost'
       },
       desiredCapabilities: {
         'appium:automationName': 'XCUITest',
         browserName: null,
-        'appium:app': '../samples/Wikipedia.app',
         'appium:appPackage': 'org.wikimedia.wikipedia',
         platformName: 'iOS',
         'appium:deviceName': 'iPhone 13',
diff --git a/test/src/api/protocol/testActivity.js b/test/src/api/protocol/testActivity.js
new file mode 100644
index 00000000..4dd70328
--- /dev/null
+++ b/test/src/api/protocol/testActivity.js
@@ -0,0 +1,62 @@
+const assert = require('assert');
+const Globals = require('../../../lib/globals.js');
+
+describe('Android activity commands', function() {
+  before(function() {
+    Globals.protocolBefore();
+  });
+
+  it('testStartActivity with correct options', function() {
+    const opts = {
+      appPackage: 'org.wikipedia',
+      appActivity: 'org.wikipedia.main.MainActivity',
+      appWaitActivity: 'org.wikipedia.onboarding.InitialOnboardingActivity'
+    };
+
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/start_activity');
+        assert.deepStrictEqual(opts.data, {
+          appPackage: 'org.wikipedia',
+          appActivity: 'org.wikipedia.main.MainActivity',
+          appWaitActivity: 'org.wikipedia.onboarding.InitialOnboardingActivity'
+        });
+      },
+      commandName: 'appium.startActivity',
+      args: [opts]
+    });
+  });
+
+  it('testStartActivity with no options', function() {
+    return Globals.protocolTest({
+      commandName: 'appium.startActivity',
+      args: []
+    }).catch(err => {
+      return err;
+    }).then((result) => {
+      assert.ok(result instanceof Error);
+      assert.strictEqual(result.message.includes('Please provide both appPackage and appActivity options while using startActivity.'), true);
+    });
+  });
+
+  it('testGetCurrentActivity', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/current_activity');
+      },
+      commandName: 'appium.getCurrentActivity',
+      args: []
+    });
+  });
+
+  it('testGetCurrentPackage', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/current_package');
+      },
+      commandName: 'appium.getCurrentPackage',
+      args: []
+    });
+  });
+});
diff --git a/test/src/api/protocol/testContext.js b/test/src/api/protocol/testContext.js
index 2e4da7bf..b75ba410 100644
--- a/test/src/api/protocol/testContext.js
+++ b/test/src/api/protocol/testContext.js
@@ -39,4 +39,38 @@ describe('context commands', function() {
       args: [text]
     });
   });
+
+  it('testGetContexts - appium', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/contexts');
+      },
+      commandName: 'appium.getContexts',
+      args: []
+    });
+  });
+
+  it('testGetContext - appium', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/context');
+      },
+      commandName: 'appium.getContext',
+      args: []
+    });
+  });
+
+  it('testSetContext - appium', function() {
+    let text = 'NATIVE';
+
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/context');
+        assert.deepStrictEqual(opts.data, {name: text});
+      },
+      commandName: 'appium.setContext',
+      args: [text]
+    });
+  });
 });
diff --git a/test/src/api/protocol/testGeolocation.js b/test/src/api/protocol/testGeolocation.js
new file mode 100644
index 00000000..2e0af5ce
--- /dev/null
+++ b/test/src/api/protocol/testGeolocation.js
@@ -0,0 +1,60 @@
+const assert = require('assert');
+const Globals = require('../../../lib/globals.js');
+
+describe('Geolocation commands', function() {
+  before(function() {
+    Globals.protocolBefore();
+  });
+
+  it('testSetGeolocation with correct options', function() {
+    const coordinates = {latitude: 35.689487, longitude: 139.691706, altitude: 5};
+
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/location');
+        assert.deepStrictEqual(opts.data, {
+          location: {latitude: 35.689487, longitude: 139.691706, altitude: 5}
+        });
+      },
+      commandName: 'appium.setGeolocation',
+      args: [coordinates]
+    });
+  });
+
+  it('testSetGeolocation with incorrect options', function() {
+    const coordinates = {latitude: 35.689487};
+
+    return Globals.protocolTest({
+      commandName: 'appium.setGeolocation',
+      args: [coordinates]
+    }).catch(err => {
+      return err;
+    }).then((result) => {
+      assert.ok(result instanceof Error);
+      assert.strictEqual(result.message.includes('Please provide both latitude and longitude while using setGeolocation.'), true);
+    });
+  });
+
+  it('testSetGeolocation with no options', function() {
+    return Globals.protocolTest({
+      commandName: 'appium.setGeolocation',
+      args: []
+    }).catch(err => {
+      return err;
+    }).then((result) => {
+      assert.ok(result instanceof Error);
+      assert.strictEqual(result.message.includes('Please provide both latitude and longitude while using setGeolocation.'), true);
+    });
+  });
+
+  it('testGetGeolocation', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/location');
+      },
+      commandName: 'appium.getGeolocation',
+      args: []
+    });
+  });
+});
diff --git a/test/src/api/protocol/testKeyboardInteraction.js b/test/src/api/protocol/testKeyboardInteraction.js
new file mode 100644
index 00000000..0a9d5d47
--- /dev/null
+++ b/test/src/api/protocol/testKeyboardInteraction.js
@@ -0,0 +1,141 @@
+const assert = require('assert');
+const Globals = require('../../../lib/globals.js');
+
+describe('Keyboard interaction commands', function() {
+  before(function() {
+    Globals.protocolBefore();
+  });
+
+  it('test hideKeyboard', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/hide_keyboard');
+        assert.deepStrictEqual(opts.data, {});
+      },
+      commandName: 'appium.hideKeyboard',
+      args: []
+    });
+  });
+
+  it('test isKeyboardShown', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/is_keyboard_shown');
+      },
+      commandName: 'appium.isKeyboardShown',
+      args: []
+    });
+  });
+
+  it('test pressKeyCode - with just keycode', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15
+        });
+      },
+      commandName: 'appium.pressKeyCode',
+      args: [15]
+    });
+  });
+
+  it('test pressKeyCode - with all options', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15,
+          metastate: 14,
+          flags: 13
+        });
+      },
+      commandName: 'appium.pressKeyCode',
+      args: [15, 14, 13]
+    });
+  });
+
+  it('test pressKeyCode - with callback as second arg and then random args', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15
+        });
+      },
+      commandName: 'appium.pressKeyCode',
+      args: [15, () => {}, 14]
+    });
+  });
+
+  it('test pressKeyCode - with no options', function() {
+    return Globals.protocolTest({
+      commandName: 'appium.pressKeyCode',
+      args: []
+    }).catch(err => {
+      return err;
+    }).then((result) => {
+      assert.ok(result instanceof Error);
+      assert.strictEqual(result.message.includes('The first argument to pressKeyCode is mandatory and must be a number.'), true);
+    });
+  });
+
+  it('test longPressKeyCode - with just keycode', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/long_press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15
+        });
+      },
+      commandName: 'appium.longPressKeyCode',
+      args: [15]
+    });
+  });
+
+  it('test longPressKeyCode - with 2 options', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/long_press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15,
+          metastate: 14
+        });
+      },
+      commandName: 'appium.longPressKeyCode',
+      args: [15, 14]
+    });
+  });
+
+  it('test longPressKeyCode - with callback as second arg and then random args', function() {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        assert.strictEqual(opts.method, 'POST');
+        assert.strictEqual(opts.path, '/session/1352110219202/appium/device/long_press_keycode');
+        assert.deepStrictEqual(opts.data, {
+          keycode: 15
+        });
+      },
+      commandName: 'appium.longPressKeyCode',
+      args: [15, () => {}, 14]
+    });
+  });
+
+  it('test longPressKeyCode - with no options', function() {
+    return Globals.protocolTest({
+      commandName: 'appium.longPressKeyCode',
+      args: []
+    }).catch(err => {
+      return err;
+    }).then((result) => {
+      assert.ok(result instanceof Error);
+      assert.strictEqual(result.message.includes('The first argument to longPressKeyCode is mandatory and must be a number.'), true);
+    });
+  });
+});
diff --git a/test/src/api/protocol/testOrientation.js b/test/src/api/protocol/testOrientation.js
index 331bf99b..f86bf613 100644
--- a/test/src/api/protocol/testOrientation.js
+++ b/test/src/api/protocol/testOrientation.js
@@ -43,4 +43,41 @@ describe('orientation commands', function() {
       return true;
     }).then(result => assert.strictEqual(result, true));
   });
+
+  it('testGetOrientation - appium', function () {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        strictEqual(opts.method, 'GET');
+        strictEqual(opts.path, '/session/1352110219202/orientation');
+      },
+      commandName: 'appium.getOrientation',
+      args: []
+    });
+  });
+
+  it('testSetOrientation - appium', function () {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+        strictEqual(opts.method, 'POST');
+        strictEqual(opts.path, '/session/1352110219202/orientation');
+        assert.deepStrictEqual(opts.data, {orientation: 'LANDSCAPE'});
+      },
+      commandName: 'appium.setOrientation',
+      args: ['LANDSCAPE']
+    });
+  });
+
+  it('testSetOrientationInvalid - appium', function () {
+    return Globals.protocolTest({
+      assertion: function(opts) {
+
+      },
+      commandName: 'appium.setOrientation',
+      args: ['TEST']
+    }).catch(err => {
+      strictEqual(err.message, 'Error while running "setOrientation" command: Invalid screen orientation value specified. Accepted values are: LANDSCAPE, PORTRAIT');
+
+      return true;
+    }).then(result => assert.strictEqual(result, true));
+  });
 });
diff --git a/test/src/cli/testServiceCreationFromCli.js b/test/src/cli/testServiceCreationFromCli.js
new file mode 100644
index 00000000..5e939f20
--- /dev/null
+++ b/test/src/cli/testServiceCreationFromCli.js
@@ -0,0 +1,263 @@
+const common = require('../../common.js');
+const mockery = require('mockery');
+const assert = require('assert');
+const origPath = require('path');
+
+describe('Service creation from cli.js', function() {
+  this.timeout(15000);
+
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnUnregistered: false});
+
+    mockery.registerMock('package.json', {});
+    mockery.registerMock('path', {
+      ...origPath,
+      resolve: function(path, ...args) {
+        if (path === './appium_config.json') {
+          return path;
+        }
+
+        return origPath.resolve(path, ...args);
+      }
+    });
+  });
+
+  afterEach(function() {
+    mockery.deregisterAll();
+    mockery.resetCache();
+    mockery.disable();
+  });
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function mockOptions({onAppiumPathQuery = fn}) {
+    const Options = common.require('transport/selenium-webdriver/options.js');
+    class MockOptions extends Options {
+      getAppiumPath() {
+        onAppiumPathQuery();
+
+        return 'path/to/appium';
+      }
+    }
+
+    deleteFromRequireCache('selenium-webdriver/options');
+
+    mockery.registerMock('./options.js', MockOptions);
+  }
+
+  it('test appium server startup', function() {
+    let appiumPathQueried = false;
+    mockOptions({
+      onAppiumPathQuery() {
+        appiumPathQueried = true;
+      }
+    });
+
+    mockery.registerMock('./appium_config.json', {
+      src_folders: ['test/sampletests/before-after'],
+      output_folder: false,
+      selenium: {
+        start_process: true,
+        use_appium: true
+      },
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false,
+          desiredCapabilities: {
+            browserName: ''
+          }
+        }
+      }
+    });
+  
+    mockery.registerMock('./service-builders/appium', class AppiumServer {
+      constructor(settings) {
+        this.settings = settings;
+        this.service = {
+          kill() {
+            return Promise.resolve();
+          }
+        };
+      }
+  
+      async init() {
+        this.initCalled = true;
+      }
+  
+      stop() {
+        this.stopped = true;
+      }
+  
+      setOutputFile(filename) {
+        this.outfilename = filename;
+      }
+    });
+  
+    const CliRunner = common.require('runner/cli/cli.js');
+    const runner = new CliRunner({
+      config: './appium_config.json'
+    }).setup();
+  
+    return runner.runTests().then(_ => {
+      assert.strictEqual(runner.seleniumService.initCalled, true);
+      assert.strictEqual(runner.seleniumService.stopped, true);
+      assert.strictEqual(runner.seleniumService.outfilename, '');
+      assert.strictEqual(appiumPathQueried, true);
+      assert.strictEqual(runner.seleniumService.settings.webdriver.server_path, 'path/to/appium');
+      assert.strictEqual(runner.seleniumService.settings.selenium['[_started]'], true);
+    });
+  });
+
+  it('test appium server startup with server_path=appium', function() {
+    let appiumPathQueried = false;
+    mockOptions({
+      onAppiumPathQuery() {
+        appiumPathQueried = true;
+      }
+    });
+
+    mockery.registerMock('./appium_config.json', {
+      src_folders: ['test/sampletests/before-after'],
+      output_folder: false,
+      selenium: {
+        start_process: true,
+        use_appium: true,
+        server_path: 'appium'
+      },
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false,
+          desiredCapabilities: {
+            browserName: 'firefox'
+          }
+        }
+      }
+    });
+  
+    mockery.registerMock('./service-builders/appium', class AppiumServer {
+      constructor(settings) {
+        this.settings = settings;
+        this.service = {
+          kill() {
+            return Promise.resolve();
+          }
+        };
+      }
+  
+      async init() {
+        this.initCalled = true;
+      }
+  
+      stop() {
+        this.stopped = true;
+      }
+  
+      setOutputFile(filename) {
+        this.outfilename = filename;
+      }
+    });
+  
+    const CliRunner = common.require('runner/cli/cli.js');
+    const runner = new CliRunner({
+      config: './appium_config.json'
+    }).setup();
+  
+    return runner.runTests().then(_ => {
+      assert.strictEqual(runner.seleniumService.initCalled, true);
+      assert.strictEqual(runner.seleniumService.stopped, true);
+      assert.strictEqual(runner.seleniumService.outfilename, '');
+      assert.strictEqual(appiumPathQueried, false);
+      assert.strictEqual(runner.seleniumService.settings.webdriver.server_path, 'appium');
+      assert.strictEqual(runner.seleniumService.settings.selenium['[_started]'], true);
+    });
+  });
+
+  it('test appium server startup with start_process false', function() {
+    mockery.registerMock('./appium_config.json', {
+      src_folders: ['test/sampletests/before-after'],
+      output_folder: false,
+      selenium: {
+        start_process: false,
+        use_appium: true,
+        host: 'localhost'
+      },
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false,
+          desiredCapabilities: {
+            browserName: ''
+          }
+        }
+      }
+    });
+  
+    let appiumServerStarted = false;
+    mockery.registerMock('./selenium-webdriver/appium.js', class AppiumServer {
+      static createService() {
+        appiumServerStarted = true;
+      }
+    });
+  
+    const CliRunner = common.require('runner/cli/cli.js');
+    const runner = new CliRunner({
+      config: './appium_config.json'
+    }).setup();
+  
+    return runner.runTests().then(_ => {
+      assert.strictEqual(appiumServerStarted, false);
+      assert.strictEqual(runner.seleniumService, undefined);
+    });
+  });
+
+  it('test selenium server startup if use_appium not used', function() {
+    mockery.registerMock('./appium_config.json', {
+      src_folders: ['test/sampletests/before-after'],
+      output_folder: false,
+      selenium: {
+        start_process: true
+      },
+      test_settings: {
+        'default': {
+          output: false,
+          silent: false,
+          desiredCapabilities: {
+            browserName: 'chrome'
+          }
+        }
+      }
+    });
+  
+    let seleniumServerStarted = false;
+    mockery.registerMock('./selenium-webdriver/selenium.js', class SeleniumServer {
+      static createService() {
+        seleniumServerStarted = true;
+
+        return {
+          async init() {}
+        };
+      }
+    });
+  
+    const CliRunner = common.require('runner/cli/cli.js');
+    const runner = new CliRunner({
+      config: './appium_config.json'
+    }).setup();
+  
+    return runner.runTests().then(_ => {
+      assert.strictEqual(seleniumServerStarted, true);
+      assert.strictEqual(runner.test_settings.selenium['[_started]'], true);
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/src/core/testCreateSession.js b/test/src/core/testCreateSession.js
index 654e1097..815471e4 100644
--- a/test/src/core/testCreateSession.js
+++ b/test/src/core/testCreateSession.js
@@ -388,8 +388,7 @@ describe('test Request With Credentials', function () {
     }, /Error: Unknown browser:/);
   });
 
-  it('Test create session with browsername null - mobile support', async function () {
-
+  it('Test create session with browsername null - Appium support', async function () {
     nock('http://localhost:4723')
       .post('/wd/hub/session')
       .reply(201, {
@@ -433,6 +432,48 @@ describe('test Request With Credentials', function () {
     });
   });
 
+  it('Test create session with use_appium property - Appium support', async function () {
+    nock('http://somewhere:9999')
+      .post('/wd/hub/session')
+      .reply(201, {
+        value: {
+          capabilities: {
+            platformName: 'android',
+            platformVersion: '12.0',
+            name: 'sample test goes here'
+          },
+          sessionId: '1352110219202'
+        }
+      });
+
+    const client = Nightwatch.createClient({
+      webdriver: {
+        start_process: false
+      },
+      selenium: {
+        start_process: false,
+        use_appium: true,
+        host: 'somewhere',
+        port: 9999
+      },
+      desiredCapabilities: {
+        browserName: '',
+        platformName: 'android',
+        platformVersion: '12.0'
+      }
+    });
+
+    const result = await client.createSession();
+    assert.deepStrictEqual(result, {
+      capabilities: {
+        platformName: 'android',
+        platformVersion: '12.0',
+        name: 'sample test goes here'
+      },
+      sessionId: '1352110219202'
+    });
+  });
+
   it('Test create session with browserstack', async function () {
     nock('https://hub.browserstack.com')
       .post('/wd/hub/session')
@@ -573,7 +614,7 @@ describe('test Request With Credentials', function () {
       });
 
     nock('https://api.browserstack.com')
-      .get('/automate/builds.json')
+      .get('/app-automate/builds.json')
       .reply(200, [
         {
           automation_build: {
diff --git a/test/src/index/transport/testAppiumOptions.js b/test/src/index/transport/testAppiumOptions.js
deleted file mode 100644
index 0a79e0d9..00000000
--- a/test/src/index/transport/testAppiumOptions.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const assert = require('assert');
-const {describe} = require('mocha');
-const Nightwatch = require('../../../lib/nightwatch.js');
-
-describe('Test Appium client', function () {
-  it('have isIOS() function', function () {
-    const client = Nightwatch.createClient({
-      desiredCapabilities: {
-        browserName: 'Safari',
-        javascriptEnabled: true,
-        platformName: 'iOS',
-        platformVersion: '15.0',
-        deviceName: 'iPhone 13'
-      }
-    });
-
-    client.transport.createSessionOptions();
-    assert.strictEqual(client.api.isIOS(), true);
-    assert.strictEqual(client.api.isMobile(), true);
-  });
-
-  it('have isAndroid() function', function () {
-    const client = Nightwatch.createClient({
-      desiredCapabilities: {
-        browserName: 'Chrome',
-        javascriptEnabled: true,
-        platformName: 'Android',
-        platformVersion: '13.0',
-        deviceName: 'Google Pixel'
-      }
-    });
-
-    client.transport.createSessionOptions();
-    assert.strictEqual(client.api.isAndroid(), true);
-    assert.strictEqual(client.api.isMobile(), true);
-  });
-});
-
-describe('test nightwatch client', function () {
-  it('doesn\'t have isIOS(), isAndroid() and isMobile() function', function () {
-    const client = Nightwatch.createClient({
-      desiredCapabilities: {
-        browserName: 'Chrome'
-      }
-    });
-
-    client.transport.createSessionOptions();
-    assert.strictEqual(client.api.isIOS(), false);
-    assert.strictEqual(client.api.isAndroid(), false);
-    assert.strictEqual(client.api.isMobile(), false);
-  });
-});
diff --git a/test/src/index/transport/testAppiumTransport.js b/test/src/index/transport/testAppiumTransport.js
new file mode 100644
index 00000000..07076433
--- /dev/null
+++ b/test/src/index/transport/testAppiumTransport.js
@@ -0,0 +1,90 @@
+const assert = require('assert');
+const common = require('../../../common.js');
+const NightwatchClient = common.require('index.js');
+const Appium = common.require('transport/selenium-webdriver/appium.js');
+
+
+describe('AppiumTransport', function () {
+  it('test create Transport for Appium external',  function(){
+    const client = NightwatchClient.client({
+      selenium: {
+        start_process: false,
+        use_appium: true,
+        host: 'remote.host',
+        port: 443
+      },
+      webdriver: {
+        start_process: false
+      },
+      output: true,
+      silent: false,
+      desiredCapabilities: {
+        browserName: '',
+        platformName: 'iOS',
+        platformVersion: '15.5',
+        deviceName: 'iPhone 13'
+      }
+    });
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.settings.webdriver.host, 'remote.host');
+    assert.strictEqual(client.settings.webdriver.port, 443);
+    assert.strictEqual(client.settings.webdriver.ssl, true);
+    assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
+    assert.strictEqual(client.settings.webdriver.start_process, false);
+    assert.strictEqual(client.transport.desiredCapabilities.browserName, '');
+  });
+
+  it('test create Transport for Appium managed with Firefox',  function(){
+    const client = NightwatchClient.client({
+      selenium: {
+        start_process: true,
+        use_appium: true
+      },
+      webdriver: {
+        start_process: false
+      },
+      output: true,
+      silent: false,
+      desiredCapabilities: {
+        browserName: 'firefox',
+        platformName: 'Android'
+      }
+    });
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.settings.webdriver.host, 'localhost');
+    assert.strictEqual(client.settings.webdriver.port, 4723);
+    assert.strictEqual(client.settings.webdriver.ssl, false);
+    assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
+    assert.strictEqual(client.settings.webdriver.start_process, true);
+    assert.strictEqual(client.transport.desiredCapabilities.browserName, 'firefox');
+  });
+
+  it('test create Transport for Appium without use_appium and with browserName=null',  function(){
+    const client = NightwatchClient.client({
+      webdriver: {
+        start_process: false
+      },
+      selenium: {
+        start_process: true,
+        host: 'localhost',
+        port: 9999,
+        default_path_prefix: '',
+        '[_started]': true
+      },
+      output: true,
+      silent: false,
+      desiredCapabilities: {
+        browserName: null
+      }
+    });
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.settings.webdriver.host, 'localhost');
+    assert.strictEqual(client.settings.webdriver.port, 9999);
+    assert.strictEqual(client.settings.webdriver.default_path_prefix, '');
+    assert.strictEqual(client.settings.webdriver.start_process, false);
+    assert.strictEqual(client.transport.desiredCapabilities.browserName, null);
+  });
+});
diff --git a/test/src/index/transport/testBrowserstackTransport.js b/test/src/index/transport/testBrowserstackTransport.js
index 4c559ddd..01909607 100644
--- a/test/src/index/transport/testBrowserstackTransport.js
+++ b/test/src/index/transport/testBrowserstackTransport.js
@@ -3,9 +3,10 @@ const nock = require('nock');
 const common = require('../../../common.js');
 const NightwatchClient = common.require('index.js');
 const SeleniumRemote = common.require('transport/selenium-webdriver/selenium.js');
-const Browserstack = common.require('transport/selenium-webdriver/browserstack.js');
+const Automate = common.require('transport/selenium-webdriver/browserstack/automate.js');
+const AppAutomate = common.require('transport/selenium-webdriver/browserstack/appAutomate.js');
 
-xdescribe('BrowserstackTransport', function () {
+describe('BrowserstackTransport', function () {
   beforeEach(function() {
     try {
       nock.activate();
@@ -65,12 +66,10 @@ xdescribe('BrowserstackTransport', function () {
     nock('https://hub-cloud.browserstack.com')
       .post('/wd/hub/session')
       .reply(201, function (uri, requestBody) {
-        const reqObj = JSON.parse(requestBody);
-
         return {
           value: {
             sessionId: '1352110219202',
-            capabilities: reqObj.desiredCapabilities
+            capabilities: requestBody.capabilities
           }
         };
       });
@@ -104,7 +103,7 @@ xdescribe('BrowserstackTransport', function () {
     delete process.env['KEY'];
   });
   
-  it('test create Transport for Browserstack', async function() {
+  it('test create Transport for Browserstack - Automate', async function() {
     const client = NightwatchClient.client({
       webdriver: {
         host: 'hub-cloud.browserstack.com',
@@ -121,12 +120,10 @@ xdescribe('BrowserstackTransport', function () {
     nock('https://hub-cloud.browserstack.com')
       .post('/wd/hub/session')
       .reply(201, function (uri, requestBody) {
-        const reqObj = JSON.parse(requestBody);
-
         return {
           value: {
             sessionId: '1352110219202',
-            capabilities: reqObj.desiredCapabilities
+            capabilities: requestBody.capabilities
           }
         };
       });
@@ -147,7 +144,7 @@ xdescribe('BrowserstackTransport', function () {
         }
       ]);
 
-    assert.ok(client.transport instanceof Browserstack);
+    assert.ok(client.transport instanceof Automate);
     assert.strictEqual(client.settings.webdriver.host, 'hub-cloud.browserstack.com');
     assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
     assert.strictEqual(client.settings.webdriver.ssl, true);
@@ -178,10 +175,8 @@ xdescribe('BrowserstackTransport', function () {
 
   });
 
-  xit('test create Transport for Browserstack with failures', async function() {
+  it('test create Transport for Browserstack - App Automate', async function() {
     const client = NightwatchClient.client({
-      output: false,
-      silent: false,
       webdriver: {
         host: 'hub-cloud.browserstack.com',
         port: 443,
@@ -190,25 +185,23 @@ xdescribe('BrowserstackTransport', function () {
       desiredCapabilities: {
         'browserstack.user': 'test-access-user',
         'browserstack.key': 'test-access-key',
-        browserName: 'chrome'
+        browserName: ''
       }
     });
 
     nock('https://hub-cloud.browserstack.com')
       .post('/wd/hub/session')
       .reply(201, function (uri, requestBody) {
-        const reqObj = JSON.parse(requestBody);
-
         return {
           value: {
             sessionId: '1352110219202',
-            capabilities: reqObj.desiredCapabilities
+            capabilities: requestBody.capabilities
           }
         };
       });
 
     nock('https://api.browserstack.com')
-      .get('/automate/builds.json')
+      .get('/app-automate/builds.json')
       .reply(200, [
         {
           automation_build: {
@@ -223,104 +216,107 @@ xdescribe('BrowserstackTransport', function () {
         }
       ]);
 
+    assert.ok(client.transport instanceof AppAutomate);
+    assert.strictEqual(client.settings.webdriver.host, 'hub-cloud.browserstack.com');
+    assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
+    assert.strictEqual(client.settings.webdriver.ssl, true);
+
     const {transport} = client;
+    assert.ok(transport instanceof SeleniumRemote);
+
     let result = await transport.createSession({argv: undefined, moduleKey: ''});
     result.sessionId = '1234567';
     client.emit('nightwatch:session.create', result);
 
-    return new Promise((resolve, reject) => {
-      setTimeout(async function() {
-        try {
-          nock('https://api.browserstack.com')
-            .put('/automate/sessions/1234567.json', {
-              status: 'failed',
-              reason: 'NightwatchAssertError: Timed out while waiting for element <#james> to be present for 5000 milliseconds. - expected "visible" but got: "not found" (5400ms)'
-            })
-            .reply(200, {});
-    
-          const error = new Error('Timed out while waiting for element <#james> to be present for 5000 milliseconds. - expected "visible" but got: "not found" (5400ms)');
-          error.name = 'NightwatchAssertError';
-    
-          result = await transport.testSuiteFinished(error);
-    
-          assert.strictEqual(result, true);
-          assert.strictEqual(transport.sessionId, null);
-          resolve();
-        } catch (err) {
-          reject(err);
-        }
-      }, 100);
-    });
+    assert.strictEqual(transport.username, 'test-access-user');
+    assert.strictEqual(transport.accessKey, 'test-access-key');
+    assert.strictEqual(client.settings.webdriver.start_process, false);
+      
+    nock('https://api.browserstack.com')
+      .put('/app-automate/sessions/1234567.json', {
+        status: 'passed',
+        reason: ''
+      })
+      .reply(200, {});
+
+    result = await transport.testSuiteFinished(false);
+    assert.strictEqual(result, true);
+    assert.strictEqual(transport.sessionId, null);
+
+    assert.strictEqual(transport.buildId, '123-567-89');
+
   });
 
-  it('test create Transport for Browserstack - App automate', async function() {
+  it('test create Transport for Browserstack with failures', async function() {
     const client = NightwatchClient.client({
+      output: false,
+      silent: false,
       webdriver: {
         host: 'hub-cloud.browserstack.com',
         port: 443,
-        start_process: false
+        start_process: true
       },
       desiredCapabilities: {
-        'browserstack.user': 'test-acess-user',
-        'browserstack.key': 'test-acess-key',
-        platformName: 'ios',
-        platfromVersion: '12',
-        deviceName: 'iPhone 12'
+        'browserstack.user': 'test-access-user',
+        'browserstack.key': 'test-access-key',
+        browserName: 'chrome'
       }
     });
 
     nock('https://hub-cloud.browserstack.com')
       .post('/wd/hub/session')
       .reply(201, function (uri, requestBody) {
-        const reqObj = JSON.parse(requestBody);
-
         return {
           value: {
             sessionId: '1352110219202',
-            capabilities: reqObj.desiredCapabilities
+            capabilities: requestBody.capabilities
           }
         };
       });
-    
+
     nock('https://api.browserstack.com')
-      .get('/app-automate/builds.json')
-      .reply(200, [{
-        automation_build: {
-          name: 'nightwatch-test-build',
-          hashed_id: '123-567-89'
-        } 
-      },
-      {
-        automation_build: {
-          name: 'test-build'
+      .get('/automate/builds.json')
+      .reply(200, [
+        {
+          automation_build: {
+            name: 'nightwatch-test-build',
+            hashed_id: '123-567-89'
+          }
+        },
+        {
+          automation_build: {
+            name: 'test-build'
+          }
         }
-      }
       ]);
-    
-
-    assert.ok(client.transport instanceof Browserstack);
-    assert.strictEqual(client.settings.webdriver.host, 'hub-cloud.browserstack.com');
-    assert.strictEqual(client.settings.webdriver.default_path_prefix, '/wd/hub');
-    assert.strictEqual(client.settings.webdriver.start_process, false);
-    assert.strictEqual(client.settings.webdriver.ssl, true);
 
     const {transport} = client;
-
     let result = await transport.createSession({argv: undefined, moduleKey: ''});
     result.sessionId = '1234567';
     client.emit('nightwatch:session.create', result);
 
-    nock('https://api.browserstack.com')
-      .put('/app-automate/sessions/1234567.json', {
-        status: 'passed',
-        reason: ''
-      })
-      .reply(200, {});
-
-    result = await transport.testSuiteFinished(false);
-    assert.strictEqual(result, true);
-    assert.strictEqual(transport.sessionId, null);
-    assert.strictEqual(transport.buildId, '123-567-89');
+    return new Promise((resolve, reject) => {
+      setTimeout(async function() {
+        try {
+          nock('https://api.browserstack.com')
+            .put('/automate/sessions/1234567.json', {
+              status: 'failed',
+              reason: 'NightwatchAssertError: Timed out while waiting for element <#james> to be present for 5000 milliseconds. - expected "visible" but got: "not found" (5400ms)'
+            })
+            .reply(200, {});
+    
+          const error = new Error('Timed out while waiting for element <#james> to be present for 5000 milliseconds. - expected "visible" but got: "not found" (5400ms)');
+          error.name = 'NightwatchAssertError';
+    
+          result = await transport.testSuiteFinished(error);
 
+          assert.strictEqual(result, true);
+          assert.strictEqual(transport.sessionId, null);
+          resolve();
+        } catch (err) {
+          reject(err);
+        }
+      }, 100);
+    });
   });
 });
diff --git a/test/src/index/transport/testMobileOptions.js b/test/src/index/transport/testMobileOptions.js
new file mode 100644
index 00000000..f0d94783
--- /dev/null
+++ b/test/src/index/transport/testMobileOptions.js
@@ -0,0 +1,128 @@
+const assert = require('assert');
+const {describe} = require('mocha');
+const common = require('../../../common.js');
+const Nightwatch = require('../../../lib/nightwatch.js');
+const Appium = common.require('transport/selenium-webdriver/appium.js');
+const ChromeDriver = common.require('transport/selenium-webdriver/chrome.js');
+const SafariDriver = common.require('transport/selenium-webdriver/safari.js');
+
+describe('Test mobile options in Nightwatch/Appium client', function () {
+  it('have isIOS() for web testing on iOS', function () {
+    const client = Nightwatch.createClient({
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        browserName: 'Safari',
+        javascriptEnabled: true,
+        platformName: 'iOS',
+        platformVersion: '15.0',
+        deviceName: 'iPhone 13'
+      }
+    });
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof SafariDriver);
+    assert.strictEqual(client.api.isIOS(), true);
+    assert.strictEqual(client.api.isAndroid(), false);
+    assert.strictEqual(client.api.isMobile(), true);
+  });
+
+  it('have isAndroid() for web testing on Android', function () {
+    const client = Nightwatch.createClient({
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        browserName: 'Chrome',
+        javascriptEnabled: true,
+        platformName: 'Android',
+        platformVersion: '13.0',
+        deviceName: 'Google Pixel'
+      }
+    });
+
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof ChromeDriver);
+    assert.strictEqual(client.api.isIOS(), false);
+    assert.strictEqual(client.api.isAndroid(), true);
+    assert.strictEqual(client.api.isMobile(), true);
+  });
+
+  it('doesn\'t have isIOS(), isAndroid() and isMobile() for web testing on desktop', function () {
+    const client = Nightwatch.createClient({
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        browserName: 'Chrome'
+      }
+    });
+
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof ChromeDriver);
+    assert.strictEqual(client.api.isIOS(), false);
+    assert.strictEqual(client.api.isAndroid(), false);
+    assert.strictEqual(client.api.isMobile(), false);
+  });
+
+  it('have isIOS() for native testing on iOS', function () {
+    const client = Nightwatch.createClient({
+      selenium: {
+        use_appium: true
+      },
+      desiredCapabilities: {
+        browserName: null,
+        platformName: 'iOS',
+        platformVersion: '15.0',
+        deviceName: 'iPhone 13'
+      }
+    });
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.api.isIOS(), true);
+    assert.strictEqual(client.api.isAndroid(), false);
+    assert.strictEqual(client.api.isMobile(), true);
+  });
+
+  it('have isAndroid() for native testing on Android', function () {
+    const client = Nightwatch.createClient({
+      selenium: {
+        use_appium: true
+      },
+      desiredCapabilities: {
+        browserName: '',
+        platformName: 'Android'
+      }
+    });
+
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.api.isIOS(), false);
+    assert.strictEqual(client.api.isAndroid(), true);
+    assert.strictEqual(client.api.isMobile(), true);
+  });
+
+  it('have isAndroid() for web testing on Android using Appium', function () {
+    const client = Nightwatch.createClient({
+      selenium: {
+        use_appium: true
+      },
+      desiredCapabilities: {
+        browserName: 'Chrome',
+        platformName: 'Android'
+      }
+    });
+
+    client.transport.createSessionOptions();
+
+    assert.ok(client.transport instanceof Appium);
+    assert.strictEqual(client.api.isIOS(), false);
+    assert.strictEqual(client.api.isAndroid(), true);
+    assert.strictEqual(client.api.isMobile(), true);
+  });
+});
diff --git a/test/src/index/transport/testMobileSupport.js b/test/src/index/transport/testMobileSupport.js
deleted file mode 100644
index 30581689..00000000
--- a/test/src/index/transport/testMobileSupport.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const nock = require('nock');
-const assert = require('assert');
-const common = require('../../../common.js');
-const NightwatchClient = common.require('index.js');
-const Mobile = common.require('transport/selenium-webdriver/mobile-webdriver.js');
-
-
-describe('MobileTransport', function () {
-  it('test create Transport for Mobile',  function(){
-    const client = NightwatchClient.client({
-      webdriver: {
-        start_process: false
-      },
-      selenium: {
-        start_process: false,
-        host: 'localhost',
-        port: '4723'
-      },
-      output: true,
-      silent: false,
-      desiredCapabilities: {
-        browserName: null,
-        platformName: 'iOS',
-        platformVersion: '15.5',
-        deviceName: 'iPhone 13'
-      }
-    });
-
-    const {transport} = client;
-    assert.ok(transport instanceof Mobile);
-    assert.ok(client.settings.webdriver.host, 'localhost');
-    assert.ok(client.settings.webdriver.default_path_prefix, '/wd/hub');
-    assert.strictEqual(client.settings.webdriver.start_process, false);
-  });
-});
\ No newline at end of file
diff --git a/test/src/runner/testRunWithGlobalHooks.js b/test/src/runner/testRunWithGlobalHooks.js
index 8110aa7d..9f90ac7f 100644
--- a/test/src/runner/testRunWithGlobalHooks.js
+++ b/test/src/runner/testRunWithGlobalHooks.js
@@ -52,7 +52,7 @@ describe('testRunWithGlobalHooks', function() {
         assert.deepStrictEqual(globals.settings.selenium, {
           check_process_delay: 500,
           cli_args: {},
-          log_path: '',
+          log_path: './logs',
           max_status_poll_tries: 15,
           port: 10195,
           server_path: null,
diff --git a/test/src/runner/testRunWithTags.js b/test/src/runner/testRunWithTags.js
index c48a8951..fe222d8c 100644
--- a/test/src/runner/testRunWithTags.js
+++ b/test/src/runner/testRunWithTags.js
@@ -31,6 +31,24 @@ describe('testRunWithTags', function() {
     });
   });
 
+  it('testRunner with tags', function() {
+    const testsPath = path.join(__dirname, '../../sampletests');
+
+    return runTests({
+      _source: [testsPath],
+      tag: ['browser']
+    }, settings({
+      output: false,
+      disable_typescript: true,
+      globals: {
+        reporter(results) {
+          assert.strictEqual(Object.keys(results.modules).length, 1);
+          assert.ok('demoTagTest' in results.modules[`tagswithbrowserobject${path.sep}sample`].completed);
+        }
+      }
+    }));
+  });
+
   it('testRunWithTags', function() {
     let testsPath = path.join(__dirname, '../../sampletests');
 
diff --git a/test/src/service-builders/testAppiumServer.js b/test/src/service-builders/testAppiumServer.js
new file mode 100644
index 00000000..5f08040e
--- /dev/null
+++ b/test/src/service-builders/testAppiumServer.js
@@ -0,0 +1,340 @@
+const assert = require('assert');
+const mockery = require('mockery');
+const path = require('path');
+const common = require('../../common.js');
+const NightwatchClient = common.require('index.js');
+const Settings = common.require('settings/settings.js');
+
+describe('AppiumServer Transport Tests', function () {
+  beforeEach(function() {
+    mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false});
+  });
+
+  afterEach(function(){
+    mockery.deregisterAll();
+    mockery.disable();
+    mockery.resetCache();
+  });
+
+  const sessionData = {
+    sessionId: '111',
+    getId() {
+      return '1111';
+    },
+    getCapabilities() {
+      return {
+        getPlatform() {
+          return 'MAC';
+        },
+        getBrowserName() {
+          return 'chrome';
+        },
+        getBrowserVersion() {
+          return '11';
+        },
+        get(name) {
+          return name;
+        },
+        keys() {
+          return new Map();
+        }
+      };
+    }
+  };
+
+  const mockDriver = {
+    session_: Promise.resolve(),
+    async getSession() {
+      return sessionData;
+    },
+    async getExecutor() {
+      return {
+        w3c: true
+      };
+    }
+  };
+
+  const fn = function() {};
+  function deleteFromRequireCache(location) {
+    const entry = Object.keys(require.cache).filter(item => {
+      return item.includes(location);
+    });
+
+    entry.forEach(item => {
+      delete require.cache[item];
+    });
+  }
+
+  function mockHttpClient({onServerUrl = fn}) {
+    function httpClient() {
+      return class HttpClient {
+        constructor(serverUrl) {
+          onServerUrl(serverUrl);
+        }
+      };
+    }
+
+    mockery.registerMock('./httpclient.js', httpClient);
+  }
+
+  function mockWebDriver({onSessionOpts = fn}) {
+    const SeleniumWebdriver = require('selenium-webdriver');
+    class MockWebDriver extends SeleniumWebdriver.WebDriver {
+      static createSession(executor, options) {
+        onSessionOpts(options);
+
+        return Promise.resolve(mockDriver);
+      }
+    }
+
+    deleteFromRequireCache('node_modules/selenium-webdriver');
+
+    mockery.registerMock('selenium-webdriver', {
+      ...SeleniumWebdriver,
+      WebDriver: MockWebDriver
+    });
+  }
+
+
+  function mockServiceBuilder({onConstruct = fn, onOpts = fn, onSetPort = fn}) {
+    const remote = require('selenium-webdriver/remote');
+    const {Options, SeleniumServer} = remote;
+
+    deleteFromRequireCache('node_modules/selenium-webdriver/remote');
+
+    class MockDriverService {
+      constructor(command, opts) {
+        onConstruct.call(this, command);
+
+        if (opts.port) {
+          onSetPort(opts.port);
+        }
+
+        onOpts(opts);
+      }
+
+      async start() {
+        return {};
+      }
+    }
+
+    mockery.registerMock('selenium-webdriver/remote', {
+      Options,
+      SeleniumServer,
+      DriverService: MockDriverService
+    });
+  }
+
+  async function AppiumServerTestSetup(useSettings, {argv = {}, onLogFile = fn} = {}) {
+    const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
+    class MockBaseService extends BaseService {
+      constructor(settings) {
+        super(settings);
+
+        this.setOutputFile('');
+
+        assert.strictEqual(this.serviceName, 'Appium Server');
+        assert.strictEqual(this.outputFile, '_appium-server.log');
+        assert.strictEqual(this.defaultPort, 4723);
+        assert.strictEqual(this.npmPackageName, 'appium');
+        assert.strictEqual(this.serviceDownloadUrl, '');
+        assert.strictEqual(this.downloadMessage, 'install Appium globally with "npm i -g appium" command, \n and set "selenium.server_path" config option to "appium".');
+      }
+      needsSinkProcess() {
+        return true;
+      }
+      hasSinkSupport() {
+        return true;
+      }
+      async writeLogFile() {
+      }
+      async createSinkProcess() {
+        this.process = {
+          kill: () => {}
+        };
+        const logPath = this.getLogPath();
+        if (!logPath) {
+          return true;
+        }
+
+        const filePath = this.getOutputFilePath();
+        onLogFile(filePath);
+      }
+    }
+
+    deleteFromRequireCache('transport/selenium-webdriver/service-builders/base-service.js');
+    mockery.registerMock('./base-service.js', MockBaseService);
+
+    let command;
+    let serverPort;
+    let serverUrl;
+    let logFilePath;
+    let buildOptions;
+    let sessionOptions;
+
+    mockServiceBuilder({
+      onConstruct(cmd) {
+        command = cmd;
+      },
+
+      onSetPort(p) {
+        serverPort = p;
+      },
+
+      onOpts(o) {
+        buildOptions = o;
+      }
+    });
+
+    mockHttpClient({
+      onServerUrl(url) {
+        serverUrl = url;
+      }
+    });
+
+    mockWebDriver({
+      onSessionOpts(opts) {
+        sessionOptions = opts;
+      }
+    });
+
+    const settings = Settings.parse(useSettings);
+
+    const client = NightwatchClient.client(settings, null, {});
+    const {transport} = client;
+
+    const session = await transport.createSession({argv, moduleKey: 'testModuleKey'});
+
+    return {
+      session,
+      command,
+      serverPort,
+      serverUrl,
+      buildOptions,
+      sessionOptions
+    };
+  }
+
+  it('test create session with Appium', async function() {
+    let logFilePath;
+    const {session, command, serverPort, buildOptions, sessionOptions} = await AppiumServerTestSetup({
+      desiredCapabilities: {
+        browserName: '',
+        platformName: 'ios',
+        'appium:options': {
+          automationName: 'XCUITest',
+          app: 'samples/sample.app'
+        }
+      },
+      selenium: {
+        port: 9999,
+        use_appium: true,
+        start_process: true,
+        // server_path is only set automatically when server is started from cli.js
+        server_path: '/path/to/appium/main.js'
+      }
+    }, {
+      argv: {
+        deviceId: '00008030-00024C2C3453402E'
+      },
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    assert.strictEqual(command, 'node');
+    assert.strictEqual(serverPort, 9999);
+    assert.strictEqual(buildOptions.port, 9999);
+    assert.strictEqual(buildOptions.path, '/wd/hub');
+    assert.deepStrictEqual(buildOptions.args, ['/path/to/appium/main.js', '--port', 9999]);
+    assert.deepStrictEqual(buildOptions.stdio, ['pipe', undefined, undefined]);
+    assert.strictEqual(sessionOptions.browserName, '');
+    assert.strictEqual(sessionOptions.platformName, 'ios');
+    assert.strictEqual(sessionOptions['appium:automationName'], 'XCUITest');
+    assert.strictEqual(sessionOptions['appium:app'], 'samples/sample.app');
+    assert.strictEqual(sessionOptions['appium:udid'], '00008030-00024C2C3453402E');
+    assert.strictEqual('appium:options' in sessionOptions, false);
+    assert.ok(logFilePath.endsWith('testModuleKey_appium-server.log'));
+  });
+
+  it('test create session with globally installed appium 2 server', async function() {
+    mockery.registerMock('chromedriver', {
+      path: '/path/to/chromedriver'
+    });
+
+    let logFilePath;
+    const {command, serverPort, buildOptions, sessionOptions} = await AppiumServerTestSetup({
+      desiredCapabilities: {
+        browserName: null,
+        'appium:options': {
+          app: './sample.apk',
+          chromedriverExecutable: ''
+        }
+      },
+      selenium: {
+        start_process: true,
+        use_appium: true,
+        server_path: 'appium',
+        cli_args: [
+          '--allow-insecure=chromedriver_autodownload'
+        ],
+        default_path_prefix: ''
+      }
+    }, {
+      argv: {
+        deviceId: 'ZD2222W62Y'
+      },
+      onLogFile(filePath) {
+        logFilePath = filePath;
+      }
+    });
+
+    assert.strictEqual(command, 'appium');
+    assert.strictEqual(serverPort, 4723);
+    assert.strictEqual(buildOptions.path, '');
+    assert.deepStrictEqual(buildOptions.args, ['--allow-insecure=chromedriver_autodownload']);
+    assert.deepStrictEqual(buildOptions.stdio, ['pipe', undefined, undefined]);
+    assert.strictEqual(sessionOptions.browserName, null);
+    assert.strictEqual(sessionOptions['appium:app'], './sample.apk');
+    assert.strictEqual(sessionOptions['appium:chromedriverExecutable'], '/path/to/chromedriver');
+    assert.strictEqual(sessionOptions['appium:udid'], 'ZD2222W62Y');
+    assert.strictEqual('appium:options' in sessionOptions, false);
+    assert.ok(logFilePath.endsWith('testModuleKey_appium-server.log'));
+  });
+
+  it('test create session with appium server -- externally managed', async function() {
+    const {session, command, serverPort, serverUrl, buildOptions, sessionOptions} = await AppiumServerTestSetup({
+      desiredCapabilities: {
+        browserName: 'firefox',
+        platformName: 'android',
+        'appium:automationName': 'UiAutomator2'
+
+      },
+      selenium: {
+        host: 'somewhere',
+        port: '4725',
+        start_process: false,
+        use_appium: true
+      }
+    });
+
+    assert.deepStrictEqual(session, {
+      sessionId: '1111', capabilities: {}
+    });
+    // service not created
+    assert.ok(!serverPort);
+    assert.ok(!command);
+    assert.ok(!buildOptions);
+    // session assertions
+    assert.strictEqual(serverUrl, 'http://somewhere:4725');
+    // sessionOptions is a FirefoxOptions instance
+    assert.strictEqual(sessionOptions.browserName, undefined);
+    assert.strictEqual(sessionOptions.getBrowserName(), 'firefox');
+    assert.strictEqual(sessionOptions.getPlatform(), 'android');
+    assert.strictEqual(sessionOptions.get('appium:automationName'), 'UiAutomator2');
+    assert.strictEqual(sessionOptions.has('appium:options'), false);
+  });
+});