Repository: Codeception/CodeceptJS
Pull request #1956 (2019-10-14 22:57:26)

2.3.3 release

martomo: @DavertMik In regard to @PeterNgTr comments, I noticed that Robo task does not always update the documents accordingly, perhaps the Robo task should be updated to clear build directory before generating?

What worked for me when generating Puppeteer/WebDriver documentation was the following:
```bash
rm docs/build/* docs/helpers/Puppeteer.md docs/helpers/WebDriver.md && robo docs:helpers
```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecd4b687..8ac66a2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,29 @@
+## 2.3.3
+
+* **[customLocator plugin](#customlocator) introduced**. Adds a locator strategy for special test attributes on elements.
+
+```js
+// when data-test-id is a special test attribute
+// enable and configure plugin to replace this
+I.click({ css: '[data-test-id=register_button]');
+// with this
+I.click('$register_button');
+```
+* [Puppeteer][WebDriver] `pressKey` improvements by @martomo:
+Changed pressKey method to resolve issues and extend functionality.
+  * Did not properly recognize 'Meta' (or 'Command') as modifier key.
+  * Right modifier keys did not work in WebDriver using JsonWireProtocol.
+  * 'Shift' + <key> combination would not reflect actual keyboard behavior.
+  * Respect sequence with multiple modifier keys passed to pressKey.
+  * Added support to automatic change operation modifier key based on operating system.
+* [Puppeteer][WebDriver] Added `pressKeyUp` and `pressKeyDown` to press and release modifier keys like `Control` or `Shift`. By @martomo.
+* [Puppeteer][WebDriver] Added `grabElementBoundingRect` by @PeterNgTr.
+* [Puppeteer] Fixed speed degradation introduced in #1306 with accessibility locators support. See #1953.
+* Added `Config.addHook` to add a function that will update configuration on load.
+* Started [`@codeceptjs/configure`](https://github.com/codecept-js/configure) package with a collection of common configuration patterns.
+* [TestCafe] port's management removed (left on TestCafe itself) by @orihomie. Fixes #1934.
+* [REST] Headers are no more declared as singleton variable. Fixes #1959
+
 ## 2.3.2
 
 * [Puppeteer] Fixed Puppeteer 1.20 support by @davertmik
diff --git a/docs/basics.md b/docs/basics.md
index d835f5d9..2b9056da 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -358,6 +358,52 @@ npx codeceptjs run-workers 3
 
 Tests are split by scenarios, not by files. Results are aggregated and shown in the main process.
 
+## Configuration
+
+Configuration is set in `codecept.conf.js` file which was created during `init` process.
+Inside config file you can enable and configure helpers, plugins, set bootstrap and teardown scripts.
+
+```js
+exports.config = {
+  helpers: {
+    // enabled helpers with their configs
+  },
+  plugins: {
+    // list of used plugins
+  },
+  include: {
+    // current actor and page objects
+  }
+}
+```
+
+> ▶ See complete [configuration reference](https://codecept.io/configuration).
+
+You can have multiple configuration files for a one project. In this case specify a config file to be used with `-c` when running.
+
+```
+npx codeceptjs run -c codecept.ci.conf.js
+```
+
+Tuning configuration for helpers like WebDriver, Puppeteer can be hard, as it requires good understanding of how those technologies work. Use [`@codeceptjs/configure`](https://github.com/codecept-js/configure) package with common configuration recipes.
+
+For instance, you can set window size or toggle headless mode no matter of which helpers are actually used.
+
+```js
+const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure');
+
+// run headless when CI environment variable set
+setHeadlessWhen(process.env.CI);
+// set window size for any helper: Puppeteer, WebDriver, TestCafe
+setWindowSize(1600, 1200);
+
+exports.config = {
+  // ...
+}
+```
+
+> ▶ See more [configuration recipes](https://github.com/codecept-js/configure)
+
 ## Debug
 
 CodeceptJS allows to write and debug tests on the fly while keeping your browser opened.
@@ -418,7 +464,6 @@ This can be configured in [screenshotOnFail Plugin](https://codecept.io/plugins/
 
 To see how the test was executed, use [stepByStepReport Plugin](https://codecept.io/plugins/#stepbystepreport). It saves a screenshot of each passed step and shows them in a nice slideshow.
 
-
 ## Retries
 
 ### Retry Step
diff --git a/docs/configuration.md b/docs/configuration.md
index 2f962dc5..a5bd8414 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -93,7 +93,24 @@ exports.config = {
 If you prefer to store your configuration files in a different location, or with a different name, you can do that with `--config` or `-c:
 
 ```sh
-codeceptjs run --config=./path/to/my/config.json
+codeceptjs run --config=./path/to/my/config.js
+```
+
+## Common Configuration Patterns
+
+[`@codeceptjs/configure` package](https://github.com/codecept-js/configure) contains shared recipes for common configuration patterns. This allows to set meta-configuration, independent from a current helper enabled.
+
+Install it and enable to easily switch to headless/window mode, change window size, etc.
+
+```js
+const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure');
+
+setHeadlessWhen(process.env.CI);
+setWindowSize(1600, 1200);
+
+exports.config = {
+  // ...
+}
 ```
 
 ## Profile
diff --git a/docs/data.md b/docs/data.md
index cd26afe6..ef6568bf 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -137,7 +137,7 @@ Scenario('check post page', async (I)  => {
   // create a post and save its Id
   response = await I.sendMutation(
     'mutation createPost($input: PostInput!) { createPost(input: $input) { id }}',
-    { 
+    {
       input : {
         author: user.data.id,
         body: 'some text',
@@ -163,13 +163,13 @@ After((I) => {
 
 ## Data Generation with Factories
 
-This concept is extended by: 
+This concept is extended by:
 - [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory/) helper, and,
 - [GraphQLDataFactory](http://codecept.io/helpers/GraphQLDataFactory/) helper.
 
 These helpers build data according to defined rules and use REST API or GraphQL mutations to store them and automatically clean them up after a test.
 
-Just define how many items of any kind you need and the data factory helper will create them for you. 
+Just define how many items of any kind you need and the data factory helper will create them for you.
 
 To make this work some preparations are required.
 
@@ -312,9 +312,33 @@ By doing this we can make requests within the current browser session without a
 
 > Sharing browser session with ApiDataFactory or GraphQLDataFactory can be especially useful when you test Single Page Applications
 
-For example, let's see how to configure ApiDataFactory alongside with WebDriver to share cookies:
+Since CodeceptJS 2.3.3 there is a simple way to enable shared session for browser and data helpers.
+Install [`@codeceptjs/configure`](https://github.com/codecept-js/configure) package:
+
+```
+npm i @codeceptjs/configure --save
+```
+
+Import `setSharedCookies` function and call it inside a config:
+
+```js
+// in codecept.conf.js
+const { setSharedCookies } = require('@codeceptjs/configure');
+
+// share cookies between browser helpers and REST/GraphQL
+setSharedCookies();
+
+exports.config = {}
+```
+
+Without `setSharedCookies` you will need to update the config manually, so a data helper could receive cookies from a browser to make a request. If you would like to configure this process manually, here is an example of doing so:
 
 ```js
+
+let cookies; // share cookies
+
+exports.config = {
+helpers: {
   ApiDataFactory: {
     endpoint: 'http://local.app/api',
     cleanup: true,
@@ -329,7 +353,9 @@ For example, let's see how to configure ApiDataFactory alongside with WebDriver
       }
     },
     onRequest: async (request) => {
-      let cookies = await codeceptjs.container.helpers('WebDriver').grabCookie();
+      // get a cookie if it's not obtained yet
+      if (cookies) cookies = await codeceptjs.container.helpers('WebDriver').grabCookie();
+      // add cookies to request for a current request
       request.headers = { Cookie: cookies.map(c => `${c.name}=${c.value}`).join('; ') };
     },
   }
@@ -337,6 +363,7 @@ For example, let's see how to configure ApiDataFactory alongside with WebDriver
     url: 'https://local.app/',
     browser: 'chrome',
   }
+}
 ```
 
 In this case we are accessing WebDriver helper. However, you can replace WebDriver with any helper you use.
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 16fcfd92..292d9a7e 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -833,6 +833,36 @@ let data = await I.grabDataFromPerformanceTiming();
 
 
 
+### grabElementBoundingRect
+
+Grab the width, height, location of given locator.
+Provide `width` or `height`as second param to get your desired prop.
+Resumes test execution, so should be used inside an async function with `await` operator.
+
+Returns an object with `x`, `y`, `width`, `hegith` keys.
+
+```js
+const value = await I.grabElementBoundingRect('h3');
+// value is like { x: 226.5, y: 89, width: 527, height: 220 }
+```
+
+To get only one metric use second parameter:
+
+```js
+const width = await I.grabElementBoundingRect('h3', 'width');
+// width == 527
+```
+
+#### Parameters
+
+-   `locator` ([string][8] \| [object][6]) element located by CSS|XPath|strict locator.
+-   `prop`  
+-   `elementSize` [string][8] x, y, width or height of the given element.
+
+Returns [object][6] Element bounding rectangle
+
+
+
 ### grabHTMLFrom
 
 Retrieves the innerHTML from an element located by CSS or XPath and returns it to test.
@@ -1052,15 +1082,11 @@ To press a key in combination with modifier keys, pass the sequence as an array.
 I.pressKey(['Control', 'Z']);
 ```
 
-By default `pressKey` will change operation modifier key based on operating system.
-Mapping `'Control'` to `'Meta'` (also known as `'Command'`) on macOS machines or mapping `'Meta'` to `'Control'` on non-macOS machines.
-
-In order to use disable changing of operation modifier key based on operating system, specify the second argument as `false`.
+For specifying operation modifier key based on operating system it is suggested to use `'CommandOrControl'`.
+This will press `'Command'` (also known as `'Meta'`) on macOS machines and `'Control'` on non-macOS machines.
 
 ```js
-// On macOS this will actually press 'Control + Z' instead of
-// default behavior which would change keys to 'Meta + Z'
-I.pressKey(['Control', 'Z'], false);
+I.pressKey(['CommandOrControl', 'Z']);
 ```
 
 Some of the supported key names are:
@@ -1075,6 +1101,8 @@ Some of the supported key names are:
 -   `'Clear'`
 -   `'ControlLeft'` or `'Control'`
 -   `'ControlRight'`
+-   `'Command'`
+-   `'CommandOrControl'`
 -   `'Delete'`
 -   `'End'`
 -   `'Enter'`
@@ -1102,15 +1130,14 @@ Some of the supported key names are:
 #### Parameters
 
 -   `key` ([string][8] \| [array][15]) key or array of keys to press.
--   `detectOS` [boolean][16] (optional, `true` by default) change operation modifier key based on operating system.
     
-_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313][17]).
+_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313][16]).
 
 ### pressKeyDown
 
 Presses a key in the browser and leaves it in a down state.
 
-To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][18]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][17]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1121,7 +1148,6 @@ I.pressKeyUp('Control');
 #### Parameters
 
 -   `key` [string][8] name of key to press down.
