Repository: nightwatchjs/nightwatch
Pull request #3565 (2023-01-11 08:44:42)

Disable parallelism when running tests on safari browser
### Changes
  Disabling parallelism when running tests on safari browser on localhost



swrdfish: @prudhvi22 what happens when we try to run tests in parallel in multiple environments, and include safari as one of those environments?

```npx nightwatch tests/ -e chrome,safari --parallel```
prudhvi22: @swrdfish It will execute chrome tests in parallel and safari tests in serial mode. But i found there is an issue if we run in multiple environments with safari being on them( parallelism being disabled for safari). The reporter is showing results from safari environment only. I am looking into that.
prudhvi22: After having discussion with @swrdfish, below changes are made.

- npx nightwatch -e safari --parallel ( tests will run in serial mode)
- npx nightwatch -e safari,chrome --parallel ( tests from both environments will run in serial mode)
beatfactor: > After having discussion with @swrdfish, below changes are made.
> 
> * npx nightwatch -e safari --parallel ( tests will run in serial mode)
> * npx nightwatch -e safari,chrome --parallel ( tests from both environments will run in serial mode)

Can you add a warning that says explains the situation for the second case? 

Patch:
diff --git a/cucumber-js/_setup_cucumber_runner.js b/cucumber-js/_setup_cucumber_runner.js
index 62553828..0ed0c506 100644
--- a/cucumber-js/_setup_cucumber_runner.js
+++ b/cucumber-js/_setup_cucumber_runner.js
@@ -40,6 +40,7 @@ Before(function({pickle}) {
     webdriver,
     persist_globals,
     config: this.parameters.config,
+    test_settings: this.parameters.settings,
     globals
   });
 
