Repository: nightwatchjs/nightwatch
Pull request #3622 (2023-02-28 12:53:33)

Fixes/consider test suite settings in cucumber runner
## Overview
- when settings is updated in global plugin it should be considered while creating sessions using cucumber as a test-runner

## Changes
- merge `test_settings` from cucumber-testsuite to `CliRunner.test_settings`.
- added test.

## Impact
- with this change browserstack-local plugin will work with cucumber runner as well


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/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/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index 3a01e22a..30dfc8e3 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) {


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