--   `detectOS` [boolean][16] (optional, `true` by default) change operation modifier key based on operating system.
     
 
 
@@ -1129,7 +1155,7 @@ I.pressKeyUp('Control');
 
 Releases a key in the browser which was previously set to a down state.
 
-To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][18]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][17]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1140,7 +1166,6 @@ I.pressKeyUp('Control');
 #### Parameters
 
 -   `key` [string][8] name of key to release.
--   `detectOS` [boolean][16] (optional, `true` by default) change operation modifier key based on operating system.
     
 
 
@@ -1207,7 +1232,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
 #### Parameters
 
 -   `fileName` [string][8] file name to save.
--   `fullPage` [boolean][16] (optional, `false` by default) flag to enable fullscreen screenshot mode.
+-   `fullPage` [boolean][18] (optional, `false` by default) flag to enable fullscreen screenshot mode.
     
 
 
@@ -1940,10 +1965,10 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
 
-[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+[16]: https://github.com/GoogleChrome/puppeteer/issues/1313
 
-[17]: https://github.com/GoogleChrome/puppeteer/issues/1313
+[17]: #click
 
-[18]: #click
+[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
 [19]: https://codecept.io/react
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index d66246ac..777f6172 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -24,7 +24,6 @@ This helper should be configured in codecept.json or codecept.conf.js
 -   `port`:  - WebDriver port to connect.
 -   `protocol`:  - protocol for WebDriver server.
 -   `path`:  - path to WebDriver server,
--   `remoteFileUpload`:  - upload file to remote server when running `attachFile`.
 -   `restart`:  - restart browser between tests.
 -   `smartWait`: (optional) enables [SmartWait][3]; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
 -   `disableScreenshots`:  - don't save screenshots on failure.
@@ -963,6 +962,36 @@ Useful for referencing it when calling `I.switchToWindow(handle)`
 const window = await I.grabCurrentWindowHandle();
 ```
 
+### grabElementBoundingRect
+
+Grab the width, height, location of given locator.
+Provide `width` or `height`as second param to get your desired prop.
+Resumes test execution, so should be used inside an async function with `await` operator.
+
+Returns an object with `x`, `y`, `width`, `hegith` keys.
+
+```js
+const value = await I.grabElementBoundingRect('h3');
+// value is like { x: 226.5, y: 89, width: 527, height: 220 }
+```
+
+To get only one metric use second parameter:
+
+```js
+const width = await I.grabElementBoundingRect('h3', 'width');
+// width == 527
+```
+
+#### Parameters
+
+-   `locator` ([string][19] \| [object][20]) element located by CSS|XPath|strict locator.
+-   `prop`  
+-   `elementSize` [string][19] x, y, width or height of the given element.
+
+Returns [object][20] Element bounding rectangle
+
+
+
 ### grabGeoLocation
 
 Return the current geo location 
@@ -1150,15 +1179,11 @@ To press a key in combination with modifier keys, pass the sequence as an array.
 I.pressKey(['Control', 'Z']);
 ```
 
-By default `pressKey` will change operation modifier key based on operating system.
-Mapping `'Control'` to `'Meta'` (also known as `'Command'`) on macOS machines or mapping `'Meta'` to `'Control'` on non-macOS machines.
-
-In order to use disable changing of operation modifier key based on operating system, specify the second argument as `false`.
+For specifying operation modifier key based on operating system it is suggested to use `'CommandOrControl'`.
+This will press `'Command'` (also known as `'Meta'`) on macOS machines and `'Control'` on non-macOS machines.
 
 ```js
-// On macOS this will actually press 'Control + Z' instead of
-// default behavior which would change keys to 'Meta + Z'
-I.pressKey(['Control', 'Z'], false);
+I.pressKey(['CommandOrControl', 'Z']);
 ```
 
 Some of the supported key names are:
@@ -1173,6 +1198,8 @@ Some of the supported key names are:
 -   `'Clear'`
 -   `'ControlLeft'` or `'Control'`
 -   `'ControlRight'`
+-   `'Command'`
+-   `'CommandOrControl'`
 -   `'Delete'`
 -   `'End'`
 -   `'Enter'`
@@ -1200,7 +1227,6 @@ Some of the supported key names are:
 #### Parameters
 
 -   `key` ([string][19] \| [array][27]) key or array of keys to press.
--   `detectOS` [boolean][28] (optional, `true` by default) change operation modifier key based on operating system.
     
 _Note:_ In case a text field or textarea is focused be aware that some browsers do not respect active modifier when combining modifier keys with other keys.
 
@@ -1208,7 +1234,7 @@ _Note:_ In case a text field or textarea is focused be aware that some browsers
 
 Presses a key in the browser and leaves it in a down state.
 
-To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][29]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][28]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1219,7 +1245,6 @@ I.pressKeyUp('Control');
 #### Parameters
 
 -   `key` [string][19] name of key to press down.