diff --git a/lib/api/client-commands/registerBasicAuth.js b/lib/api/client-commands/registerBasicAuth.js
index 032c25b3..47888e92 100644
--- a/lib/api/client-commands/registerBasicAuth.js
+++ b/lib/api/client-commands/registerBasicAuth.js
@@ -3,6 +3,7 @@ const {Logger} = require('../../utils');
 
 /**
  * Automate the input of basic auth credentials whenever they arise.
+ * This feature is currently implemented on top of Selenium 4’s CDP(Chrome DevTools Protocol) support, and so only works on those browser that support that protocol
  *
  * @example
  *  this.demoTest = function (browser) {
diff --git a/lib/core/client.js b/lib/core/client.js
index 4e59c3e2..f6d361d7 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -14,7 +14,7 @@ const Factory = require('../transport/factory.js');
 const {isAndroid, isIos} = require('../utils/mobile');
 
 const {LocateStrategy, Locator} = Element;
-const {Logger, isUndefined, isDefined, isObject, isFunction} = Utils;
+const {Logger, isUndefined, isDefined, isObject, isFunction, isSafari} = Utils;
 
 class NightwatchAPI {
   get WEBDRIVER_ELEMENT_ID() {
@@ -93,7 +93,7 @@ class NightwatchAPI {
   }
 
   isSafari() {
-    return this.__isBrowserName(Browser.SAFARI);
+    return isSafari(this.desiredCapabilities);
   }
 
   isChrome() {
diff --git a/lib/element/appium-locator.js b/lib/element/appium-locator.js
index 6caf9e06..91539bbc 100644
--- a/lib/element/appium-locator.js
+++ b/lib/element/appium-locator.js
@@ -1,7 +1,7 @@
 const {By, RelativeBy} = require('selenium-webdriver');
 
 const LocateElement = require('./locator.js');
-const {AVAILABLE_LOCATORS} = LocateElement;
+const LocateStrategy = require('./strategy.js');
 
 class AppiumLocator extends LocateElement {
   /**
@@ -20,11 +20,9 @@ class AppiumLocator extends LocateElement {
 
     const elementInstance = LocateElement.createElementInstance(element);
 
-    if (elementInstance.locateStrategy === 'id') {
-      return new By(elementInstance.locateStrategy, elementInstance.selector);
-    }
+    LocateStrategy.validate(elementInstance.locateStrategy);
 
-    return By[AVAILABLE_LOCATORS[elementInstance.locateStrategy]](elementInstance.selector);
+    return new By(elementInstance.locateStrategy, elementInstance.selector);
   }
 }
 
diff --git a/lib/element/locator-factory.js b/lib/element/locator-factory.js
new file mode 100644
index 00000000..b5532fa0
--- /dev/null
+++ b/lib/element/locator-factory.js
@@ -0,0 +1,21 @@
+const {By, RelativeBy} = require('selenium-webdriver');
+
+const Locator = require('./locator.js');
+const AppiumLocator = require('./appium-locator.js');
+
+class NightwatchLocator {
+  /**
+   * @param {object|string} element
+   * @param {boolean} isAppiumClient
+   * @return {By|RelativeBy}
+   */
+  static create(element, isAppiumClient) {
+    if (isAppiumClient) {
+      return AppiumLocator.create(element);
+    }
+
+    return Locator.create(element);
+  }
+}
+
+module.exports = NightwatchLocator;
diff --git a/lib/element/locator.js b/lib/element/locator.js
index bd5dd56a..089594db 100644
--- a/lib/element/locator.js
+++ b/lib/element/locator.js
@@ -337,4 +337,3 @@ class NoSuchElementError extends Error {
 
 module.exports = LocateElement;
 module.exports.NoSuchElementError = NoSuchElementError;
-module.exports.AVAILABLE_LOCATORS = AVAILABLE_LOCATORS;
diff --git a/lib/element/strategy.js b/lib/element/strategy.js
index b768f6bc..27e5110c 100644
--- a/lib/element/strategy.js
+++ b/lib/element/strategy.js
@@ -5,10 +5,17 @@ class LocateStrategy {
     return {
       ID: 'id',
       CSS_SELECTOR: 'css selector',
-      LINK_TEST: 'link text',
+      LINK_TEXT: 'link text',
       PARTIAL_LINK_TEXT: 'partial link text',
       TAG_NAME: 'tag name',
-      XPATH: 'xpath'
+      XPATH: 'xpath',
+      NAME: 'name',
+      CLASS_NAME: 'class name',
+      // Appium-specific strategies
+      ACCESSIBILITY_ID: 'accessibility id',
+      ANDROID_UIAUTOMATOR: '-android uiautomator',
+      IOS_PREDICATE_STRING: '-ios predicate string',
+      IOS_CLASS_CHAIN: '-ios class chain'
     };
   }
 
diff --git a/lib/index.js b/lib/index.js
index 7b47a18e..0f452e54 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,4 +1,5 @@
 const {By, Key, Capabilities} = require('selenium-webdriver');
+const lodashMerge = require('lodash.merge');
 const Utils = require('./utils');
 const Settings = require('./settings/settings.js');
 const ElementGlobal = require('./api/_loaders/element-global.js');
@@ -40,7 +41,8 @@ module.exports.createClient = function({
   devtools = false,
   debug = false,
   enable_global_apis = false,
-  config = './nightwatch.json'
+  config = './nightwatch.json',
+  test_settings
 } = {}) {
   if (browserName && !env) {
     switch (browserName) {
@@ -102,6 +104,10 @@ module.exports.createClient = function({
   });
 
   cliRunner.test_settings.disable_global_apis = cliRunner.test_settings.disable_global_apis || !enable_global_apis;
+  
+  //merge settings recieved from testsuite to cli runner settings (this might have changed in hooks) 
+  lodashMerge(cliRunner.test_settings, test_settings);
+  
   const client = Nightwatch.client(cliRunner.test_settings, reporter, cliRunner.argv, true);
 
   Object.defineProperty(Nightwatch, 'element', {
@@ -181,6 +187,16 @@ Nightwatch.cli = function(callback) {
   const ArgvSetup = require('./runner/cli/argv-setup.js');
   const {argv} = ArgvSetup;
 
+  if (argv['list-files']) {
+    argv._source = argv['_'].slice(0);
+    const runner = Nightwatch.CliRunner(argv);
+
+    return runner.setupAsync()
+      .then(() => runner.getTestsFiles())
+      // eslint-disable-next-line no-console
+      .then((result) => console.log(JSON.stringify(result)));
+  }
+
   if (argv.help) {
     ArgvSetup.showHelp();
   } else if (argv.info) {
@@ -201,14 +217,6 @@ Nightwatch.cli = function(callback) {
     ).then(console.log);
   } else if (argv.version) {
     Utils.printVersionInfo();
-  } else if (argv['list-files']) {
-    argv._source = argv['_'].slice(0);
-    const runner = Nightwatch.CliRunner(argv);
-    
-    runner.setupAsync()
-      .then(() => runner.getTestsFiles())
-      // eslint-disable-next-line no-console
-      .then((result) => console.log(Logger.inspectObject(result)));
   } else {
     if (!Utils.isFunction(callback)) {
       throw new Error('Supplied callback argument needs to be a function.');
diff --git a/lib/reporter/global-reporter.js b/lib/reporter/global-reporter.js
index 74cf40dd..a01b8896 100644
--- a/lib/reporter/global-reporter.js
+++ b/lib/reporter/global-reporter.js
@@ -38,8 +38,8 @@ module.exports = class GlobalReporter {
     this.reporterFile = reporter;
     this.reportFileName = reportFileName;
 
-    if (!Array.isArray(this.reporterFile)) {
-      this.reporterFile = [reporter];
+    if (!Array.isArray(this.reporterFile) && typeof this.reporterFile == 'string') {
+      this.reporterFile = this.reporterFile.split(',');
     }
     this.settings = settings;
     this.summary = new Summary(settings);
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index 20c05226..b15baf8c 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -9,7 +9,7 @@ const Utils = require('../../utils');
 const Runner = require('../runner.js');
 const ProcessListener = require('../process-listener.js');
 const analyticsCollector = require('../../utils/analytics.js');
-const {Logger, singleSourceFile} = Utils;
+const {Logger, singleSourceFile, isSafari, isLocalhost} = Utils;
 const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator} = require('../../utils/mobile.js');
 
 class CliRunner {
@@ -390,7 +390,10 @@ class CliRunner {
     }
 
     for (const env of this.testEnvArray) {
-      if (isMobile(this.testEnvSettings[env].desiredCapabilities)) {
+      const {webdriver = {}} = this.testEnvSettings[env];
+      const desiredCapabilities = this.testEnvSettings[env].capabilities || this.testEnvSettings[env].desiredCapabilities;
+      
+      if (isMobile(desiredCapabilities)) {
         
         if  (Concurrency.isChildProcess()) {
           Logger.info('Disabling parallelism while running tests on mobile platform');
@@ -398,6 +401,16 @@ class CliRunner {
         
         return false;
       }
+
+      if (isSafari(desiredCapabilities) && isLocalhost(webdriver)) {
+        this.isSafariEnvPresent = true;
+        if (Concurrency.isMasterProcess()) {
+          // eslint-disable-next-line no-console
+          console.warn('Running tests in parallel is not supported in Safari. Tests will run in serial mode.');
+        }
+
+        return false;
+      }
     }
 
 
@@ -496,7 +509,7 @@ class CliRunner {
 
         return promise.then(() => {
           if (this.isConcurrencyEnabled()) {
-            return this.testRunner.runConcurrent(this.testEnvArray, modules, this.isTestWorkersEnabled())
+            return this.testRunner.runConcurrent(this.testEnvArray, modules, this.isTestWorkersEnabled(), this.isSafariEnvPresent)
               .then(exitCode => {
                 if (exitCode > 0) {
                   this.processListener.setExitCode(exitCode);
diff --git a/lib/runner/concurrency/index.js b/lib/runner/concurrency/index.js
index 6e0ffb0b..e1f625b4 100644
--- a/lib/runner/concurrency/index.js
+++ b/lib/runner/concurrency/index.js
@@ -6,7 +6,7 @@ const lodashClone = require('lodash.clone');
 const {Logger} = Utils;
 
 class Concurrency extends EventEmitter {
-  constructor(settings = {}, argv = {}, isTestWorkerEnabled) {
+  constructor(settings = {}, argv = {}, isTestWorkerEnabled, isSafariEnvPresent = false) {
     super();
 
     this.argv = argv;
@@ -14,6 +14,7 @@ class Concurrency extends EventEmitter {
     this.childProcessOutput = {};
     this.globalExitCode = 0;
     this.testWorkersEnabled = typeof isTestWorkerEnabled !== undefined ? isTestWorkerEnabled : settings.testWorkersEnabled;
+    this.isSafariEnvPresent = isSafariEnvPresent;
   }
 
   static getChildProcessArgs(envs) {
@@ -91,7 +92,11 @@ class Concurrency extends EventEmitter {
    */
   runTestEnvironments(envs, args, availColors) {
     return Promise.all(envs.map((environment, index) => {
-      let childProcess = this.createChildProcess(environment, args, ['--env', environment], index);
+      const extraArgs = ['--env', environment];
+      if (this.isSafariEnvPresent) {
+        extraArgs.push('--serial');
+      }
+      let childProcess = this.createChildProcess(environment, args, extraArgs, index);
 
       return this.runChildProcess(childProcess, environment + ' environment', availColors, 'envs');
     }));
diff --git a/lib/transport/selenium-webdriver/cdp.js b/lib/transport/selenium-webdriver/cdp.js
index 3d9e8886..d48a8a42 100644
--- a/lib/transport/selenium-webdriver/cdp.js
+++ b/lib/transport/selenium-webdriver/cdp.js
@@ -9,6 +9,19 @@ class Cdp {
 
   resetConnection() {
     this._connection = undefined;
+    this._networkMocks = undefined;
+  }
+
+  get networkMocks() {
+    if (this._networkMocks) {
+      return this._networkMocks;
+    }
+
+    return this._networkMocks = {};
+  }
+
+  addNetworkMock(url, response) {
+    this.networkMocks[url] = response;
   }
 }
 
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index 3a01e22a..13f4f729 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -1,6 +1,6 @@
 const {WebElement, WebDriver, Origin, By} = require('selenium-webdriver');
 const {Locator} = require('../../element');
-const AppiumLocator = require('../../element/appium-locator.js');
+const NightwatchLocator = require('../../element/locator-factory.js');
 const {isString} = require('../../utils');
 const fs = require('fs');
 const cdp = require('./cdp.js');
@@ -356,7 +356,7 @@ module.exports = class MethodMappings {
         // Elements
         ///////////////////////////////////////////////////////////
         async locateSingleElement(element) {
-          const locator = Locator.create(element);
+          const locator = NightwatchLocator.create(element, this.transport.api.isAppiumClient());
           const webElement = await this.driver.findElement(locator);
           const elementId = await webElement.getId();
 
@@ -368,12 +368,7 @@ module.exports = class MethodMappings {
         },
 
         async locateMultipleElements(element) {
-          let locator;
-          if (this.transport.api.isAppiumClient()) {
-            locator = AppiumLocator.create(element);
-          } else {
-            locator = Locator.create(element);
-          }
+          const locator = NightwatchLocator.create(element, this.transport.api.isAppiumClient());
           const resultValue = await this.driver.findElements(locator);
 
           if (Array.isArray(resultValue) && resultValue.length === 0) {
@@ -1110,36 +1105,43 @@ module.exports = class MethodMappings {
           const cdpConnection = await cdp.getConnection(this.driver);
 
           const {status=200, headers: headersObject, body: bodyPlain=''} = response;
-
           const headers = [];
           if (headersObject) {
             for (const [name, value] of Object.entries(headersObject)) {
               headers.push({name, value});
             }
           }
-
           // Convert body to base64
           const bodyBase64 = Buffer.from(bodyPlain, 'utf-8').toString('base64');
-          
-          cdpConnection._wsConnection.on('message', (message) => {
-            const params = JSON.parse(message);
-            if (params.method === 'Fetch.requestPaused') {
-              const requestPausedParams = params['params'];
-              if (requestPausedParams.request.url === urlToIntercept) {
-                cdpConnection.execute('Fetch.fulfillRequest', {
-                  requestId: requestPausedParams['requestId'],
-                  responseCode: status,
-                  responseHeaders: headers,
-                  body: bodyBase64
-                });
-              } else {
-                cdpConnection.execute('Fetch.continueRequest', {
-                  requestId: requestPausedParams['requestId']
-                });
+
+          cdp.addNetworkMock(urlToIntercept, {status, headers, body: bodyBase64});
+
+          // Add event listener only the first time.
+          if (Object.keys(cdp.networkMocks).length === 1) {
+            cdpConnection._wsConnection.on('message', (message) => {
+              const params = JSON.parse(message);
+              if (params.method === 'Fetch.requestPaused') {
+                const requestPausedParams = params['params'];
+                const requestUrl = requestPausedParams.request.url;
+
+                const networkMocks = cdp.networkMocks;
+                if (Object.keys(networkMocks).includes(requestUrl)) {
+                  const mockResponse = networkMocks[requestUrl];
+                  cdpConnection.execute('Fetch.fulfillRequest', {
+                    requestId: requestPausedParams['requestId'],
+                    responseCode: mockResponse.status,
+                    responseHeaders: mockResponse.headers,
+                    body: mockResponse.body
+                  });
+                } else {
+                  cdpConnection.execute('Fetch.continueRequest', {
+                    requestId: requestPausedParams['requestId']
+                  });
+                }
               }
-            }
-          });
-      
+            });
+          }
+
           await cdpConnection.execute(
             'Fetch.enable',
             {}
diff --git a/lib/utils/analytics.js b/lib/utils/analytics.js
index c5e2c300..974bd607 100644
--- a/lib/utils/analytics.js
+++ b/lib/utils/analytics.js
@@ -44,7 +44,6 @@ class AnalyticsCollector {
     this.queueLength = 0;
     this.parameters = {};
     this.logger = Logger;
-    this.initialized = false;
 
     // This identifies unique users and helps us separate out sessions.
     this.parameters['client_id'] = uuid.v4();
@@ -53,6 +52,25 @@ class AnalyticsCollector {
     this.parameters['non_personalized_ads'] = true;
   }
 
+  initialize() {
+    if (this.initialized) {return this.initialized}
+
+    this.initialized = new Promise((resolve) => {
+      // update queueLength
+      fs.stat(this.__getLogFileLocation())
+        .then((length) => {
+          this.queueLength = length.size / 400; // the actual value doesn't matter
+          resolve();
+        })
+        .catch((err)=>{
+          // ignore error
+          resolve();
+        });
+    });
+
+    return this.initialized;
+  }
+
   updateSettings(settings) {
     this.settings = settings;
 
@@ -62,18 +80,6 @@ class AnalyticsCollector {
 
     // update client_id
     this.parameters['client_id'] = this.settings.usage_analytics.client_id;
-
-    // update queueLength
-    if (this.settings.usage_analytics.enabled) {
-      try {
-        const stat = fs.statSync(this.__getLogFileLocation());
-        this.queueLength = stat.size / 400; // the actual value doesn't matter
-      } catch (err) {
-        // ignore
-      }
-    }
-
-    this.initialized = true;
   }
 
   updateLogger(logger) {
@@ -85,9 +91,7 @@ class AnalyticsCollector {
       return;
     }
 
-    if (!this.initialized) {
-      throw Error('AnalyticsCollector is not initialized');
-    }
+    await this.initialize();
 
     // Not all shape of events are supported.
     this.__validateEvent(name, parameters);
@@ -108,9 +112,7 @@ class AnalyticsCollector {
   }
  
   async exception(error) {
-    if (!this.initialized) {
-      throw Error('AnalyticsCollector is not initialized');
-    }
+    await this.initialize();
 
     if (!error) {
       return;
diff --git a/lib/utils/index.js b/lib/utils/index.js
index b7a7250e..0cf34c77 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -3,7 +3,7 @@ const fs = require('fs');
 const mkpath = require('mkpath');
 const glob = require('glob');
 const lodashMerge = require('lodash.merge');
-const {By} = require('selenium-webdriver');
+const {By, Capabilities} = require('selenium-webdriver');
 
 const Logger = require('./logger');
 const BrowserName = require('./browsername.js');
@@ -605,6 +605,22 @@ class Utils {
       throw err;
     }
   }
+
+  static isSafari(desiredCapabilities = {}) {
+    const browserName = desiredCapabilities.browserName || (desiredCapabilities instanceof Capabilities && desiredCapabilities.getBrowserName());
+
+    if (browserName && browserName.toLowerCase() === 'safari') {
+      return true;
+    }
+
+    return false;
+  }
+
+  static isLocalhost(webdriver = {}) {
+    const {host} = webdriver;
+    
+    return ['127.0.0.1', 'localhost'].indexOf(host) > -1;
+  }
 }
 
 lodashMerge(Utils, {


Test patch:
diff --git a/lib/runner/test-runners/cucumber.js b/lib/runner/test-runners/cucumber.js
index 01a7b921..8fb18a69 100644
--- a/lib/runner/test-runners/cucumber.js
+++ b/lib/runner/test-runners/cucumber.js
@@ -98,7 +98,7 @@ class CucumberSuite extends TestSuite {
     const {feature_path = ''} = options;
     const parallelArgs = this.usingCucumberWorkers ? ['--parallel', this.usingCucumberWorkers]: [];
     const additionalOptions = this.buildArgvValue(['tags', 'retry-tag-filter', 'profile', 'format', 'format-options', 'dry-run', 'fail-fast', ['retry', 'retries'], 'no-strict', 'name']);
-    const extraParams = ['--world-parameters', JSON.stringify(this.argv)];
+    const extraParams = ['--world-parameters', JSON.stringify({...this.argv, settings: this.settings})];
 
     return [
       process.execPath,
diff --git a/lib/runner/test-runners/default.js b/lib/runner/test-runners/default.js
index 2b0f3f17..a2fcd8c6 100644
--- a/lib/runner/test-runners/default.js
+++ b/lib/runner/test-runners/default.js
@@ -138,8 +138,8 @@ class DefaultRunner {
    * @param {Array} modules
    * @return {Promise<number>}
    */
-  async runConcurrent(testEnvArray, modules, isTestWorkerEnabled) {
-    this.concurrency = new Concurrency(this.settings, this.argv, isTestWorkerEnabled);
+  async runConcurrent(testEnvArray, modules, isTestWorkerEnabled, isSafariEnvPresent) {
+    this.concurrency = new Concurrency(this.settings, this.argv, isTestWorkerEnabled, isSafariEnvPresent);
     this.globalReporter.setupChildProcessListener(this.concurrency);
 
     const exitCode = await this.concurrency.runMultiple(testEnvArray, modules);
diff --git a/lib/utils/locatestrategy.js b/lib/utils/locatestrategy.js
index 405d8cf4..65e8862a 100644
--- a/lib/utils/locatestrategy.js
+++ b/lib/utils/locatestrategy.js
@@ -5,10 +5,17 @@ class LocateStrategy {
     return {
       ID: 'id',
       CSS_SELECTOR: 'css selector',
-      LINK_TEST: 'link text',
+      LINK_TEXT: 'link text',
       PARTIAL_LINK_TEXT: 'partial link text',
       TAG_NAME: 'tag name',
-      XPATH: 'xpath'
+      XPATH: 'xpath',
+      NAME: 'name',
+      CLASS_NAME: 'class name',
+      // Appium-specific strategies
+      ACCESSIBILITY_ID: 'accessibility id',
+      ANDROID_UIAUTOMATOR: '-android uiautomator',
+      IOS_PREDICATE_STRING: '-ios predicate string',
+      IOS_CLASS_CHAIN: '-ios class chain'
     };
   }
 
diff --git a/test/apidemos/appium/appiumTest.js b/test/apidemos/appium/appiumTest.js
index 059fd0f8..2b5e253a 100644
--- a/test/apidemos/appium/appiumTest.js
+++ b/test/apidemos/appium/appiumTest.js
@@ -31,7 +31,9 @@ describe('appium api demo', function () {
 
   it('Search for Nightwatch', async function() {
     app  // available globally
-      .click('id', 'com.app:id/search')
+      .waitForElementPresent({selector: 'Search Wikipedia', locateStrategy: 'accessibility id'})
+      .click('accessibility id', 'Search Wikipedia')
+      .element('class name', 'android.widget.ImageButton')
       .sendKeys('id', 'com.app:id/search', 'Nightwatch');
   });
 });
diff --git a/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/plugin/index.js b/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/plugin/index.js
new file mode 100644
index 00000000..e69de29b
diff --git a/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/plugin/nightwatch/globals.js b/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/plugin/nightwatch/globals.js
new file mode 100644
index 00000000..618c2ab1
--- /dev/null
+++ b/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/plugin/nightwatch/globals.js
@@ -0,0 +1,5 @@
+module.exports = {
+  before(settings) {
+    settings.desiredCapabilities.browserName = 'chrome';
+  }
+};
\ No newline at end of file
diff --git a/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/testSample.js b/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/testSample.js
new file mode 100644
index 00000000..1df1493c
--- /dev/null
+++ b/test/cucumber-integration-tests/sample_cucumber_tests/with-modified-settings/testSample.js
@@ -0,0 +1,31 @@
+const assert = require('assert');
+const {Given, Then, After} = require('@cucumber/cucumber');
+
+Given('I navigate to localhost', function() {
+  browser.globals.test_calls = 0;
+  browser.globals.test_calls++;
+
+  return browser.navigateTo('http://localhost');
+});
+
+Then('I check if webdriver is present', function() {
+  browser.globals.test_calls++;
+
+  return browser.assert.elementPresent('#webdriver');
+});
+
+Then('I check if webdriver is present and contains text', async function() {
+  browser.globals.test_calls++;
+
+  await browser.expect.element('#webdriver').text.to.contain('xx')
+});
+
+Then('I check if badElement is present', function() {
+  browser.globals.test_calls++;
+
+  return browser.assert.elementPresent('#badElement');
+});
+
+After(function() {
+  assert.strictEqual(browser.globals.test_calls, 2);
+});
\ No newline at end of file
diff --git a/test/cucumber-integration-tests/testCucumberTestsWithModifiedSettings.js b/test/cucumber-integration-tests/testCucumberTestsWithModifiedSettings.js
new file mode 100644
index 00000000..140712d9
--- /dev/null
+++ b/test/cucumber-integration-tests/testCucumberTestsWithModifiedSettings.js
@@ -0,0 +1,57 @@
+const path = require('path');
+const assert = require('assert');
+const Globals = require('../lib/globals/commands.js');
+const common = require('../common.js');
+const MockServer = require('../lib/mockserver.js');
+const {runTests} = common.require('index.js');
+
+describe('Cucumber integration - with modified settings in plugin', function() {
+  beforeEach(function(done) {
+    this.server = MockServer.init(null, {port: 10195});
+    this.server.on('listening', () => done());
+  });
+
+  afterEach(function(done) {
+    Globals.afterEach.call(this, done);
+  });
+
+  it('testCucumberSampleTests with browsername modified (chrome) in plugin global hook', function() {
+    const source = [path.join(__dirname, './sample_cucumber_tests/with-modified-settings/testSample.js')];
+
+
+    const pluginPath = [path.join(__dirname, './sample_cucumber_tests/with-modified-settings/plugin')];
+
+    MockServer.addMock({
+      url: '/wd/hub/session',
+      method: 'POST',
+      statusCode: 201,
+      postdata: {
+        capabilities: {
+          firstMatch: [{}],
+          alwaysMatch: {browserName: 'chrome', 'goog:chromeOptions': {}}
+        }
+      },
+      response: {
+        status: 0,
+        sessionId: '1352110219202',
+        value: {browserName: 'chrome', browserVersion: 'TEST_chrome'},
+        state: null
+      }
+    }, true);
+
+
+    // modify browserName to chrome in plugin global hook
+    return runTests({
+      source,
+      tags: ['@pass'],
+      verbose: false, 
+      config: path.join(__dirname, '../extra/cucumber-config.js')
+    }, {
+      plugins: pluginPath
+    })
+      .then(failures => {
+        assert.strictEqual(failures, false);
+      });
+  });
+
+});
\ No newline at end of file
diff --git a/test/extra/withsafari-concurrent.json b/test/extra/withsafari-concurrent.json
new file mode 100644
index 00000000..bcfefa7b
--- /dev/null
+++ b/test/extra/withsafari-concurrent.json
@@ -0,0 +1,41 @@
+{
+  "src_folders" : ["../sampletests"],
+  "output_folder" : "./output",
+  "custom_commands_path" : "",
+  "custom_assertions_path" : "",
+  "globals_path" : "./globals.js",
+  "test_workers" : {
+    "enabled" : true
+  },
+
+  "output" : false,
+
+  "test_settings": {
+    "default" : {
+      "desiredCapabilities" : {
+        "browserName" : "safari"
+      },
+      "webdriver": {
+        "start_process": true
+      }
+    },
+    "chrome" : {
+      "desiredCapabilities" : {
+        "browserName" : "chrome"
+      },
+      "webdriver": {
+        "start_process": true
+      }
+    },
+    "firefox" : {
+      "desiredCapabilities" : {
+        "browserName" : "firefox"
+      },
+      "webdriver": {
+        "start_process": true
+      }
+    }
+  }
+}
+
+
diff --git a/test/src/analytics/testAnalytics.js b/test/src/analytics/testAnalytics.js
index afcaa812..1bc76a97 100644
--- a/test/src/analytics/testAnalytics.js
+++ b/test/src/analytics/testAnalytics.js
@@ -3,9 +3,15 @@ const fs = require('fs').promises;
 const Nocks = require('../../lib/nocks.js');
 const common = require('../../common.js');
 const {Logger} = common.require('utils');
-const analytics = common.require('utils/analytics.js');
+let analytics = common.require('utils/analytics.js');
 const Settings = common.require('settings/settings.js');
 
+function requireUncached(module) {
+  delete require.cache[require.resolve(module)];
+
+  return require(module);
+}
+
 describe('test analytics utility', function() {
   before(function() {
     Logger.disable();
@@ -110,6 +116,36 @@ describe('test analytics utility', function() {
     await analytics.__flush();
   });
 
+  it('should flush queue after around 5 start up', async function() {
+    Nocks.analyticsCollector(analytics.__getGoogleAnalyticsPath());
+
+    const settings = Settings.parse({
+      usage_analytics: {
+        enabled: true,
+        serverUrl: 'http://localhost:13555'
+      }
+    });
+    
+    let flushBack = analytics.__flush;
+    var called = false;
+
+    for (let i = 0; i < 7; i++) {
+      analytics = requireUncached('../../../lib/utils/analytics.js');
+    
+      flushBack = analytics.__flush;
+      analytics.__flush = function() {
+        called = true;
+      };
+      analytics.updateSettings(settings);
+      await analytics.event('test', {log: 'log'});
+    }
+    
+    assert.ok(called);
+    analytics.__flush = flushBack;
+
+    await analytics.__flush();
+  });
+
   it('should report only allowed exceptions', async function() {
     const analyticsNock = Nocks.analyticsCollector(analytics.__getGoogleAnalyticsPath());
     const err = new Error('test');
diff --git a/test/src/api/commands/client/testMockNetworkResponse.js b/test/src/api/commands/client/testMockNetworkResponse.js
index b73c7a0f..38b16711 100644
--- a/test/src/api/commands/client/testMockNetworkResponse.js
+++ b/test/src/api/commands/client/testMockNetworkResponse.js
@@ -97,6 +97,81 @@ describe('.mockNetworkResponse()', function () {
     });
   });
 
+  it('browser.mockNetworkResponse(urlToIntercept, {status, headers, body}) with multiple mocks', function (done) {
+    MockServer.addMock({
+      url: '/session',
+      response: {
+        value: {
+          sessionId: '13521-10219-202',
+          capabilities: {
+            browserName: 'chrome',
+            browserVersion: '92.0'
+          }
+        }
+      },
+      method: 'POST',
+      statusCode: 201
+    }, true);
+
+    Nightwatch.initW3CClient({
+      desiredCapabilities: {
+        browserName: 'chrome',
+        'goog:chromeOptions': {}
+      },
+      output: process.env.VERBOSE === '1',
+      silent: false
+    }).then(client => {
+      const cdpCommandsExecuted = [];
+      let timesEventListenerAdded = 0;
+
+      cdp.resetConnection();
+      client.transport.driver.createCDPConnection = function() {
+        return Promise.resolve({
+          _wsConnection: {
+            on: () => {
+              timesEventListenerAdded++;
+            }
+          },
+          execute: function(command) {
+            cdpCommandsExecuted.push(command);
+          }
+        });
+      };
+
+      client.api.mockNetworkResponse('https://www.google.com/', {
+        status: 200,
+        headers: {'Content-Type': 'UTF-8'},
+        body: 'Hey there!'
+      });
+      client.api.mockNetworkResponse('https://www.duckduckgo.com/', {
+        status: 200,
+        headers: {'Content-Type': 'UTF-8'},
+        body: 'Good bye!'
+      });
+  
+      client.start(function (err) {
+        if (err) {
+          done(err);
+
+          return;
+        }
+
+        try {
+          const mocks = cdp.networkMocks;
+          assert.deepStrictEqual(Object.keys(mocks), ['https://www.google.com/', 'https://www.duckduckgo.com/']);
+          assert.strictEqual(mocks['https://www.google.com/'].body, Buffer.from('Hey there!').toString('base64'));
+          assert.strictEqual(mocks['https://www.duckduckgo.com/'].body, Buffer.from('Good bye!').toString('base64'));
+
+          assert.strictEqual(timesEventListenerAdded, 1);
+          assert.deepStrictEqual(cdpCommandsExecuted, ['Fetch.enable', 'Network.setCacheDisabled', 'Fetch.enable', 'Network.setCacheDisabled']);
+          done();
+        } catch (err) {
+          done(err);
+        }
+      });
+    });
+  });
+
   it('browser.mockNetworkResponse() with relative url to launch_url', function (done) {
     MockServer.addMock({
       url: '/session',
diff --git a/test/src/apidemos/appium/testAppiumAPI.js b/test/src/apidemos/appium/testAppiumAPI.js
index 47dfc7d2..2aea0c05 100644
--- a/test/src/apidemos/appium/testAppiumAPI.js
+++ b/test/src/apidemos/appium/testAppiumAPI.js
@@ -46,8 +46,8 @@ describe('appium api demos', function () {
       .addMock({
         url: '/wd/hub/session/13521-10219-202/elements',
         postdata: {
-          using: 'id',
-          value: 'com.app:id/search'
+          using: 'accessibility id',
+          value: 'Search Wikipedia'
         },
         method: 'POST',
         response: JSON.stringify({
@@ -56,6 +56,32 @@ describe('appium api demos', function () {
           value: [{'element-6066-11e4-a52e-4f735466cecf': '0'}]
         })
       }, true, true)
+      .addMock({
+        url: '/wd/hub/session/13521-10219-202/element',
+        postdata: {
+          using: 'class name',
+          value: 'android.widget.ImageButton'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          status: 0,
+          state: 'success',
+          value: {'element-6066-11e4-a52e-4f735466cecf': '1'}
+        })
+      }, true)
+      .addMock({
+        url: '/wd/hub/session/13521-10219-202/elements',
+        postdata: {
+          using: 'id',
+          value: 'com.app:id/search'
+        },
+        method: 'POST',
+        response: JSON.stringify({
+          status: 0,
+          state: 'success',
+          value: [{'element-6066-11e4-a52e-4f735466cecf': '2'}]
+        })
+      }, true)
       .addMock({
         url: '/wd/hub/session/13521-10219-202/element/0/click',
         method: 'POST',
@@ -64,7 +90,7 @@ describe('appium api demos', function () {
         })
       }, true)
       .addMock({
-        url: '/wd/hub/session/13521-10219-202/element/0/value',
+        url: '/wd/hub/session/13521-10219-202/element/2/value',
         method: 'POST',
         postdata: JSON.stringify({
           text: 'Nightwatch',
diff --git a/test/src/cli/testCliRunner.js b/test/src/cli/testCliRunner.js
index 7ff93aa1..197b0ed0 100644
--- a/test/src/cli/testCliRunner.js
+++ b/test/src/cli/testCliRunner.js
@@ -647,7 +647,7 @@ describe('Test CLI Runner', function() {
 
     mockery.registerMock('fs', {
       existsSync() {
-        return false
+        return false;
       },
       stat(file, cb) {
         if (file === TEST_SRC_PATH || file === './custom.js') {
@@ -1288,4 +1288,59 @@ describe('Test CLI Runner', function() {
     assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidPackage, 'org.mozilla.firefox');
     assert.strictEqual(runner.test_settings.desiredCapabilities['moz:firefoxOptions'].androidDeviceSerial, 'ZD2222W62Y');
   });
+
+  describe('Test \'list-files\' flag', () => { 
+    it('output list of files - default environment', async () => {
+      const testsPath = [origPath.join(__dirname, origPath.join('..', '..', 'sampletests', 'simple', 'test', 'sample.js'))];
+      const consoleData = [];
+      const listFileOutput = JSON.stringify({
+        default: testsPath
+      });
+  
+      const origConsoleLog = console.log;
+  
+      console.log = function (data) {
+        consoleData.push(data);
+      };
+  
+      mockery.registerMock('./runner/cli/argv-setup.js', {
+        argv: {
+          _: testsPath,
+          'list-files': true
+        }
+      });
+      const NwClient = common.require('index.js');
+      await NwClient.cli();
+      assert.deepStrictEqual(listFileOutput, consoleData[0]);
+      console.log = origConsoleLog;
+    });
+
+    it('output list of files - chrome environment', async () => {
+      const testsPath = [origPath.join(__dirname, origPath.join('..', '..', 'sampletests', 'simple', 'test', 'sample.js'))];
+      const consoleData = [];
+      const listFileOutput = JSON.stringify({
+        chrome: testsPath
+      });
+  
+      const origConsoleLog = console.log;
+  
+      console.log = function (data) {
+        consoleData.push(data);
+      };
+  
+      mockery.registerMock('./runner/cli/argv-setup.js', {
+        argv: {
+          _: testsPath,
+          env: 'chrome',
+          'list-files': true
+        }
+      });
+      const NwClient = common.require('index.js');
+      await NwClient.cli();
+      assert.deepStrictEqual(listFileOutput, consoleData[0]);
+      console.log = origConsoleLog;
+    });
+  });
+
+
 });
diff --git a/test/src/runner/cli/testCliRunnerParallel.js b/test/src/runner/cli/testCliRunnerParallel.js
index b32521f3..2a919d33 100644
--- a/test/src/runner/cli/testCliRunnerParallel.js
+++ b/test/src/runner/cli/testCliRunnerParallel.js
@@ -120,6 +120,46 @@ describe('Test CLI Runner in Parallel', function () {
     assert.strictEqual(runner.parallelMode(), false);
   });
 
+  it('disable parallelism when running tests on safari', function() {
+    class RunnerBaseMock extends RunnerBase {
+      static readTestSource(settings, argv) {
+        assert.strictEqual(settings.testWorkersEnabled, true);
+
+        return [
+          'test_file_1.js',
+          'test_file_2.js'
+        ];
+      }
+    }
+    mockery.registerMock('../runner.js', RunnerBaseMock);
+    const runner = NightwatchClient.CliRunner({
+      config: path.join(__dirname, '../../../extra/withsafari-concurrent.json'),
+      env: 'default'
+    }).setup();
+    assert.strictEqual(runner.isTestWorkersEnabled(), false);
+    assert.strictEqual(runner.parallelMode(), false);
+  });
+
+  it('disable parallelism when running tests on safari with --env chrome,safari', function() {
+    class RunnerBaseMock extends RunnerBase {
+      static readTestSource(settings, argv) {
+        assert.strictEqual(settings.testWorkersEnabled, true);
+
+        return [
+          'test_file_1.js',
+          'test_file_2.js'
+        ];
+      }
+    }
+    mockery.registerMock('../runner.js', RunnerBaseMock);
+    const runner = NightwatchClient.CliRunner({
+      config: path.join(__dirname, '../../../extra/withsafari-concurrent.json'),
+      env: ['default', 'chrome']
+    }).setup();
+    assert.strictEqual(runner.isTestWorkersEnabled(), false);
+    assert.strictEqual(runner.parallelMode(), false);
+  });
+
   it('mobile config setup on with multiple envs - enable parallelism ', function() {
     class RunnerBaseMock extends RunnerBase {
       static readTestSource(settings, argv) {
diff --git a/test/src/runner/testReporter.js b/test/src/runner/testReporter.js
index 96c62b6c..967e35d6 100644
--- a/test/src/runner/testReporter.js
+++ b/test/src/runner/testReporter.js
@@ -364,4 +364,32 @@ describe('testReporter', function() {
     }
     assert.strictEqual(possibleError, null);
   });
+
+  it('test with multiple reporters', function() {
+    mockery.registerMock('nightwatch_reporter', {
+      async write(_results, _options) {
+
+        return 'nightwatch_reporter_output';
+      }
+    });
+    mockery.registerMock('html_reporter', {
+      async write(_results, _options) {
+
+        return 'html_reporter_output';
+      }
+    });
+
+    const reporter = new Reporter('nightwatch_reporter,html_reporter', {
+      globals: {
+        reporter(_results, done) {
+          done();
+        }
+      },
+      output_folder: 'output'
+    });
+
+    return reporter.writeReportToFile().then(function(result) {
+      assert.deepStrictEqual(result, ['nightwatch_reporter_output', 'html_reporter_output']);
+    });
+  });
 });