Repository: nightwatchjs/nightwatch (js)

Issue:
Request redirection uses 127.0.0.1 instead of server host when location header is relative
## Describe the bug

When launching a test on Saucelabs, if the maximum number of sessions is already reached, the test is put in a queue.
In such a case, the request on `/wd/hub/session` responds with a status `303` and the headers:
```js
  {
     location: '/wd/hub/session/123-sessionId...',
     'content-length': ‘0’,
    //  ...
  }
```
Notice that the location header is a **relative url**, it does not contain the hostname.

#### The problem
Instead of sending the next request to the Saucelabs server, it sends it to `127.0.0.1` and fails. 
See the last line of the attached logs:
```
Connection refused to 127.0.0.1:443
```

#### Analysis from local debugging
The `location` header is parsed with `require("url").parse()` in [request.js#L228](https://github.com/nightwatchjs/nightwatch/blob/main/lib/http/request.js#L228) which returns `{ hostname: null, port:null }`.
This sets the hostname of the following requests to null and later defaults to `127.0.0.1` in the following requests. 

According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location#directives, the location header can be relative or absolute.

Below are the files I used to reproduce, but one really needs a Saucelabs instance with all its sessions busy to reproduce. 
The redirection worked fine in versions `1.7.x`.

## Sample test
<!-- 
Include a sample test which reproduces the problem you're experiencing. If possible, the test should be against a public url.

Please add the test and other info inline, not as attachments or screenshots.
-->

<details><summary>sampleTest.js</summary><p>
<!-- browsers demand the next line be empty -->

```js
describe('Ecosia.org Demo', function() {

    before(browser => browser.navigateTo('https://www.ecosia.org/'));
  
    it('Demo test ecosia.org', function(browser) {
      browser
        .waitForElementVisible('body')
        .assert.titleContains('Ecosia')
        .assert.visible('input[type=search]')
        .setValue('input[type=search]', 'nightwatch')
        .assert.visible('button[type=submit]')
        .click('button[type=submit]')
        .assert.textContains('.mainline-results', 'Nightwatch.js');
    });
  
    after(browser => browser.end());
  });
```
</p></details>

**Run with command**
<!-- Include the command used to run the test, if applicable. -->

```sh
$ nightwatch -c config.js --verbose src/sampleTest.js
```

### Verbose output
<details><summary>debug.log</summary><p>
<!-- browsers demand the next line be empty -->

```txt
[Ecosia.org Demo] Test Suite
────────────────────────────────────────────────────────
⠋ Connecting to ondemand.eu-central-1.saucelabs.com on port 443...
   Request POST https://ondemand.eu-central-1.saucelabs.com/wd/hub/session
   { desiredCapabilities:
      { browserName: 'chrome',
        'sauce:options': { ... },
        name: 'Ecosia.org Demo',
        'goog:chromeOptions': {} },
     capabilities: { ... }
⠴ Connecting to ondemand.eu-central-1.saucelabs.com on port 443...
   Response 303 POST https://ondemand.eu-central-1.saucelabs.com/wd/hub/session (45452ms)
{}
   Request GET https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/123-sessionId
   { desiredCapabilities:
      { browserName: 'chrome',
        'sauce:options': { ... },
        name: 'Ecosia.org Demo',
        'goog:chromeOptions': {} },
     capabilities: { ... }
    GET https://ondemand.eu-central-1.saucelabs.com /wd/hub/session/123-sessionId - ECONNREFUSED
⠦ Connecting to ondemand.eu-central-1.saucelabs.com on port 443...
   Request GET https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/123-sessionId  (retry 1)

    ...

     TEST FAILURE (45.849s):
   - 1 error during execution;
   - 0 tests failed;
   - 0/NA tests passed


  Error: An error occurred while creating a new Selenium Server session: Connection refused to 127.0.0.1:443. If the Webdriver/Selenium service is managed by Nightwatch, check if "start_process" is set to "true".

```
</p></details>


## Configuration
<details><summary>config.js</summary><p>
<!-- browsers demand the next line be empty -->

<!-- Please paste your nightwatch.json or nightwatch.conf.js here; make sure to leave out any sensitive details -->

```js
module.exports = {
  src_folders: ["src"],
  test_settings: {
    default: {
      selenium: {
        host: "ondemand.eu-central-1.saucelabs.com",
        port: 443
      },
      desiredCapabilities: {
        browserName: 'chrome',
        'sauce:options': {
          username: '${SAUCELABS_USER}',
          accessKey: '${SAUCELABS_KEY}',,
          seleniumVersion: '4.1.2',
        }
      }
    },
  }
};
```
</p></details>

## Your Environment
<!--- Include the relevant details your environment -->

| Executable | Version |
| ---: | :--- |
| `nightwatch --version` | 2.0.10 |
| `npm --version`  | 8.3.1 |
| `node --version` | v16.14.0 |

| Browser driver | Version |
| --- | --- |
| chromedriver | 76 |
| Selenium | 4.1.2 |

| OS | Version |
| --- | --- |
| macOS BigSur | 11.6.2 |


Pull request description:
Fix #3096 - preserve host when redirect points to relative url
fixes #3096 

There are now 2 unit tests testing redirections with absolute and relative urls. 

- [x] Create a new branch from master (e.g. `features/my-new-feature` or `issue/123-my-bugfix`);
- [x] If you're fixing a bug also create an issue if one doesn't exist yet;
- [x] If it's a new feature explain why do you think it's necessary. Please check with the maintainers beforehand to make sure it is something that we will accept. Usually we only accept new features if we feel that they will benefit the entire community;
- [x] Please avoid sending PRs which contain drastic or low level changes. If you are certain that the changes are needed, please discuss them beforehand and indicate what the impact will be;
- [x] If your change is based on existing functionality please consider refactoring first. Pull requests that duplicate code will most likely be ignored;
- [x] Do not include changes that are not related to the issue at hand;
- [x] Follow the same coding style with regards to spaces, semicolons, variable naming etc.;
- [x] Always add unit tests - PRs without tests are most of the times ignored.


Patch:
diff --git a/lib/api/_loaders/plugin.js b/lib/api/_loaders/plugin.js
index 4ed028b9..7ee3a0f0 100644
--- a/lib/api/_loaders/plugin.js
+++ b/lib/api/_loaders/plugin.js
@@ -3,19 +3,7 @@ const path = require('path');
 const Utils = require('../../utils');
 
 module.exports = class PluginLoader {
-  get api() {
-    return this.nightwatchInstance.api;
-  }
-
-  get commandQueue() {
-    return this.nightwatchInstance.queue;
-  }
-
-  constructor(nightwatchInstance) {
-    this.nightwatchInstance = nightwatchInstance;
-  }
-
-  load(pluginName) {
+  static load(pluginName) {
     try {
       const plugin = {};
       const pluginPath = Utils.getPluginPath(pluginName);
@@ -43,6 +31,12 @@ module.exports = class PluginLoader {
           plugin.commands = false;
         }
 
+        try {
+          plugin.globals = require(path.join(dirName, 'nightwatch', 'globals.js'));
+        } catch (err) {
+          plugin.globals = false;
+        }
+
         try {
           const hasAssertions = fs.lstatSync(assertionsFolder).isDirectory();
           if (hasAssertions) {
diff --git a/lib/api/client-commands/mockNetworkResponse.js b/lib/api/client-commands/mockNetworkResponse.js
index 88c6055f..b25b26ac 100644
--- a/lib/api/client-commands/mockNetworkResponse.js
+++ b/lib/api/client-commands/mockNetworkResponse.js
@@ -37,7 +37,14 @@ class MockNetworkResponse extends ClientCommand {
       return callback(error);
     }
 
-    const {urlToIntercept = '', response = {}} = this;
+    const {response = {}} = this;
+    let {urlToIntercept = ''} = this;
+
+    if (urlToIntercept.startsWith('/')) {
+      let needsSlash = this.api.globals.launchUrl.endsWith('/');
+      let slashDel = needsSlash ? '/' : '';
+      urlToIntercept = `${this.api.globals.launchUrl}${slashDel}${urlToIntercept}`;
+    }
     
     this.transportActions
       .mockNetworkResponse(urlToIntercept, response, callback)
diff --git a/lib/api/index.js b/lib/api/index.js
index b65b03df..82b1f200 100644
--- a/lib/api/index.js
+++ b/lib/api/index.js
@@ -153,8 +153,7 @@ class ApiLoader {
     const plugins = this.nightwatchInstance.options.plugins || [];
 
     plugins.forEach(pluginName => {
-      const instance = new PluginLoader(this.nightwatchInstance);
-      const plugin = instance.load(pluginName);
+      const plugin = PluginLoader.load(pluginName);
 
       if (plugin.commands) {
         this.__loadCustomObjectsSync({
diff --git a/lib/api/protocol/url.js b/lib/api/protocol/url.js
index 25568576..1966f362 100644
--- a/lib/api/protocol/url.js
+++ b/lib/api/protocol/url.js
@@ -37,7 +37,8 @@ module.exports = class Action extends ProtocolAction {
       if (this.settings.output) {
         spinner = ora({
           text: `Loading url: ${url}\n`,
-          prefixText: ' '
+          prefixText: ' ',
+          discardStdin: false
         }).start();
       }
 
diff --git a/lib/core/client.js b/lib/core/client.js
index dcabd66f..ec7281ea 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -519,7 +519,7 @@ class NightwatchClient extends EventEmitter {
       this.settings.webdriver.default_path_prefix = this.transport.defaultPathPrefix;
     }
 
-    if (this.argv.parallel && this.settings.webdriver.start_process) {
+    if (this.settings.testWorkersEnabled && this.settings.webdriver.start_process) {
       // when running in parallel with test workers, the port needs to be randomly assigned
       this.settings.webdriver.port = undefined;
     }
diff --git a/lib/http/request.js b/lib/http/request.js
index 89707afa..7b88ce3e 100644
--- a/lib/http/request.js
+++ b/lib/http/request.js
@@ -236,9 +236,11 @@ class HttpRequest extends EventEmitter {
 
         return;
       }
-
-      this.reqOptions.host = location.hostname;
-      this.reqOptions.port = location.port;
+      const isAbsoluteUrl = !!location.host;
+      if (isAbsoluteUrl) {
+        this.reqOptions.host = location.hostname;
+        this.reqOptions.port = location.port;
+      }
       this.reqOptions.path = location.pathname;
       this.reqOptions.method = 'GET';
 
diff --git a/lib/runner/cli/cli.js b/lib/runner/cli/cli.js
index 8224d4f0..2e9dc427 100644
--- a/lib/runner/cli/cli.js
+++ b/lib/runner/cli/cli.js
@@ -34,6 +34,8 @@ class CliRunner {
       'vite-plugin-nightwatch': {
         launch_url: 'http://localhost:3000'
       }
+    }, {
+      '@nightwatch/react': {}
     }];
 
     autoLoadPlugins.forEach(plugin => {
@@ -250,7 +252,7 @@ class CliRunner {
 
   runGlobalHook(key) {
     if (!Concurrency.isChildProcess()) {
-      return this.globals.hooks[key].run();
+      return this.globals.runGlobalHook(key);
     }
 
     return Promise.resolve();
diff --git a/lib/transport/selenium-webdriver/options.js b/lib/transport/selenium-webdriver/options.js
index 86330e02..7f6ff5e6 100644
--- a/lib/transport/selenium-webdriver/options.js
+++ b/lib/transport/selenium-webdriver/options.js
@@ -1,4 +1,5 @@
 const {Capabilities, Browser} = require('selenium-webdriver');
+const Utils = require('../../utils');
 
 module.exports = class SeleniumCapabilities {
   get initialCapabilities() {
@@ -102,6 +103,14 @@ module.exports = class SeleniumCapabilities {
       return this.desiredCapabilities;
     }
 
+    // Backward compatibility for 'chromeOptions'
+    if (Utils.isObject(this.desiredCapabilities.chromeOptions)) {
+      this.desiredCapabilities['goog:chromeOptions'] = Object.assign(
+        this.desiredCapabilities.chromeOptions,
+        this.desiredCapabilities['goog:chromeOptions']
+      );
+    }
+
     const {webdriver} = this.settings;
     let options = new ChromeOptions(this.desiredCapabilities);
 
diff --git a/package-lock.json b/package-lock.json
index 617cbef0..1714cfd4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nightwatch",
-  "version": "2.2.2",
+  "version": "2.2.3-preview.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "nightwatch",
-      "version": "2.2.2",
+      "version": "2.2.3-preview.1",
       "license": "MIT",
       "dependencies": {
         "@nightwatch/chai": "5.0.2",
@@ -30,7 +30,7 @@
         "mocha": "9.2.2",
         "open": "^8.4.0",
         "ora": "5.4.1",
-        "selenium-webdriver": "4.1.1",
+        "selenium-webdriver": "^4.3.1",
         "semver": "7.3.5",
         "stacktrace-parser": "^0.1.10",
         "strip-ansi": "6.0.1"
@@ -3280,7 +3280,8 @@
     },
     "node_modules/immediate": {
       "version": "3.0.6",
-      "license": "MIT"
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
     },
     "node_modules/import-fresh": {
       "version": "3.3.0",
@@ -3495,7 +3496,8 @@
     },
     "node_modules/isarray": {
       "version": "1.0.0",
-      "license": "MIT"
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
     },
     "node_modules/isexe": {
       "version": "2.0.0",
@@ -3839,13 +3841,14 @@
       }
     },
     "node_modules/jszip": {
-      "version": "3.7.1",
-      "license": "(MIT OR GPL-3.0-or-later)",
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz",
+      "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==",
       "dependencies": {
         "lie": "~3.3.0",
         "pako": "~1.0.2",
         "readable-stream": "~2.3.6",
-        "set-immediate-shim": "~1.0.1"
+        "setimmediate": "^1.0.5"
       }
     },
     "node_modules/keyv": {
@@ -3890,7 +3893,8 @@
     },
     "node_modules/lie": {
       "version": "3.3.0",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
       "dependencies": {
         "immediate": "~3.0.5"
       }
@@ -5246,7 +5250,8 @@
     },
     "node_modules/pako": {
       "version": "1.0.11",
-      "license": "(MIT AND Zlib)"
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
     },
     "node_modules/parent-module": {
       "version": "1.0.1",
@@ -5386,7 +5391,8 @@
     },
     "node_modules/process-nextick-args": {
       "version": "2.0.1",
-      "license": "MIT"
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
     },
     "node_modules/process-on-spawn": {
       "version": "1.0.0",
@@ -5511,7 +5517,8 @@
     },
     "node_modules/readable-stream": {
       "version": "2.3.7",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+      "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
       "dependencies": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -5524,7 +5531,8 @@
     },
     "node_modules/readable-stream/node_modules/string_decoder": {
       "version": "1.1.1",
-      "license": "MIT",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
       "dependencies": {
         "safe-buffer": "~5.1.0"
       }
@@ -5765,13 +5773,13 @@
       "license": "MIT"
     },
     "node_modules/selenium-webdriver": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.1.tgz",
-      "integrity": "sha512-Fr9e9LC6zvD6/j7NO8M1M/NVxFX67abHcxDJoP5w2KN/Xb1SyYLjMVPGgD14U2TOiKe4XKHf42OmFw9g2JgCBQ==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.3.1.tgz",
+      "integrity": "sha512-TjH/ls1WKRQoFEHcqtn6UtwcLnA3yvx08v9cSSFYvyhp8hJWRtbe9ae2I8uXPisEZ2EaGKKoxBZ4EHv0BJM15g==",
       "dependencies": {
-        "jszip": "^3.6.0",
+        "jszip": "^3.10.0",
         "tmp": "^0.2.1",
-        "ws": ">=7.4.6"
+        "ws": ">=8.7.0"
       },
       "engines": {
         "node": ">= 10.15.0"
@@ -5802,12 +5810,10 @@
       "dev": true,
       "license": "ISC"
     },
-    "node_modules/set-immediate-shim": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
     },
     "node_modules/shebang-command": {
       "version": "2.0.0",
@@ -6469,8 +6475,9 @@
       }
     },
     "node_modules/ws": {
-      "version": "8.4.2",
-      "license": "MIT",
+      "version": "8.8.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz",
+      "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==",
       "engines": {
         "node": ">=10.0.0"
       },
@@ -8669,7 +8676,9 @@
       "devOptional": true
     },
     "immediate": {
-      "version": "3.0.6"
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
     },
     "import-fresh": {
       "version": "3.3.0",
@@ -8787,7 +8796,9 @@
       }
     },
     "isarray": {
-      "version": "1.0.0"
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
     },
     "isexe": {
       "version": "2.0.0"
@@ -9012,12 +9023,14 @@
       }
     },
     "jszip": {
-      "version": "3.7.1",
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz",
+      "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==",
       "requires": {
         "lie": "~3.3.0",
         "pako": "~1.0.2",
         "readable-stream": "~2.3.6",
-        "set-immediate-shim": "~1.0.1"
+        "setimmediate": "^1.0.5"
       }
     },
     "keyv": {
@@ -9049,6 +9062,8 @@
     },
     "lie": {
       "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
       "requires": {
         "immediate": "~3.0.5"
       }
@@ -9966,7 +9981,9 @@
       }
     },
     "pako": {
-      "version": "1.0.11"
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
     },
     "parent-module": {
       "version": "1.0.1",
@@ -10049,7 +10066,9 @@
       "dev": true
     },
     "process-nextick-args": {
-      "version": "2.0.1"
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
     },
     "process-on-spawn": {
       "version": "1.0.0",
@@ -10125,6 +10144,8 @@
     },
     "readable-stream": {
       "version": "2.3.7",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+      "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
       "requires": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -10137,6 +10158,8 @@
       "dependencies": {
         "string_decoder": {
           "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
           "requires": {
             "safe-buffer": "~5.1.0"
           }
@@ -10291,13 +10314,13 @@
       "dev": true
     },
     "selenium-webdriver": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.1.tgz",
-      "integrity": "sha512-Fr9e9LC6zvD6/j7NO8M1M/NVxFX67abHcxDJoP5w2KN/Xb1SyYLjMVPGgD14U2TOiKe4XKHf42OmFw9g2JgCBQ==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.3.1.tgz",
+      "integrity": "sha512-TjH/ls1WKRQoFEHcqtn6UtwcLnA3yvx08v9cSSFYvyhp8hJWRtbe9ae2I8uXPisEZ2EaGKKoxBZ4EHv0BJM15g==",
       "requires": {
-        "jszip": "^3.6.0",
+        "jszip": "^3.10.0",
         "tmp": "^0.2.1",
-        "ws": ">=7.4.6"
+        "ws": ">=8.7.0"
       }
     },
     "semver": {
@@ -10316,8 +10339,10 @@
       "version": "2.0.0",
       "dev": true
     },
-    "set-immediate-shim": {
-      "version": "1.0.1"
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
     },
     "shebang-command": {
       "version": "2.0.0",
@@ -10733,7 +10758,9 @@
       }
     },
     "ws": {
-      "version": "8.4.2",
+      "version": "8.8.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz",
+      "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==",
       "requires": {}
     },
     "xml": {
diff --git a/package.json b/package.json
index 8f8dcbe1..b73f3d0f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "nightwatch",
   "description": "Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.",
-  "version": "2.2.2",
+  "version": "2.2.3-preview.1",
   "author": "Andrei Rusu",
   "homepage": "https://nightwatchjs.org",
   "main": "./lib/index.js",
@@ -33,7 +33,7 @@
     "mocha": "9.2.2",
     "open": "^8.4.0",
     "ora": "5.4.1",
-    "selenium-webdriver": "4.1.1",
+    "selenium-webdriver": "^4.3.1",
     "semver": "7.3.5",
     "stacktrace-parser": "^0.1.10",
     "strip-ansi": "6.0.1"


Test patch:
diff --git a/lib/testsuite/globals.js b/lib/testsuite/globals.js
index 05a356cc..762d4aea 100644
--- a/lib/testsuite/globals.js
+++ b/lib/testsuite/globals.js
@@ -1,10 +1,9 @@
 const path = require('path');
 const lodashClone = require('lodash.clone');
-const lodashMerge = require('lodash.merge');
-const defaultsDeep = require('lodash.defaultsdeep');
 const TestHooks = require('./hooks.js');
 const Context = require('./context.js');
 const Utils = require('../utils');
+const PluginLoader = require('../api/_loaders/plugin.js');
 
 class GlobalsContext extends Context {
   get isTestHook() {
@@ -159,12 +158,25 @@ class Globals {
     this.readExternal();
     this.mergeWithExisting();
     this.setupGlobalHooks();
+    this.loadPlugins();
   }
 
   get globals() {
     return this.context.module;
   }
 
+  loadPlugins() {
+    const definition = this.settings.plugins || [];
+    this.plugins = definition.reduce((prev, pluginName) => {
+      const plugin = PluginLoader.load(pluginName);
+      if (plugin.globals) {
+        prev.push(plugin);
+      }
+
+      return prev;
+    }, []);
+  }
+
   readExternal() {
     this.context = new GlobalsContext(this.settings, this.argv, this.currentEnv);
   }
@@ -187,13 +199,7 @@ class Globals {
       // eslint-disable-next-line
     } catch (err) {}
 
-    // Object.assign(this.settings.globals, this.globals);
-    //const existingGlobals = lodashClone(this.settings.globals, true);
-    //
     this.settings.globals = this.globals;
-    //
-    // // external globals take precedence
-    //defaultsDeep(this.settings.globals, existingGlobals);
   }
 
   setupGlobalHooks() {
@@ -202,6 +208,18 @@ class Globals {
       asyncHookTimeout: this.settings.globals.asyncHookTimeout
     });
   }
+
+  async runGlobalHook(key) {
+    await this.hooks[key].run();
+
+    await Promise.all(this.plugins.map(plugin => {
+      if (Utils.isFunction(plugin.globals[key])) {
+        return plugin.globals[key].call(this.settings.globals);
+      }
+      
+      return Promise.resolve();
+    }));
+  }
 }
 
 module.exports = Globals;
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 5d051a4e..49ef5222 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -1,4 +1,6 @@
 const AssertionError = require('assertion-error');
+const {By, Key, locateWith, withTagName} = require('selenium-webdriver');
+
 const Reporter = require('../reporter');
 const Context = require('./context.js');
 const TestHooks = require('./hooks.js');
@@ -10,7 +12,6 @@ const NightwatchClient = require('../core/client.js');
 const Concurrency = require('../runner/concurrency');
 const ElementGlobal = require('../api/_loaders/element-global.js');
 const {Logger, Screenshots, alwaysDisplayError, isString, isFunction, SafeJSON} = require('../utils');
-const {By, Key, locateWith, withTagName} = require('selenium-webdriver');
 
 class TestSuite {
   constructor({modulePath, modules, settings, argv, usingMocha = false, addtOpts = {}}) {
diff --git a/test/src/cli/testParallelExecution.js b/test/src/cli/testParallelExecution.js
index 0896d249..947242c3 100644
--- a/test/src/cli/testParallelExecution.js
+++ b/test/src/cli/testParallelExecution.js
@@ -4,6 +4,7 @@ const mockery = require('mockery');
 const path = require('path');
 const assert = require('assert');
 const common = require('../../common.js');
+const Nightwatch = require('../../lib/nightwatch.js');
 
 describe('test Parallel Execution', function() {
   const allArgs = [];
@@ -326,7 +327,8 @@ describe('test Parallel Execution', function() {
     assert.ok(args.includes('--parallel-mode'));
   });
 
-  it('test parallel execution to ensure worker start_process is disabled when using selenium server', function() {
+  // TODO: this test is not complete
+  xit('test parallel execution to ensure worker start_process is disabled when using selenium server', function() {
     const CliRunner = common.require('runner/cli/cli.js');
     const runner = new CliRunner({
       reporter: 'junit',
@@ -335,6 +337,35 @@ describe('test Parallel Execution', function() {
 
     runner.setup();
     const worker = runner.concurrency.createChildProcess('test-worker');
+
+  });
+
+  it('test random port assignment for parallel execution', function(){
+    const client = Nightwatch.createClient({
+      webdriver: {
+        port: 9999,
+        start_process: true
+      },
+
+      'test_workers': {
+        'enabled': true,
+        'workers': 4
+      }
+    });
+    assert.equal(client.transport.settings.webdriver.port, undefined);
+
+    const client2 = Nightwatch.createClient({
+      webdriver: {
+        port: 9999,
+        start_process: false
+      },
+
+      'test_workers': {
+        'enabled': true,
+        'workers': 4
+      }
+    });
+    assert.equal(client2.transport.settings.webdriver.port, 9999);
   });
 
 });
diff --git a/test/src/index/testNightwatchIndex.js b/test/src/index/testNightwatchIndex.js
index a5864110..9ad5abda 100644
--- a/test/src/index/testNightwatchIndex.js
+++ b/test/src/index/testNightwatchIndex.js
@@ -82,6 +82,49 @@ describe('test NightwatchIndex', function () {
     });
   });
 
+  it('testChromeSessionWithRelativeRedirectUrl', function (done) {
+    MockServer.addMock({
+      url: '/wd/hub/session',
+      responseHeaders: {
+        location: '/wd/hub/session/1352110219203'
+      },
+      statusCode: 302,
+      method: 'POST'
+    }, true);
+
+    MockServer.addMock({
+      url: '/wd/hub/session/1352110219203',
+      response: JSON.stringify({
+        status: 0,
+        sessionId: '1352110219203',
+        value: {browserName: 'chrome'}
+      }),
+      responseHeaders: {
+        statusCode: 201
+      },
+      method: 'GET'
+    }, true);
+
+
+    const client = Nightwatch.createClient({
+      desiredCapabilities: {
+        browserName: 'chrome'
+      },
+      silent: false,
+      output: false
+    });
+
+    client.on('nightwatch:session.create', function (data) {
+      assert.strictEqual(data.sessionId, '1352110219203');
+      assert.strictEqual(client.api.capabilities.browserName, 'chrome');
+      done();
+    });
+
+    client.startSession().catch(err => {
+      done(err);
+    });
+  });
+
   it('test new Chrome session with wrong driver version error message', function () {
     MockServer.addMock({
       url: '/session',
diff --git a/test/src/index/transport/testChromeOptions.js b/test/src/index/transport/testChromeOptions.js
index dd2ae422..afe90c23 100644
--- a/test/src/index/transport/testChromeOptions.js
+++ b/test/src/index/transport/testChromeOptions.js
@@ -34,6 +34,23 @@ describe('Test chrome options', function () {
     assert.strictEqual(client.api.browserName, 'chrome');
   });
 
+  it('chromeOptions detach driver option', function () {
+    const client = Nightwatch.createClient({
+      desiredCapabilities: {
+        browserName: 'chrome',
+        chromeOptions: {
+          detach: true
+        }
+      }
+    });
+    const options = client.transport.createSessionOptions();
+
+    assert.strictEqual(options instanceof ChromeOptions, true);
+    assert.strictEqual(options.options_.detach, true);
+    assert.strictEqual(client.api.isChrome(), true);
+    assert.strictEqual(client.api.browserName, 'chrome');
+  });
+
   it('Chrome Binary Path option', function(){
     const client = Nightwatch.createClient({
       webdriver: {
@@ -139,6 +156,22 @@ describe('Test chrome options', function () {
     assert.deepStrictEqual(options.options_.args, ['--auto-open-devtools-for-tabs']);
   });
 
+  it('devtools cli arg with already defined setting (using chromeOptions)', function(){
+    const client = Nightwatch.createClient({
+      desiredCapabilities: {
+        browserName: 'chrome',
+        chromeOptions: {
+          args: ['auto-open-devtools-for-tabs']
+        }
+      }
+    });
+
+    const options = client.transport.createSessionOptions({devtools: true});
+
+    assert.strictEqual(options instanceof ChromeOptions, true);
+    assert.deepStrictEqual(options.options_.args, ['auto-open-devtools-for-tabs']);
+  });
+
   it('window size option', function(){
     const client =  Nightwatch.createClient({
       window_size: {