Repository: nightwatchjs/nightwatch
Pull request #3519 (2022-12-13 18:36:40)

Improve Appium support in Nightwatch
This PR does the following:
* Expects users to add `use_appium` property in `selenium` setting in the Nightwatch configuration when using Appium server.
* ~Expects user to add `appium_version` property like above if using Appium v2. (Support not complete, also need to decide how the users would use this property, i.e., would they just add `1` or `2`, or the complete version number?)~
* Automatically starts the Appium server on the specified port.
* Can use a globally installed Appium package instead of a locally installed one by passing the command as `server_path`. For example, to use global installation of appium, set `server_path` to `appium`.
* Works fine with Appium v2 as well, if `default_path_prefix` is set to `''` explicitely.


TODOs:
- [ ] Check whether BrowserStack App Automate still works correctly.
- [x] Figure out why `browserName=null` was required earlier.
- [ ] ~Automatically set `defaultPathPrefix` correctly based on which Appium version is being used.~
- [ ] ~Parse Appium version to `1` or `2` based on the possible values that can be set by user in `appium_version` property.~
- [x] ~Convert relative path values of `appium:` properties to absolute paths in case Appium v2 is being used.~
- [x] Automatically set `appium:chromedriverExecutable` when not passed, if testing on Android.

Topperfalkon: Regarding "why browsername=null was needed earlier", I might be able to provide additional context.

I introduced some checking logic a while back for NW 1.7: https://github.com/nightwatchjs/nightwatch/pull/2882

I _suspect_ this was to allow support for older versions of Appium that I guess Browserstack was using by default at the time? It's been a fair while since I've looked at this though, so that might be the limit of my memory :)
garg3133: Thanks @Topperfalkon, your comment was helpful. It helped me realize that `browserName=null` was required earlier to work with Appium just because `browserName` defaults to `firefox` if not passed, which causes an error in Appium that both `browserName` and `appPackage` cannot be defined together. And if set to something not a browser, Nightwatch used to throw an error.

But with the current change, users will be able to set `browserName` to `null` or `''` for doing native app testing and to any browser for running tests on mobile browsers. But not passing `browserName` altogether would still result in it being set to `firefox` by default, so they would need to set it to something.
garg3133: Everything is done in this PR now according to me. There are just two issues with BrowserStack that are left:
* Since `browserstack.js` extends `appium..js`, `createSessionOptions()` is also being overridden for BrowserStack, which shouldn't be the case for BrowserStack Automate atleast.
* `isMobile()` API method makes BrowserStack use app-automate even for mobile-web testing, while mobile-web testing should be performed on BrowserStack Automate.
garg3133: Commit [e000786](https://github.com/nightwatchjs/nightwatch/pull/3519/commits/e000786fc403ddaf6f9bd79b25ce0549da4e8880) contains my approach for solving the BrowserStack related issues and commit [1167538](https://github.com/nightwatchjs/nightwatch/pull/3519/commits/1167538dd52740915ea97e285a1454ce54662102) contains Ravi's approach for solving the problem.

I personally like my approach better for its simplicity but if we don't want to have `AppiumServer` as the parent class for BrowserStack and go forward with Ravi's approach, I'd still prefer to have a single class for BrowserStack and not three classes, because most of the things in Automate and AppAutomate are still the same (we would still want to use Appium's `createDriver` method in Automate to test on browsers not supported by Selenium, and use part of Appium's `createSessionOptions` since testing mobile-web on Automate still happens using Appium).
gravityvi: It's better to have different classes rather than hiding every functionality under one class. It will help us in the future when adding additional capabilities which are specific to app-automate. Technically we should use selenium driver instance created from the builder for automate sessions in BrowserStack.
garg3133: Makes sense!
gravityvi: I think for browserstack classes we can create a separate folder and rename it automate and AppAutomate respectively

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/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/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/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/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/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);
+  });
+});