--   `detectOS` [boolean][28] (optional, `true` by default) change operation modifier key based on operating system.
     
 
 
@@ -1227,7 +1252,7 @@ I.pressKeyUp('Control');
 
 Releases a key in the browser which was previously set to a down state.
 
-To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][29]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][28]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1238,7 +1263,6 @@ I.pressKeyUp('Control');
 #### Parameters
 
 -   `key` [string][19] name of key to release.
--   `detectOS` [boolean][28] (optional, `true` by default) change operation modifier key based on operating system.
     
 
 
@@ -1330,7 +1354,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
 #### Parameters
 
 -   `fileName` [string][19] file name to save.
--   `fullPage` [boolean][28] (optional, `false` by default) flag to enable fullscreen screenshot mode.
+-   `fullPage` [boolean][29] (optional, `false` by default) flag to enable fullscreen screenshot mode.
     
  
 
@@ -2072,8 +2096,8 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
 
-[28]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+[28]: #click
 
-[29]: #click
+[29]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
 [30]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
diff --git a/docs/plugins.md b/docs/plugins.md
index 616b1ba0..683271d8 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -188,11 +188,11 @@ autoLogin: {
          I.fillField('email', 'user@site.com');
          I.fillField('password', '123456');
          I.click('Login');
-      }
+      },
       check: (I) => {
          I.amOnPage('/');
          I.see('User', '.navbar');
-      }
+      },
     },
     admin: {
       login: (I) => {
@@ -200,10 +200,11 @@ autoLogin: {
          I.fillField('email', 'admin@site.com');
          I.fillField('password', '123456');
          I.click('Login');
-      }
+      },
       check: (I) => {
          I.amOnPage('/');
-         I.see('Admin', '.navbar'),
+         I.see('Admin', '.navbar');
+      },
     },
   }
 }
@@ -253,7 +254,7 @@ plugins: {
    autoLogin: {
     admin: {
       login: (I) => I.loginAsAdmin(),
-      check: (I) => I.see('Admin', '.navbar');
+      check: (I) => I.see('Admin', '.navbar'),
       fetch: (I) => {
         return I.executeScript(() => localStorage.getItem('session_id'));
       },
@@ -286,7 +287,7 @@ autoLogin: {
       check: (I) => {
          I.amOnPage('/');
          I.see('Admin');
-      }
+      },
     }
   }
 }
@@ -302,6 +303,75 @@ Scenario('login', async (I, login) => {
 
 -   `config`  
 
+## customLocator
+
+Creates a [custom locator][3] by using special attributes in HTML.
+
+If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests,
+you can enable this plugin to simplify matching elements with these attributes:
+
+```js
+// replace this:
+I.click({ css: '[data-test-id=register_button]');
+// with this:
+I.click('$register_button');
+```
+
+This plugin will create a valid XPath locator for you.
+
+#### Configuration
+
+-   `enabled` (default: `false`) should a locator be enabled
+-   `prefix` (default: `$`) sets a prefix for a custom locator.
+-   `attribute` (default: `data-test-id`) to set an attribute to be matched.
+-   `strategy` (default: `xpath`) actual locator strategy to use in query (`css` or `xpath`).
+-   `showActual` (default: false) show in the output actually produced XPath or CSS locator. By default shows custom locator value.
+
+#### Examples:
+
+Using `data-test` attribute with `$` prefix:
+
+```js
+// in codecept.conf.js
+plugins: {
+ customLocator: {
+   enabled: true
+   attribute: 'data-test'
+ }
+}
+```
+
+In a test:
+
+```js
+I.seeElement('$user'); // matches => [data-test=user]
+I.click('$sign-up'); // matches => [data-test=sign-up]
+```
+
+Using `data-qa` attribute with `=` prefix:
+
+```js
+// in codecept.conf.js
+plugins: {
+ customLocator: {
+   enabled: true
+   prefix: '=',
+   attribute: 'data-qa'
+ }
+}
+```
+
+In a test:
+
+```js
+I.seeElement('=user'); // matches => [data-qa=user]
+I.click('=sign-up'); // matches => [data-qa=sign-up]
+```
+
+### Parameters
+
+-   `config`  
+
 ## puppeteerCoverage
 
 Dumps puppeteers code coverage after every test.
@@ -320,7 +390,7 @@ plugins: {
 
 Possible config options:
 
--   `outputDir`: directory to dump coverage files
+-   `coverageDir`: directory to dump coverage files
 -   `uniqueFileName`: generate a unique filename by adding uuid
 
     First of all, your mileage may vary!
@@ -333,9 +403,9 @@ Possible config options:
 
     Links:
 
--   [https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage][3]
--   [https://github.com/istanbuljs/puppeteer-to-istanbul][4]
--   [https://github.com/gotwarlost/istanbul][5]
+-   [https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage][4]
+-   [https://github.com/istanbuljs/puppeteer-to-istanbul][5]
+-   [https://github.com/gotwarlost/istanbul][6]
 
 ### Parameters
 
@@ -439,7 +509,7 @@ Possible config options:
 
 ## stepByStepReport
 
-![step-by-step-report][6]
+![step-by-step-report][7]
 
 Generates step by step report for a test.
 After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow.
@@ -484,7 +554,7 @@ This plugin allows to run webdriverio services like:
 -   browserstack
 -   appium
 
-A complete list of all available services can be found on [webdriverio website][7].
+A complete list of all available services can be found on [webdriverio website][8].
 
 ###### Setup
 
@@ -496,7 +566,7 @@ See examples below:
 
 ###### Selenium Standalone Service
 
-Install `@wdio/selenium-standalone-service` package, as [described here][8].
+Install `@wdio/selenium-standalone-service` package, as [described here][9].
 It is important to make sure it is compatible with current webdriverio version.
 
 Enable `wdio` plugin in plugins list and add `selenium-standalone` service:
@@ -515,7 +585,7 @@ Please note, this service can be used with Protractor helper as well!
 
 ##### Sauce Service
 
-Install `@wdio/sauce-service` package, as [described here][9].
+Install `@wdio/sauce-service` package, as [described here][10].
 It is important to make sure it is compatible with current webdriverio version.
 
 Enable `wdio` plugin in plugins list and add `sauce` service:
@@ -549,16 +619,18 @@ In the same manner additional services from webdriverio can be installed, enable
 
 [2]: https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md
 
-[3]: https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
+[3]: https://codecept.io/locators#custom-locators
+
+[4]: https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
 
-[4]: https://github.com/istanbuljs/puppeteer-to-istanbul
+[5]: https://github.com/istanbuljs/puppeteer-to-istanbul
 
-[5]: https://github.com/gotwarlost/istanbul
+[6]: https://github.com/gotwarlost/istanbul
 
-[6]: https://codecept.io/img/codeceptjs-slideshow.gif
+[7]: https://codecept.io/img/codeceptjs-slideshow.gif
 
-[7]: https://webdriver.io
+[8]: https://webdriver.io
 
-[8]: https://webdriver.io/docs/selenium-standalone-service.html
+[9]: https://webdriver.io/docs/selenium-standalone-service.html
 
-[9]: https://webdriver.io/docs/sauce-service.html
+[10]: https://webdriver.io/docs/sauce-service.html
diff --git a/docs/webapi/grabElementBoundingRect.mustache b/docs/webapi/grabElementBoundingRect.mustache
index 87fb150e..1773bde9 100644
--- a/docs/webapi/grabElementBoundingRect.mustache
+++ b/docs/webapi/grabElementBoundingRect.mustache
@@ -2,30 +2,19 @@ Grab the width, height, location of given locator.
 Provide `width` or `height`as second param to get your desired prop.
 Resumes test execution, so **should be used inside an async function with `await`** operator.
 
+Returns an object with `x`, `y`, `width`, `hegith` keys.
+
 ```js
-/**
-* return an object
-* one found element
-* { x: 226.5, y: 89, width: 527, height: 220 }
-*
-* multiple found elements
-* {
-*  '0': { x: 296, y: 322.5, width: 387, height: 34 },
-*  '1': { x: 350.734375, y: 393, width: 127.40625, height: 36 },
-*  '2': { x: 489.765625, y: 393, width: 142.5, height: 36 }
-* }
-*/
 const value = await I.grabElementBoundingRect('h3');
-/**
-* return only the width
-* value = 200
-*/
-value = {x: 1, y: 2, width: 200, height: 200}
-const value = await I.grabElementBoundingRect('h3', 'width');
+// value is like { x: 226.5, y: 89, width: 527, height: 220 }
 ```
 
-If multiple elements found returns an object.
+To get only one metric use second parameter:
 
+```js
+const width = await I.grabElementBoundingRect('h3', 'width');
+// width == 527
+```
 @param {string|object} locator element located by CSS|XPath|strict locator.
-@param {string} elementSize the width or height of the given element.
-@returns {object|number} Element bounding rectangle
\ No newline at end of file
+@param {string} elementSize x, y, width or height of the given element.
+@returns {object} Element bounding rectangle
\ No newline at end of file
diff --git a/docs/webapi/pressKeyDown.mustache b/docs/webapi/pressKeyDown.mustache
index e26f8e80..d3bd5a41 100644
--- a/docs/webapi/pressKeyDown.mustache
+++ b/docs/webapi/pressKeyDown.mustache
@@ -9,4 +9,3 @@ I.pressKeyUp('Control');
 ```
 
 @param {string} key name of key to press down.
-@param {boolean} detectOS (optional, `true` by default) change operation modifier key based on operating system.
diff --git a/docs/webapi/pressKeyUp.mustache b/docs/webapi/pressKeyUp.mustache
index a08d2f51..36daa9dd 100644
--- a/docs/webapi/pressKeyUp.mustache
+++ b/docs/webapi/pressKeyUp.mustache
@@ -9,4 +9,3 @@ I.pressKeyUp('Control');
 ```
 
 @param {string} key name of key to release.
-@param {boolean} detectOS (optional, `true` by default) change operation modifier key based on operating system.
diff --git a/docs/webapi/pressKeyWithDetectOS.mustache b/docs/webapi/pressKeyWithKeyNormalization.mustache
similarity index 63%
rename from docs/webapi/pressKeyWithDetectOS.mustache
rename to docs/webapi/pressKeyWithKeyNormalization.mustache
index fcff7950..f7ec2a6d 100644
--- a/docs/webapi/pressKeyWithDetectOS.mustache
+++ b/docs/webapi/pressKeyWithKeyNormalization.mustache
@@ -12,15 +12,11 @@ To press a key in combination with modifier keys, pass the sequence as an array.
 I.pressKey(['Control', 'Z']);
 ```
 
-By default `pressKey` will change operation modifier key based on operating system.
-Mapping `'Control'` to `'Meta'` (also known as `'Command'`) on macOS machines or mapping `'Meta'` to `'Control'` on non-macOS machines.
-
-In order to use disable changing of operation modifier key based on operating system, specify the second argument as `false`.
+For specifying operation modifier key based on operating system it is suggested to use `'CommandOrControl'`.
+This will press `'Command'` (also known as `'Meta'`) on macOS machines and `'Control'` on non-macOS machines.
 
 ```js
-// On macOS this will actually press 'Control + Z' instead of
-// default behavior which would change keys to 'Meta + Z'
-I.pressKey(['Control', 'Z'], false);
+I.pressKey(['CommandOrControl', 'Z']);
 ```
 
 Some of the supported key names are:
@@ -34,6 +30,8 @@ Some of the supported key names are:
 - `'Clear'`
 - `'ControlLeft'` or `'Control'`
 - `'ControlRight'`
+- `'Command'`
+- `'CommandOrControl'`
 - `'Delete'`
 - `'End'`
 - `'Enter'`
@@ -59,4 +57,3 @@ Some of the supported key names are:
 - `'Tab'`
 
 @param {string|array} key key or array of keys to press.
-@param {boolean} detectOS (optional, `true` by default) change operation modifier key based on operating system.
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index fdebffb6..e1f916b6 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -1046,8 +1046,8 @@ class Puppeteer extends Helper {
   /**
    * {{> pressKeyDown }}
    */
-  async pressKeyDown(key, detectOS) {
-    key = getNormalizedKey.call(this, key, detectOS);
+  async pressKeyDown(key) {
+    key = getNormalizedKey.call(this, key);
     await this.page.keyboard.down(key);
     return this._waitForAction();
   }
@@ -1055,22 +1055,22 @@ class Puppeteer extends Helper {
   /**
    * {{> pressKeyUp }}
    */
-  async pressKeyUp(key, detectOS) {
-    key = getNormalizedKey.call(this, key, detectOS);
+  async pressKeyUp(key) {
+    key = getNormalizedKey.call(this, key);
     await this.page.keyboard.up(key);
     return this._waitForAction();
   }
 
   /**
-   * {{> pressKeyWithDetectOS }}
+   * {{> pressKeyWithKeyNormalization }}
    *
    * _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
    */
-  async pressKey(key, detectOS) {
+  async pressKey(key) {
     const modifiers = [];
     if (Array.isArray(key)) {
       for (let k of key) {
-        k = getNormalizedKey.call(this, k, detectOS);
+        k = getNormalizedKey.call(this, k);
         if (isModifierKey(k)) {
           modifiers.push(k);
         } else {
@@ -1079,7 +1079,7 @@ class Puppeteer extends Helper {
         }
       }
     } else {
-      key = getNormalizedKey.call(this, key, detectOS);
+      key = getNormalizedKey.call(this, key);
     }
     for (const modifier of modifiers) {
       await this.page.keyboard.down(modifier);
@@ -2015,31 +2015,14 @@ class Puppeteer extends Helper {
   }
 
   /**
-   * {{> grabElementSize }}
-   *
+   * {{> grabElementBoundingRect }}
    */
   async grabElementBoundingRect(locator, prop) {
-    let els = await this._locate(locator);
-    els = await Promise.all(els.map(el => el.boundingBox()));
-
-    const result = [];
-    els = els.filter(v => v);
-
-    if (prop) {
-      els.forEach((element) => {
-        result.push(element[prop]);
-      });
-    } else {
-      els.forEach((element) => {
-        result.push(element);
-      });
-    }
-
-    if (result.length === 1) {
-      return result[0];
-    }
-
-    return Object.assign({}, result);
+    const els = await this._locate(locator);
+    assertElementExists(els, locator);
+    const rect = await els[0].boundingBox();
+    if (prop) return rect[prop];
+    return rect;
   }
 }
 
@@ -2411,8 +2394,8 @@ const keyDefinitionMap = {
   /* eslint-enable quote-props */
 };
 
-function getNormalizedKey(key, detectOS) {
-  const normalizedKey = getNormalizedKeyAttributeValue(key, detectOS);
+function getNormalizedKey(key) {
+  const normalizedKey = getNormalizedKeyAttributeValue(key);
   if (key !== normalizedKey) {
     this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`);
   }
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index dd44069c..7552000b 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -1,8 +1,6 @@
 const Helper = require('../helper');
 const requireg = require('requireg');
-
-let axios = requireg('axios');
-let headers = {};
+const axios = require('axios').default;
 
 /**
  * REST helper allows to send additional requests to the REST API during acceptance tests.
@@ -42,14 +40,13 @@ let headers = {};
 class REST extends Helper {
   constructor(config) {
     super(config);
-    axios = requireg('axios');
     this.options = {
       timeout: 10000,
       defaultHeaders: {},
       endpoint: '',
     };
     this.options = Object.assign(this.options, config);
-    headers = Object.assign({}, this.options.defaultHeaders);
+    this.headers = Object.assign({}, this.options.defaultHeaders);
     axios.defaults.headers = this.options.defaultHeaders;
   }
 
@@ -69,8 +66,8 @@ class REST extends Helper {
   async _executeRequest(request) {
     axios.defaults.timeout = request.timeout || this.options.timeout;
 
-    if (headers && headers.auth) {
-      request.auth = headers.auth;
+    if (this.headers && this.headers.auth) {
+      request.auth = this.headers.auth;
     }
 
     if ((typeof request.data) === 'string') {
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 25d81f9d..faf01e34 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -44,7 +44,6 @@ const webRoot = 'body';
  * * `port`: (optional, default: 4444) - WebDriver port to connect.
  * * `protocol`: (optional, default: http) - protocol for WebDriver server.
  * * `path`: (optional, default: /wd/hub) - path to WebDriver server,
- * * `remoteFileUpload`: (optional, default: true) - upload file to remote server when running `attachFile`.
  * * `restart`: (optional, default: true) - restart browser between tests.
  * * `smartWait`: (optional) **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
  * * `disableScreenshots`: (optional, default: false) - don't save screenshots on failure.
@@ -381,7 +380,6 @@ class WebDriver extends Helper {
   _validateConfig(config) {
     const defaults = {
       logLevel: 'silent',
-
       // codeceptjs
       remoteFileUpload: true,
       smartWait: 0,
@@ -1495,8 +1493,8 @@ class WebDriver extends Helper {
   /**
    * {{> pressKeyDown }}
    */
-  async pressKeyDown(key, detectOS) {
-    key = getNormalizedKey.call(this, key, detectOS);
+  async pressKeyDown(key) {
+    key = getNormalizedKey.call(this, key);
     if (!this.browser.isW3C) {
       return this.browser.sendKeys([key]);
     }
@@ -1513,8 +1511,8 @@ class WebDriver extends Helper {
   /**
    * {{> pressKeyUp }}
    */
-  async pressKeyUp(key, detectOS) {
-    key = getNormalizedKey.call(this, key, detectOS);
+  async pressKeyUp(key) {
+    key = getNormalizedKey.call(this, key);
     if (!this.browser.isW3C) {
       return this.browser.sendKeys([key]);
     }
@@ -1529,15 +1527,15 @@ class WebDriver extends Helper {
   }
 
   /**
-   * {{> pressKeyWithDetectOS }}
+   * {{> pressKeyWithKeyNormalization }}
    *
    * _Note:_ In case a text field or textarea is focused be aware that some browsers do not respect active modifier when combining modifier keys with other keys.
    */
-  async pressKey(key, detectOS) {
+  async pressKey(key) {
     const modifiers = [];
     if (Array.isArray(key)) {
       for (let k of key) {
-        k = getNormalizedKey.call(this, k, detectOS);
+        k = getNormalizedKey.call(this, k);
         if (isModifierKey(k)) {
           modifiers.push(k);
         } else {
@@ -1546,7 +1544,7 @@ class WebDriver extends Helper {
         }
       }
     } else {
-      key = getNormalizedKey.call(this, key, detectOS);
+      key = getNormalizedKey.call(this, key);
     }
     for (const modifier of modifiers) {
       await this.pressKeyDown(modifier);
@@ -2114,34 +2112,15 @@ class WebDriver extends Helper {
 
   /**
    * {{> grabElementBoundingRect }}
-   *
    */
   async grabElementBoundingRect(locator, prop) {
     const res = await this._locate(locator, true);
     assertElementExists(res, locator);
+    const el = usingFirstElement(res);
 
-    const arr1 = await forEachAsync(res, async el => el.getLocation());
-    const arr2 = await forEachAsync(res, async el => el.getSize());
-    const arr = [];
-    for (let index = 0; index < arr1.length; index++) {
-      arr.push(Object.assign({}, arr1[index], arr2[index]));
-    }
-    const result = [];
-    if (prop) {
-      arr.forEach((element) => {
-        result.push(element[prop]);
-      });
-    } else {
-      arr.forEach((element) => {
-        result.push(element);
-      });
-    }
-
-    if (result.length === 1) {
-      return result[0];
-    }
-
-    return Object.assign({}, result);
+    const rect = Object.assign({}, await el.getLocation(), await el.getSize());
+    if (prop) return rect[prop];
+    return rect;
   }
 
   /**
@@ -2557,8 +2536,8 @@ function convertKeyToRawKey(key) {
   return key;
 }
 
-function getNormalizedKey(key, detectOS) {
-  let normalizedKey = getNormalizedKeyAttributeValue(key, detectOS);
+function getNormalizedKey(key) {
+  let normalizedKey = getNormalizedKeyAttributeValue(key);
   // Always use "left" modifier keys for non-W3C sessions,
   // as JsonWireProtocol does not support "right" modifier keys
   if (!this.browser.isW3C) {
diff --git a/lib/plugin/autoLogin.js b/lib/plugin/autoLogin.js
index 9c3fb4e4..1f68444e 100644
--- a/lib/plugin/autoLogin.js
+++ b/lib/plugin/autoLogin.js
@@ -101,11 +101,11 @@ const defaultConfig = {
  *          I.fillField('email', 'user@site.com');
  *          I.fillField('password', '123456');
  *          I.click('Login');
- *       }
+ *       },
  *       check: (I) => {
  *          I.amOnPage('/');
  *          I.see('User', '.navbar');
- *       }
+ *       },
  *     },
  *     admin: {
  *       login: (I) => {
@@ -113,10 +113,11 @@ const defaultConfig = {
  *          I.fillField('email', 'admin@site.com');
  *          I.fillField('password', '123456');
  *          I.click('Login');
- *       }
+ *       },
  *       check: (I) => {
  *          I.amOnPage('/');
- *          I.see('Admin', '.navbar'),
+ *          I.see('Admin', '.navbar');
+ *       },
  *     },
  *   }
  * }
@@ -166,7 +167,7 @@ const defaultConfig = {
  *    autoLogin: {
  *     admin: {
  *       login: (I) => I.loginAsAdmin(),
- *       check: (I) => I.see('Admin', '.navbar');
+ *       check: (I) => I.see('Admin', '.navbar'),
  *       fetch: (I) => {
  *         return I.executeScript(() => localStorage.getItem('session_id'));
  *       },
@@ -199,7 +200,7 @@ const defaultConfig = {
  *       check: (I) => {
  *          I.amOnPage('/');
  *          I.see('Admin');
- *       }
+ *       },
  *     }
  *   }
  * }
diff --git a/lib/utils.js b/lib/utils.js
index e58f384c..9475128e 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -391,26 +391,20 @@ function normalizeKeyReplacer(match, prefix, key, suffix, offset, string) {
   return normalizedKey + position.charAt(0).toUpperCase() + position.substr(1).toLowerCase();
 }
 
-module.exports.getNormalizedKeyAttributeValue = function (key, detectOS = true) {
+module.exports.getNormalizedKeyAttributeValue = function (key) {
+  // Use operation modifier key based on operating system
+  key = key.replace(/(Ctrl|Control|Cmd|Command)[ _]?Or[ _]?(Ctrl|Control|Cmd|Command)/i, os.platform() === 'darwin' ? 'Meta' : 'Control');
   // Selection of keys (https://www.w3.org/TR/uievents-key/#named-key-attribute-values)
   // which can be written in various ways and should be normalized.
   // For example 'LEFT ALT', 'ALT_Left', 'alt left' or 'LeftAlt' will be normalized as 'AltLeft'.
-  key = key.replace(/^\s*(?:(Down|Left|Right|Up)[ _]?)?(Arrow|Alt|Ctrl|Control|Command|Meta|Option|Page|Shift)(?:[ _]?(Down|Left|Right|Up))?\s*$/i, normalizeKeyReplacer);
+  key = key.replace(/^\s*(?:(Down|Left|Right|Up)[ _]?)?(Arrow|Alt|Ctrl|Control|Cmd|Command|Meta|Option|OS|Page|Shift|Super)(?:[ _]?(Down|Left|Right|Up|Gr(?:aph)?))?\s*$/i, normalizeKeyReplacer);
   // Map alias to corresponding key value
   key = key.replace(/^(Add|Divide|Decimal|Multiply|Subtract)$/, 'Numpad$1');
-  key = key.replace('AltGr', 'AltGraph');
-  key = key.replace('Command', 'Meta');
+  key = key.replace(/^AltGr$/, 'AltGraph');
+  key = key.replace(/^(Cmd|Command|Os|Super)/, 'Meta');
   key = key.replace('Ctrl', 'Control');
   key = key.replace('Option', 'Alt');
-  key = key.replace(/^NumpadComma|Separator$/, 'Comma');
-  // Change operation modifier key based on operating system
-  if (detectOS) {
-    if (os.platform() === 'darwin') {
-      key = key.replace('Control', 'Meta');
-    } else {
-      key = key.replace('Meta', 'Control');
-    }
-  }
+  key = key.replace(/^(NumpadComma|Separator)$/, 'Comma');
   return key;
 };
 
diff --git a/package.json b/package.json
index 72f9310a..ee0fb07b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "codeceptjs",
-  "version": "2.3.2",
+  "version": "2.3.3",
   "description": "Modern Era Acceptance Testing Framework for NodeJS",
   "keywords": [
     "acceptance",
@@ -39,7 +39,6 @@
     "dev:graphql": "nodemon test/data/graphql/index.js"
   },
   "dependencies": {
-    "@codeceptjs/configure": "^0.3.0",
     "allure-js-commons": "^1.3.2",
     "axios": "^0.19.0",
     "chalk": "^1.1.3",


Test patch:
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 909a7ce3..b0f3dd14 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -451,18 +451,13 @@ describe('Puppeteer', function () {
       await I.seeInField('Name', '!ABC123');
     });
 
-    it('should not change operator modifier key', async () => {
+    it('should use modifier key based on operating system', async () => {
       await I.amOnPage('/form/field');
-      await I.appendField('Name', '-');
-
-      // Does nothing, because operator modifier key is not changed for operating system
-      await I.pressKey(['Right Meta', 'a'], false); // Select all
-      await I.pressKey('Backspace');
-      await I.seeInField('Name', 'OLD_VALUE');
+      await I.fillField('Name', 'value that is cleared using select all shortcut');
 
-      await I.pressKey(['Right Meta', 'a']); // Select all ('Meta' is changed to 'Control')
+      await I.pressKey(['ControlOrCommand', 'a']);
       await I.pressKey('Backspace');
-      await I.dontSeeInField('Name', 'OLD_VALUE');
+      await I.dontSeeInField('Name', 'value that is cleared using select all shortcut');
     });
 
     it('should show correct numpad or punctuation key when Shift modifier is active', async () => {
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 2b4e7134..99ca7092 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -27,7 +27,6 @@ describe('WebDriver', function () {
       url: siteUrl,
       browser: 'chrome',
       windowSize: '500x700',
-      remoteFileUpload: true,
       smartWait: 0, // just to try
       host: TestHelper.seleniumHost(),
       port: TestHelper.seleniumPort(),
@@ -190,18 +189,13 @@ describe('WebDriver', function () {
       await wd.seeInField('Name', '!ABC123');
     });
 
-    it('should not change operator modifier key', async () => {
+    it('should use modifier key based on operating system', async () => {
       await wd.amOnPage('/form/field');
-      await wd.appendField('Name', '-');
-
-      // Does nothing, because operator modifier key is not changed for operating system
-      await wd.pressKey(['Right Meta', 'a'], false); // Select all
-      await wd.pressKey('Backspace');
-      await wd.seeInField('Name', 'OLD_VALUE');
+      await wd.fillField('Name', 'value that is cleared using select all shortcut');
 
-      await wd.pressKey(['Right Meta', 'a']); // Select all ('Meta' is changed to 'Control')
+      await wd.pressKey(['CommandOrControl', 'A']);
       await wd.pressKey('Backspace');
-      await wd.dontSeeInField('Name', 'OLD_VALUE');
+      await wd.dontSeeInField('Name', 'value that is cleared using select all shortcut');
     });
 
     it('should show correct numpad or punctuation key when Shift modifier is active', async () => {
diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js
index 42fe69e1..985ec211 100644
--- a/test/unit/utils_test.js
+++ b/test/unit/utils_test.js
@@ -1,5 +1,7 @@
 const utils = require('../../lib/utils');
 const assert = require('assert');
+const os = require('os');
+const sinon = require('sinon');
 
 describe('utils', () => {
   describe('#fileExists', () => {
@@ -233,4 +235,55 @@ describe('utils', () => {
       });
     });
   });
+
+  describe('#getNormalizedKeyAttributeValue', () => {
+    it('should normalize key (alias) to key attribute value', () => {
+      utils.getNormalizedKeyAttributeValue('Arrow down').should.equal('ArrowDown');
+      utils.getNormalizedKeyAttributeValue('RIGHT_ARROW').should.equal('ArrowRight');
+      utils.getNormalizedKeyAttributeValue('leftarrow').should.equal('ArrowLeft');
+      utils.getNormalizedKeyAttributeValue('Up arrow').should.equal('ArrowUp');
+
+      utils.getNormalizedKeyAttributeValue('Left Alt').should.equal('AltLeft');
+      utils.getNormalizedKeyAttributeValue('RIGHT_ALT').should.equal('AltRight');
+      utils.getNormalizedKeyAttributeValue('alt').should.equal('Alt');
+
+      utils.getNormalizedKeyAttributeValue('oPTION left').should.equal('AltLeft');
+      utils.getNormalizedKeyAttributeValue('ALTGR').should.equal('AltGraph');
+      utils.getNormalizedKeyAttributeValue('alt graph').should.equal('AltGraph');
+
+      utils.getNormalizedKeyAttributeValue('Control Left').should.equal('ControlLeft');
+      utils.getNormalizedKeyAttributeValue('RIGHT_CTRL').should.equal('ControlRight');
+      utils.getNormalizedKeyAttributeValue('Ctrl').should.equal('Control');
+
+      utils.getNormalizedKeyAttributeValue('Cmd').should.equal('Meta');
+      utils.getNormalizedKeyAttributeValue('LeftCommand').should.equal('MetaLeft');
+      utils.getNormalizedKeyAttributeValue('os right').should.equal('MetaRight');
+      utils.getNormalizedKeyAttributeValue('SUPER').should.equal('Meta');
+
+      utils.getNormalizedKeyAttributeValue('NumpadComma').should.equal('Comma');
+      utils.getNormalizedKeyAttributeValue('Separator').should.equal('Comma');
+
+      utils.getNormalizedKeyAttributeValue('Add').should.equal('NumpadAdd');
+      utils.getNormalizedKeyAttributeValue('Decimal').should.equal('NumpadDecimal');
+      utils.getNormalizedKeyAttributeValue('Divide').should.equal('NumpadDivide');
+      utils.getNormalizedKeyAttributeValue('Multiply').should.equal('NumpadMultiply');
+      utils.getNormalizedKeyAttributeValue('Subtract').should.equal('NumpadSubtract');
+    });
+
+    it('should normalize modifier key based on operating system', () => {
+      sinon.stub(os, 'platform', () => { return 'notdarwin'; });
+      utils.getNormalizedKeyAttributeValue('CmdOrCtrl').should.equal('Control');
+      utils.getNormalizedKeyAttributeValue('COMMANDORCONTROL').should.equal('Control');
+      utils.getNormalizedKeyAttributeValue('ControlOrCommand').should.equal('Control');
+      utils.getNormalizedKeyAttributeValue('left ctrl or command').should.equal('ControlLeft');
+      os.platform.restore();
+
+      sinon.stub(os, 'platform', () => { return 'darwin'; });
+      utils.getNormalizedKeyAttributeValue('CtrlOrCmd').should.equal('Meta');
+      utils.getNormalizedKeyAttributeValue('CONTROLORCOMMAND').should.equal('Meta');
+      utils.getNormalizedKeyAttributeValue('CommandOrControl').should.equal('Meta');
+      utils.getNormalizedKeyAttributeValue('right command or ctrl').should.equal('MetaRight');
+      os.platform.restore();
+    });
+  });
 });