Repository: Codeception/CodeceptJS
Pull request #1926 (2019-10-04 11:20:31)

implement grabElementBoundingRect for wdio and puppeteer helpers
https://github.com/Codeception/CodeceptJS/issues/1867
this would add `grabElementBoundingRect`method for WDIO and Puppeteer.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0de634d6..484fa5cf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,20 @@ Please try to add corresponding testcase to runner or unit.
 
 Documentation is stored in `/docs` directory in markdown format.
 
-**Documentation for helpers is a part of a source code**. Whenever you need to update docs for a helper do it inside a .js file. Then run
+**Documentation for helpers is a part of a source code**.
+Whenever you need to update docs for a helper do it inside a .js file.
+
+In order to generate new documentation from source code run the following command with [Robo](https://robo.li/):
 
 ```
-npm run docs
+robo docs:helpers
 ```
 
 To update markdown documentation. Shared documentation for helpers are located in `docs/webapi/*.mustache`. Inside a docblock those files can be included like this:
 
 ```js
   /**
-   * {{> ../webapi/click }}
+   * {{> click }}
    */
   click() {
     // ...
diff --git a/README.md b/README.md
index eef5ef2a..d6656830 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
 
 Reference: [Helpers API](https://github.com/Codeception/CodeceptJS/blob/master/docs) | [Demo](https://github.com/Codeception/codeceptjs-demo)
 
-## Modern Era Acceptance Testing Framework for NodeJS
+## Supercharged End 2 End Testing
 
 CodeceptJS is a new testing framework for end-to-end testing with WebDriver (or others).
 It abstracts browser interaction to simple steps which is written from a user perspective.
-A simple test that verifies that "Welcome" text is present on a main page of a site will look like:
+A simple test that verifies the "Welcome" text is present on a main page of a site will look like:
 
 ```js
 Feature('CodeceptJS demo');
@@ -32,6 +32,7 @@ CodeceptJS uses **Helper** modules to provide actions to `I` object. Currently C
 * [**TestCafe**](https://github.com/Codeception/CodeceptJS/blob/master/docs/helpers/TestCafe.md) - cheap and fast cross-browser test automation.
 * [**Nightmare**](https://github.com/Codeception/CodeceptJS/blob/master/docs/helpers/Nightmare.md) - uses Electron and NightmareJS to run tests.
 * [**Appium**](https://github.com/Codeception/CodeceptJS/blob/master/docs/helpers/Appium.md) - for **mobile testing** with Appium
+* [**Detox**](https://github.com/Codeception/CodeceptJS/blob/master/docs/helpers/Detox.md) - This is a wrapper on top of Detox library, aimied to unify testing experience for CodeceptJS framework. Detox provides a grey box testing for mobile applications, playing especially good for React Native apps.
 
 And more to come...
 
@@ -39,8 +40,7 @@ And more to come...
 
 CodeceptJS is a successor of [Codeception](http://codeception.com), a popular full-stack testing framework for PHP.
 With CodeceptJS your scenario-driven functional and acceptance tests will be as simple and clean as they can be.
-You don't need to worry about asynchronous nature of NodeJS or about various APIs of Selenium, Puppeteer, Protractor, TestCafe etc,
-as CodeceptJS unifies them and makes them work as they were synchronous.
+You don't need to worry about asynchronous nature of NodeJS or about various APIs of Selenium, Puppeteer, Protractor, TestCafe, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
 
 ## Features
 
@@ -118,7 +118,7 @@ In this examples all methods of `I` are taken from WebDriver helper, see [refere
 Let's execute this test with `run` command. Additional option `--steps` will show us the running process. We recommend use `--steps` or `--debug` during development.
 
 ```sh
-codeceptjs run --steps
+npx codeceptjs run --steps
 ```
 
 This will produce an output:
@@ -144,7 +144,7 @@ Just add `pause()` call at any place in a test and run it.
 Interactive shell can be started outside test context by running:
 
 ```sh
-codeceptjs shell
+npx codeceptjs shell
 ```
 
 ### Actions
@@ -182,13 +182,13 @@ In case you need to return a value from a webpage and use it directly in test, y
 They are expected to be used inside async/await functions, and their results will be available in test:
 
 ```js
-var assert = require('assert');
+let assert = require('assert');
 
 Feature('CodeceptJS Demonstration');
 
 Scenario('test page title', async (I) => {
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
-  var title = await I.grabTitle();
+  let title = await I.grabTitle();
   assert.equal(title, 'Example application with SimpleForm and Twitter Bootstrap');
 });
 ```
diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md
index 6665cfde..30c0aa65 100644
--- a/docs/helpers/Appium.md
+++ b/docs/helpers/Appium.md
@@ -225,9 +225,9 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -252,7 +252,7 @@ I.dontSeeElement('.modal'); // modal is not shown
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/docs/helpers/Nightmare.md b/docs/helpers/Nightmare.md
index 02fa3e8c..4c45ab56 100644
--- a/docs/helpers/Nightmare.md
+++ b/docs/helpers/Nightmare.md
@@ -221,9 +221,9 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -302,7 +302,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/docs/helpers/Protractor.md b/docs/helpers/Protractor.md
index 3fe28748..1571c8e9 100644
--- a/docs/helpers/Protractor.md
+++ b/docs/helpers/Protractor.md
@@ -357,9 +357,9 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -438,7 +438,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 9d76dde6..16fcfd92 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -203,9 +203,8 @@ libraries][7].
 
 ### amAcceptingPopups
 
-\[existingPages.length -1
-\[existingPages.length -1
-\[existingPages.length -1
+Set the automatic popup response to Accept.
+This must be set before a popup is triggered.
 
 ```js
 I.amAcceptingPopups();
@@ -432,9 +431,9 @@ This action supports [React locators](https://codecept.io/react#locators)
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -518,7 +517,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
@@ -587,7 +586,7 @@ This action supports [React locators](https://codecept.io/react#locators)
 
 ### downloadFile
 
-This method is depreacted.
+This method is deprecated.
 
 Please use `handleDownloads()` instead.
 
@@ -1039,54 +1038,111 @@ I.openNewTab();
 
 ### pressKey
 
-Presses a key on a focused element.
-Special keys like 'Enter', 'Control', [etc][14]
-will be replaced with corresponding unicode.
-If modifier key is used (Control, Command, Alt, Shift) in array, it will be released afterwards.
+Presses a key in the browser (on a focused element).
+
+_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][14].
 
 ```js
-I.pressKey('Enter');
-I.pressKey(['Control','a']);
+I.pressKey('Backspace');
 ```
 
+To press a key in combination with modifier keys, pass the sequence as an array. All modifier keys (`'Alt'`, `'Control'`, `'Meta'`, `'Shift'`) will be released afterwards.
+
+```js
+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`.
+
+```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);
+```
+
+Some of the supported key names are:
+
+-   `'AltLeft'` or `'Alt'`
+-   `'AltRight'`
+-   `'ArrowDown'`
+-   `'ArrowLeft'`
+-   `'ArrowRight'`
+-   `'ArrowUp'`
+-   `'Backspace'`
+-   `'Clear'`
+-   `'ControlLeft'` or `'Control'`
+-   `'ControlRight'`
+-   `'Delete'`
+-   `'End'`
+-   `'Enter'`
+-   `'Escape'`
+-   `'F1'` to `'F12'`
+-   `'Home'`
+-   `'Insert'`
+-   `'MetaLeft'` or `'Meta'`
+-   `'MetaRight'`
+-   `'Numpad0'` to `'Numpad9'`
+-   `'NumpadAdd'`
+-   `'NumpadDecimal'`
+-   `'NumpadDivide'`
+-   `'NumpadMultiply'`
+-   `'NumpadSubtract'`
+-   `'PageDown'`
+-   `'PageUp'`
+-   `'Pause'`
+-   `'Return'`
+-   `'ShiftLeft'` or `'Shift'`
+-   `'ShiftRight'`
+-   `'Space'`
+-   `'Tab'`
+
 #### 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]).
+
+### 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]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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.
     
 
 
+### pressKeyUp
+
+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]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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.
+    
 
-[Valid key names](https://w3c.github.io/webdriver/#keyboard-actions) are:
-
-- `'Add'`,
-- `'Alt'`,
-- `'ArrowDown'` or `'Down arrow'`,
-- `'ArrowLeft'` or `'Left arrow'`,
-- `'ArrowRight'` or `'Right arrow'`,
-- `'ArrowUp'` or `'Up arrow'`,
-- `'Backspace'`,
-- `'Command'`,
-- `'Control'`,
-- `'Del'`,
-- `'Divide'`,
-- `'End'`,
-- `'Enter'`,
-- `'Equals'`,
-- `'Escape'`,
-- `'F1 to F12'`,
-- `'Home'`,
-- `'Insert'`,
-- `'Meta'`,
-- `'Multiply'`,
-- `'Numpad 0'` to `'Numpad 9'`,
-- `'Pagedown'` or `'PageDown'`,
-- `'Pageup'` or `'PageUp'`,
-- `'Pause'`,
-- `'Semicolon'`,
-- `'Shift'`,
-- `'Space'`,
-- `'Subtract'`,
-- `'Tab'`.
 
 ### refreshPage
 
@@ -1767,7 +1823,7 @@ I.waitForVisible('#popup');
 -   `locator` ([string][8] \| [object][6]) element located by CSS|XPath|strict locator.
 -   `sec` [number][9] (optional, `1` by default) time in seconds to wait
     
-This method accepts [React selectors][17].
+This method accepts [React selectors][19].
 
 ### waitInUrl
 
@@ -1880,10 +1936,14 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [13]: https://codecept.io/helpers/FileSystem
 
-[14]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value
+[14]: #fillfield
 
 [15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
 
 [16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[17]: https://codecept.io/react
+[17]: https://github.com/GoogleChrome/puppeteer/issues/1313
+
+[18]: #click
+
+[19]: https://codecept.io/react
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 15be4e0d..d66246ac 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -592,9 +592,9 @@ This action supports [React locators](https://codecept.io/react#locators)
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -679,7 +679,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
@@ -1136,59 +1136,111 @@ I.openNewTab();
 
 ### pressKey
 
-Presses a key on a focused element.
-Special keys like 'Enter', 'Control', [etc][26]
-will be replaced with corresponding unicode.
-If modifier key is used (Control, Command, Alt, Shift) in array, it will be released afterwards.
+Presses a key in the browser (on a focused element).
+
+_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][26].
+
+```js
+I.pressKey('Backspace');
+```
+
+To press a key in combination with modifier keys, pass the sequence as an array. All modifier keys (`'Alt'`, `'Control'`, `'Meta'`, `'Shift'`) will be released afterwards.
+
+```js
+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`.
 
 ```js
-I.pressKey('Enter');
-I.pressKey(['Control','a']);
+// On macOS this will actually press 'Control + Z' instead of
+// default behavior which would change keys to 'Meta + Z'
+I.pressKey(['Control', 'Z'], false);
 ```
 
+Some of the supported key names are:
+
+-   `'AltLeft'` or `'Alt'`
+-   `'AltRight'`
+-   `'ArrowDown'`
+-   `'ArrowLeft'`
+-   `'ArrowRight'`
+-   `'ArrowUp'`
+-   `'Backspace'`
+-   `'Clear'`
+-   `'ControlLeft'` or `'Control'`
+-   `'ControlRight'`
+-   `'Delete'`
+-   `'End'`
+-   `'Enter'`
+-   `'Escape'`
+-   `'F1'` to `'F12'`
+-   `'Home'`
+-   `'Insert'`
+-   `'MetaLeft'` or `'Meta'`
+-   `'MetaRight'`
+-   `'Numpad0'` to `'Numpad9'`
+-   `'NumpadAdd'`
+-   `'NumpadDecimal'`
+-   `'NumpadDivide'`
+-   `'NumpadMultiply'`
+-   `'NumpadSubtract'`
+-   `'PageDown'`
+-   `'PageUp'`
+-   `'Pause'`
+-   `'Return'`
+-   `'ShiftLeft'` or `'Shift'`
+-   `'ShiftRight'`
+-   `'Space'`
+-   `'Tab'`
+
 #### 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.
 
+### 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`][29]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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.
     
 
 
-[Valid key names](https://w3c.github.io/webdriver/#keyboard-actions) are:
-
-- `'Add'`,
-- `'Alt'`,
-- `'ArrowDown'` or `'Down arrow'`,
-- `'ArrowLeft'` or `'Left arrow'`,
-- `'ArrowRight'` or `'Right arrow'`,
-- `'ArrowUp'` or `'Up arrow'`,
-- `'Backspace'`,
-- `'Command'`,
-- `'Control'`,
-- `'Del'`,
-- `'Divide'`,
-- `'End'`,
-- `'Enter'`,
-- `'Equals'`,
-- `'Escape'`,
-- `'F1 to F12'`,
-- `'Home'`,
-- `'Insert'`,
-- `'Meta'`,
-- `'Multiply'`,
-- `'Numpad 0'` to `'Numpad 9'`,
-- `'Pagedown'` or `'PageDown'`,
-- `'Pageup'` or `'PageUp'`,
-- `'Pause'`,
-- `'Semicolon'`,
-- `'Shift'`,
-- `'Space'`,
-- `'Subtract'`,
-- `'Tab'`.To make combinations with modifier and mouse clicks (like Ctrl+Click) press a modifier, click, then release it.```js
-    I.pressKey('Control');
-    I.click('#someelement');
-    I.pressKey('Control');
-    ```
+### pressKeyUp
+
+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]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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.
+    
+
 
 ### refreshPage
 
@@ -1635,7 +1687,7 @@ I.setCookie({name: 'auth', value: true});
 -   `cookie` [object][20] a cookie object.
     
 Uses Selenium's JSON [cookie
-    format][29].
+    format][30].
 
 ### setGeoLocation
 
@@ -2016,10 +2068,12 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
 
-[26]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value
+[26]: #fillfield
 
 [27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
 
 [28]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[29]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
+[29]: #click
+
+[30]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
diff --git a/docs/helpers/WebDriverIO.md b/docs/helpers/WebDriverIO.md
index a5592d93..43c63657 100644
--- a/docs/helpers/WebDriverIO.md
+++ b/docs/helpers/WebDriverIO.md
@@ -439,9 +439,9 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -514,7 +514,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/docs/webapi/dontSeeCheckboxIsChecked.mustache b/docs/webapi/dontSeeCheckboxIsChecked.mustache
index 85b835bb..93158410 100644
--- a/docs/webapi/dontSeeCheckboxIsChecked.mustache
+++ b/docs/webapi/dontSeeCheckboxIsChecked.mustache
@@ -1,9 +1,9 @@
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
-@param {string|object} field located by label|name|CSS|XPath|strict locator.
\ No newline at end of file
+@param {string|object} field located by label|name|CSS|XPath|strict locator.
diff --git a/docs/webapi/dontSeeInField.mustache b/docs/webapi/dontSeeInField.mustache
index 525504f6..d04b1c0d 100644
--- a/docs/webapi/dontSeeInField.mustache
+++ b/docs/webapi/dontSeeInField.mustache
@@ -1,4 +1,4 @@
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/docs/webapi/grabElementBoundingRect.mustache b/docs/webapi/grabElementBoundingRect.mustache
new file mode 100644
index 00000000..87fb150e
--- /dev/null
+++ b/docs/webapi/grabElementBoundingRect.mustache
@@ -0,0 +1,31 @@
+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.
+
+```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');
+```
+
+If multiple elements found returns an object.
+
+@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
diff --git a/docs/webapi/pressKeyDown.mustache b/docs/webapi/pressKeyDown.mustache
new file mode 100644
index 00000000..e26f8e80
--- /dev/null
+++ b/docs/webapi/pressKeyDown.mustache
@@ -0,0 +1,12 @@
+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`](#click)).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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
new file mode 100644
index 00000000..a08d2f51
--- /dev/null
+++ b/docs/webapi/pressKeyUp.mustache
@@ -0,0 +1,12 @@
+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`](#click)).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+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/pressKeyWithDetectOS.mustache
new file mode 100644
index 00000000..fcff7950
--- /dev/null
+++ b/docs/webapi/pressKeyWithDetectOS.mustache
@@ -0,0 +1,62 @@
+Presses a key in the browser (on a focused element).
+
+_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`](#fillfield).
+
+```js
+I.pressKey('Backspace');
+```
+
+To press a key in combination with modifier keys, pass the sequence as an array. All modifier keys (`'Alt'`, `'Control'`, `'Meta'`, `'Shift'`) will be released afterwards.
+
+```js
+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`.
+
+```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);
+```
+
+Some of the supported key names are:
+- `'AltLeft'` or `'Alt'`
+- `'AltRight'`
+- `'ArrowDown'`
+- `'ArrowLeft'`
+- `'ArrowRight'`
+- `'ArrowUp'`
+- `'Backspace'`
+- `'Clear'`
+- `'ControlLeft'` or `'Control'`
+- `'ControlRight'`
+- `'Delete'`
+- `'End'`
+- `'Enter'`
+- `'Escape'`
+- `'F1'` to `'F12'`
+- `'Home'`
+- `'Insert'`
+- `'MetaLeft'` or `'Meta'`
+- `'MetaRight'`
+- `'Numpad0'` to `'Numpad9'`
+- `'NumpadAdd'`
+- `'NumpadDecimal'`
+- `'NumpadDivide'`
+- `'NumpadMultiply'`
+- `'NumpadSubtract'`
+- `'PageDown'`
+- `'PageUp'`
+- `'Pause'`
+- `'Return'`
+- `'ShiftLeft'` or `'Shift'`
+- `'ShiftRight'`
+- `'Space'`
+- `'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/examples/codecept.conf.js b/examples/codecept.conf.js
index 6443b875..c8c324d1 100644
--- a/examples/codecept.conf.js
+++ b/examples/codecept.conf.js
@@ -1,9 +1,3 @@
-const { useHeadlessWhen, useSharedCookies } = require('@codeceptjs/config-hooks');
-
-useHeadlessWhen(process.env.headless);
-useSharedCookies();
-useWindowSize(1600, 1000);
-
 exports.config = {
   output: './output',
   helpers: {
@@ -12,7 +6,7 @@ exports.config = {
       browser: 'chrome',
       restart: false,
       windowSize: '1600x1200',
-      show: false,
+      show: true,
     },
     REST: {},
   },
diff --git a/lib/config.js b/lib/config.js
index c333ed10..4ca0b712 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,8 +1,11 @@
 const fs = require('fs');
 const path = require('path');
-const isFile = require('./utils').isFile;
-const fileExists = require('./utils').fileExists;
-const deepMerge = require('./utils').deepMerge;
+const {
+  fileExists,
+  isFile,
+  deepMerge,
+  deepClone,
+} = require('./utils');
 
 const defaultConfig = {
   output: './_output',
@@ -33,7 +36,7 @@ class Config {
    * @param {*} newConfig
    */
   static create(newConfig) {
-    config = deepMerge(defaultConfig, newConfig);
+    config = deepMerge(deepClone(defaultConfig), newConfig);
     hooks.forEach(f => f(config));
     return config;
   }
@@ -104,7 +107,7 @@ class Config {
    */
   static reset() {
     hooks = [];
-    return config = defaultConfig;
+    return config = Object.assign({}, defaultConfig);
   }
 }
 
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index 1f26890f..fdebffb6 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -14,6 +14,8 @@ const {
   chunkArray,
   convertCssPropertiesToCamelCase,
   screenshotOutputFolder,
+  getNormalizedKeyAttributeValue,
+  isModifierKey,
 } = require('../utils');
 const {
   isColorProperty,
@@ -273,10 +275,7 @@ class Puppeteer extends Helper {
         // is closed by _after
       },
       loadVars: async (context) => {
-        // if (this.withinLocator)
         const existingPages = context.targets().filter(t => t.type() === 'page');
-        // this.page = await existingPages[0].page();
-        // this.context = await page.$('body');
         return this._setPage(await existingPages[0].page());
       },
       restoreVars: async () => {
@@ -289,9 +288,9 @@ class Puppeteer extends Helper {
   }
 
 
-  /** [existingPages.length -1
-   * [existingPages.length -1
-   * [existingPages.length -1
+  /**
+   * Set the automatic popup response to Accept.
+   * This must be set before a popup is triggered.
    *
    * ```js
    * I.amAcceptingPopups();
@@ -919,7 +918,7 @@ class Puppeteer extends Helper {
   }
 
   /**
-   * This method is **depreacted**.
+   * This method is **deprecated**.
    *
    * Please use `handleDownloads()` instead.
    */
@@ -1045,20 +1044,50 @@ class Puppeteer extends Helper {
   }
 
   /**
-   * {{> pressKey }}
+   * {{> pressKeyDown }}
+   */
+  async pressKeyDown(key, detectOS) {
+    key = getNormalizedKey.call(this, key, detectOS);
+    await this.page.keyboard.down(key);
+    return this._waitForAction();
+  }
+
+  /**
+   * {{> pressKeyUp }}
+   */
+  async pressKeyUp(key, detectOS) {
+    key = getNormalizedKey.call(this, key, detectOS);
+    await this.page.keyboard.up(key);
+    return this._waitForAction();
+  }
+
+  /**
+   * {{> pressKeyWithDetectOS }}
    *
-   * {{ keys }}
-   */
-  async pressKey(key) {
-    let modifier;
-    const modifiers = ['Control', 'Command', 'Shift', 'Alt'];
-    if (Array.isArray(key) && modifiers.indexOf(key[0]) > -1) {
-      modifier = key[0];
-      key = key[1];
-    }
-    if (modifier) await this.page.keyboard.down(modifier);
-    await await this.page.keyboard.press(key);
-    if (modifier) await this.page.keyboard.up(modifier);
+   * _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
+   */
+  async pressKey(key, detectOS) {
+    const modifiers = [];
+    if (Array.isArray(key)) {
+      for (let k of key) {
+        k = getNormalizedKey.call(this, k, detectOS);
+        if (isModifierKey(k)) {
+          modifiers.push(k);
+        } else {
+          key = k;
+          break;
+        }
+      }
+    } else {
+      key = getNormalizedKey.call(this, key, detectOS);
+    }
+    for (const modifier of modifiers) {
+      await this.page.keyboard.down(modifier);
+    }
+    await this.page.keyboard.press(key);
+    for (const modifier of modifiers) {
+      await this.page.keyboard.up(modifier);
+    }
     return this._waitForAction();
   }
 
@@ -1070,7 +1099,6 @@ class Puppeteer extends Helper {
     const els = await findFields.call(this, field);
     assertElementExists(els, field, 'Field');
     const el = els[0];
-    // await el.focus();
     const tag = await el.getProperty('tagName').then(el => el.jsonValue());
     const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
     if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1985,6 +2013,34 @@ class Puppeteer extends Helper {
   async grabDataFromPerformanceTiming() {
     return perfTiming;
   }
+
+  /**
+   * {{> grabElementSize }}
+   *
+   */
+  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);
+  }
 }
 
 module.exports = Puppeteer;
@@ -2300,3 +2356,69 @@ async function targetCreatedHandler(page) {
     await page.setViewport({ width, height });
   }
 }
+
+// List of key values to key definitions
+// https://github.com/GoogleChrome/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
+const keyDefinitionMap = {
+  /* eslint-disable quote-props */
+  '0': 'Digit0',
+  '1': 'Digit1',
+  '2': 'Digit2',
+  '3': 'Digit3',
+  '4': 'Digit4',
+  '5': 'Digit5',
+  '6': 'Digit6',
+  '7': 'Digit7',
+  '8': 'Digit8',
+  '9': 'Digit9',
+  'a': 'KeyA',
+  'b': 'KeyB',
+  'c': 'KeyC',
+  'd': 'KeyD',
+  'e': 'KeyE',
+  'f': 'KeyF',
+  'g': 'KeyG',
+  'h': 'KeyH',
+  'i': 'KeyI',
+  'j': 'KeyJ',
+  'k': 'KeyK',
+  'l': 'KeyL',
+  'm': 'KeyM',
+  'n': 'KeyN',
+  'o': 'KeyO',
+  'p': 'KeyP',
+  'q': 'KeyQ',
+  'r': 'KeyR',
+  's': 'KeyS',
+  't': 'KeyT',
+  'u': 'KeyU',
+  'v': 'KeyV',
+  'w': 'KeyW',
+  'x': 'KeyX',
+  'y': 'KeyY',
+  'z': 'KeyZ',
+  ';': 'Semicolon',
+  '=': 'Equal',
+  ',': 'Comma',
+  '-': 'Minus',
+  '.': 'Period',
+  '/': 'Slash',
+  '`': 'Backquote',
+  '[': 'BracketLeft',
+  '\\': 'Backslash',
+  ']': 'BracketRight',
+  '\'': 'Quote',
+  /* eslint-enable quote-props */
+};
+
+function getNormalizedKey(key, detectOS) {
+  const normalizedKey = getNormalizedKeyAttributeValue(key, detectOS);
+  if (key !== normalizedKey) {
+    this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`);
+  }
+  // Use key definition to ensure correct key is displayed when Shift modifier is active
+  if (Object.prototype.hasOwnProperty.call(keyDefinitionMap, normalizedKey)) {
+    return keyDefinitionMap[normalizedKey];
+  }
+  return normalizedKey;
+}
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index d2b57c64..25d81f9d 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -15,6 +15,8 @@ const {
   chunkArray,
   convertCssPropertiesToCamelCase,
   screenshotOutputFolder,
+  getNormalizedKeyAttributeValue,
+  modifierKeys,
 } = require('../utils');
 const {
   isColorProperty,
@@ -1491,27 +1493,82 @@ class WebDriver extends Helper {
   }
 
   /**
-   * {{> pressKey }}
-   * {{ keys }}
-   *
-   * To make combinations with modifier and mouse clicks (like Ctrl+Click) press a modifier, click, then release it.
-   *
-   *
-   * ```js
-   * I.pressKey('Control');
-   * I.click('#someelement');
-   * I.pressKey('Control');
-   * ```
+   * {{> pressKeyDown }}
    */
-  async pressKey(key) {
-    let modifier;
-    const modifiers = ['Control', 'Command', 'Shift', 'Alt'];
-    if (Array.isArray(key) && modifiers.indexOf(key[0]) > -1) {
-      modifier = key[0];
+  async pressKeyDown(key, detectOS) {
+    key = getNormalizedKey.call(this, key, detectOS);
+    if (!this.browser.isW3C) {
+      return this.browser.sendKeys([key]);
+    }
+    return this.browser.performActions([{
+      type: 'key',
+      id: 'keyboard',
+      actions: [{
+        type: 'keyDown',
+        value: key,
+      }],
+    }]);
+  }
+
+  /**
+   * {{> pressKeyUp }}
+   */
+  async pressKeyUp(key, detectOS) {
+    key = getNormalizedKey.call(this, key, detectOS);
+    if (!this.browser.isW3C) {
+      return this.browser.sendKeys([key]);
+    }
+    return this.browser.performActions([{
+      type: 'key',
+      id: 'keyboard',
+      actions: [{
+        type: 'keyUp',
+        value: key,
+      }],
+    }]);
+  }
+
+  /**
+   * {{> pressKeyWithDetectOS }}
+   *
+   * _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) {
+    const modifiers = [];
+    if (Array.isArray(key)) {
+      for (let k of key) {
+        k = getNormalizedKey.call(this, k, detectOS);
+        if (isModifierKey(k)) {
+          modifiers.push(k);
+        } else {
+          key = k;
+          break;
+        }
+      }
+    } else {
+      key = getNormalizedKey.call(this, key, detectOS);
+    }
+    for (const modifier of modifiers) {
+      await this.pressKeyDown(modifier);
+    }
+    if (!this.browser.isW3C) {
+      await this.browser.sendKeys([key]);
+    } else {
+      await this.browser.performActions([{
+        type: 'key',
+        id: 'keyboard',
+        actions: [{
+          type: 'keyDown',
+          value: key,
+        }, {
+          type: 'keyUp',
+          value: key,
+        }],
+      }]);
+    }
+    for (const modifier of modifiers) {
+      await this.pressKeyUp(modifier);
     }
-    await this.browser.keys(key);
-    if (!modifier) return true;
-    return this.browser.keys(modifier); // release modifier
   }
 
   /**
@@ -2055,6 +2112,38 @@ class WebDriver extends Helper {
     return this.browser.getGeoLocation();
   }
 
+  /**
+   * {{> grabElementBoundingRect }}
+   *
+   */
+  async grabElementBoundingRect(locator, prop) {
+    const res = await this._locate(locator, true);
+    assertElementExists(res, locator);
+
+    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);
+  }
+
   /**
    * Placeholder for ~ locator only test case write once run on both Appium and WebDriver.
    */
@@ -2190,7 +2279,10 @@ async function findFields(locator) {
   if (!locator.isFuzzy()) return this._locate(locator, true);
 
   const literal = xpathLocator.literal(locator.value);
-  let els = await this._locate(Locator.field.byText(literal));
+  let els = await this._locate(Locator.field.labelEquals(literal));
+  if (els.length) return els;
+
+  els = await this._locate(Locator.field.labelContains(literal));
   if (els.length) return els;
 
   els = await this._locate(Locator.field.byName(literal));
@@ -2336,6 +2428,153 @@ function getElementId(el) {
   return null;
 }
 
+// List of known key values to unicode code points
+// https://www.w3.org/TR/webdriver/#keyboard-actions
+const keyUnicodeMap = {
+  /* eslint-disable quote-props */
+  'Unidentified': '\uE000',
+  'Cancel': '\uE001',
+  'Clear': '\uE005',
+  'Help': '\uE002',
+  'Pause': '\uE00B',
+  'Backspace': '\uE003',
+  'Return': '\uE006',
+  'Enter': '\uE007',
+  'Escape': '\uE00C',
+  'Alt': '\uE00A',
+  'AltLeft': '\uE00A',
+  'AltRight': '\uE052',
+  'Control': '\uE009',
+  'ControlLeft': '\uE009',
+  'ControlRight': '\uE051',
+  'Meta': '\uE03D',
+  'MetaLeft': '\uE03D',
+  'MetaRight': '\uE053',
+  'Shift': '\uE008',
+  'ShiftLeft': '\uE008',
+  'ShiftRight': '\uE050',
+  'Space': '\uE00D',
+  ' ': '\uE00D',
+  'Tab': '\uE004',
+  'Insert': '\uE016',
+  'Delete': '\uE017',
+  'End': '\uE010',
+  'Home': '\uE011',
+  'PageUp': '\uE00E',
+  'PageDown': '\uE00F',
+  'ArrowDown': '\uE015',
+  'ArrowLeft': '\uE012',
+  'ArrowRight': '\uE014',
+  'ArrowUp': '\uE013',
+  'F1': '\uE031',
+  'F2': '\uE032',
+  'F3': '\uE033',
+  'F4': '\uE034',
+  'F5': '\uE035',
+  'F6': '\uE036',
+  'F7': '\uE037',
+  'F8': '\uE038',
+  'F9': '\uE039',
+  'F10': '\uE03A',
+  'F11': '\uE03B',
+  'F12': '\uE03C',
+  'Numpad0': '\uE01A',
+  'Numpad1': '\uE01B',
+  'Numpad2': '\uE01C',
+  'Numpad3': '\uE01D',
+  'Numpad4': '\uE01E',
+  'Numpad5': '\uE01F',
+  'Numpad6': '\uE020',
+  'Numpad7': '\uE021',
+  'Numpad8': '\uE022',
+  'Numpad9': '\uE023',
+  'NumpadMultiply': '\uE024',
+  'NumpadAdd': '\uE025',
+  'NumpadSubtract': '\uE027',
+  'NumpadDecimal': '\uE028',
+  'NumpadDivide': '\uE029',
+  'NumpadEnter': '\uE007',
+  'NumpadInsert': '\uE05C', // 'Numpad0' alternate (when NumLock off)
+  'NumpadDelete': '\uE05D', // 'NumpadDecimal' alternate (when NumLock off)
+  'NumpadEnd': '\uE056', // 'Numpad1' alternate (when NumLock off)
+  'NumpadHome': '\uE057', // 'Numpad7' alternate (when NumLock off)
+  'NumpadPageDown': '\uE055', // 'Numpad3' alternate (when NumLock off)
+  'NumpadPageUp': '\uE054', // 'Numpad9' alternate (when NumLock off)
+  'NumpadArrowDown': '\uE05B', // 'Numpad2' alternate (when NumLock off)
+  'NumpadArrowLeft': '\uE058', // 'Numpad4' alternate (when NumLock off)
+  'NumpadArrowRight': '\uE05A', // 'Numpad6' alternate (when NumLock off)
+  'NumpadArrowUp': '\uE059', // 'Numpad8' alternate (when NumLock off)
+  'Comma': '\uE026', // ',' alias
+  'Digit0': '0', // '0' alias
+  'Digit1': '1', // '1' alias
+  'Digit2': '2', // '2' alias
+  'Digit3': '3', // '3' alias
+  'Digit4': '4', // '4' alias
+  'Digit5': '5', // '5' alias
+  'Digit6': '6', // '6' alias
+  'Digit7': '7', // '7' alias
+  'Digit8': '8', // '8' alias
+  'Digit9': '9', // '9' alias
+  'Equal': '\uE019', // '=' alias
+  'KeyA': 'a', // 'a' alias
+  'KeyB': 'b', // 'b' alias
+  'KeyC': 'c', // 'c' alias
+  'KeyD': 'd', // 'd' alias
+  'KeyE': 'e', // 'e' alias
+  'KeyF': 'f', // 'f' alias
+  'KeyG': 'g', // 'g' alias
+  'KeyH': 'h', // 'h' alias
+  'KeyI': 'i', // 'i' alias
+  'KeyJ': 'j', // 'j' alias
+  'KeyK': 'k', // 'k' alias
+  'KeyL': 'l', // 'l' alias
+  'KeyM': 'm', // 'm' alias
+  'KeyN': 'n', // 'n' alias
+  'KeyO': 'o', // 'o' alias
+  'KeyP': 'p', // 'p' alias
+  'KeyQ': 'q', // 'q' alias
+  'KeyR': 'r', // 'r' alias
+  'KeyS': 's', // 's' alias
+  'KeyT': 't', // 't' alias
+  'KeyU': 'u', // 'u' alias
+  'KeyV': 'v', // 'v' alias
+  'KeyW': 'w', // 'w' alias
+  'KeyX': 'x', // 'x' alias
+  'KeyY': 'y', // 'y' alias
+  'KeyZ': 'z', // 'z' alias
+  'Period': '.', // '.' alias
+  'Semicolon': '\uE018', // ';' alias
+  'Slash': '/', // '/' alias
+  'ZenkakuHankaku': '\uE040',
+  /* eslint-enable quote-props */
+};
+
+function convertKeyToRawKey(key) {
+  if (Object.prototype.hasOwnProperty.call(keyUnicodeMap, key)) {
+    return keyUnicodeMap[key];
+  }
+  // Key is raw key when no representative unicode code point for value
+  return key;
+}
+
+function getNormalizedKey(key, detectOS) {
+  let normalizedKey = getNormalizedKeyAttributeValue(key, detectOS);
+  // Always use "left" modifier keys for non-W3C sessions,
+  // as JsonWireProtocol does not support "right" modifier keys
+  if (!this.browser.isW3C) {
+    normalizedKey = normalizedKey.replace(/^(Alt|Control|Meta|Shift)Right$/, '$1');
+  }
+  if (key !== normalizedKey) {
+    this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`);
+  }
+  return convertKeyToRawKey(normalizedKey);
+}
+
+const unicodeModifierKeys = modifierKeys.map(k => convertKeyToRawKey(k));
+function isModifierKey(key) {
+  return unicodeModifierKeys.includes(key);
+}
+
 function prepareLocateFn(context) {
   if (!context) return this._locate.bind(this);
   return (l) => {
diff --git a/lib/locator.js b/lib/locator.js
index c2cbc64b..b6109495 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -209,7 +209,7 @@ Locator.clickable = {
     `.//button[./@name = ${literal}]`,
     `.//*[@aria-label = ${literal}]`,
     `.//*[@title = ${literal}]`,
-    `.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
+    `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
   ]),
 
   self: literal => `./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
@@ -217,19 +217,19 @@ Locator.clickable = {
 
 Locator.field = {
   labelEquals: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][((./@name = ${literal}) or ./@id = //label[normalize-space(string(.)) = ${literal}]/@for or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][((./@name = ${literal}) or ./@id = //label[@for][normalize-space(string(.)) = ${literal}]/@for or ./@placeholder = ${literal})]`,
     `.//label[normalize-space(string(.)) = ${literal}]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
   ]),
   labelContains: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
     `.//*[@aria-label = ${literal}]`,
     `.//*[@title = ${literal}]`,
-    `.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
+    `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
   ]),
   byName: literal => `.//*[self::input | self::textarea | self::select][@name = ${literal}]`,
   byText: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
   ]),
 
@@ -237,7 +237,7 @@ Locator.field = {
 
 Locator.checkable = {
   byText: literal => xpathLocator.combine([
-    `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
+    `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//input[@type = 'radio' or @type = 'checkbox']`,
   ]),
 
diff --git a/lib/step.js b/lib/step.js
index 9ecee54f..1ec73da5 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -179,7 +179,9 @@ function detectMetaStep(stack) {
 function loadSupportObjects() {
   const config = require('./config');
   const path = require('path');
-  support = config.get('include', {});
+  support = {
+    ...config.get('include', {}),
+  };
   if (support) {
     for (const name in support) {
       const file = support[name];
diff --git a/lib/utils.js b/lib/utils.js
index bf3a93d0..e58f384c 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,4 +1,5 @@
 const fs = require('fs');
+const os = require('os');
 const path = require('path');
 const getFunctionArguments = require('fn-args');
 const deepClone = require('lodash.clonedeep');
@@ -375,3 +376,52 @@ module.exports.tryOrDefault = function (fn, defaultValue) {
     return defaultValue;
   }
 };
+
+function normalizeKeyReplacer(match, prefix, key, suffix, offset, string) {
+  if (typeof key !== 'string') {
+    return string;
+  }
+  const normalizedKey = key.charAt(0).toUpperCase() + key.substr(1).toLowerCase();
+  let position = '';
+  if (typeof prefix === 'string') {
+    position = prefix;
+  } else if (typeof suffix === 'string') {
+    position = suffix;
+  }
+  return normalizedKey + position.charAt(0).toUpperCase() + position.substr(1).toLowerCase();
+}
+
+module.exports.getNormalizedKeyAttributeValue = function (key, detectOS = true) {
+  // 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);
+  // 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('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');
+    }
+  }
+  return key;
+};
+
+const modifierKeys = [
+  'Alt', 'AltGraph', 'AltLeft', 'AltRight',
+  'Control', 'ControlLeft', 'ControlRight',
+  'Meta', 'MetaLeft', 'MetaRight',
+  'Shift', 'ShiftLeft', 'ShiftRight',
+];
+
+module.exports.modifierKeys = modifierKeys;
+module.exports.isModifierKey = function (key) {
+  return modifierKeys.includes(key);
+};
diff --git a/package.json b/package.json
index 838d3d36..72f9310a 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
     "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",
@@ -73,10 +74,10 @@
     "@types/inquirer": "^0.0.35",
     "@types/mocha": "^5.2.7",
     "@types/node": "^8.10.54",
-    "@wdio/sauce-service": "^5.12.5",
-    "@wdio/selenium-standalone-service": "^5.12.1",
-    "@wdio/utils": "^5.13.0",
-    "apollo-server-express": "^2.9.3",
+    "@wdio/sauce-service": "^5.13.2",
+    "@wdio/selenium-standalone-service": "^5.13.2",
+    "@wdio/utils": "^5.13.2",
+    "apollo-server-express": "^2.9.4",
     "chai": "^3.4.1",
     "chai-as-promised": "^5.2.0",
     "documentation": "^8.1.2",
@@ -86,7 +87,7 @@
     "eslint-plugin-mocha": "^5.3.0",
     "express": "^4.17.1",
     "faker": "^4.1.0",
-    "graphql": "^14.5.7",
+    "graphql": "^14.5.8",
     "husky": "^1.2.1",
     "json-server": "^0.10.1",
     "nightmare": "^3.0.2",
@@ -99,7 +100,7 @@
     "testcafe": "^1.5.0",
     "typescript": "^2.9.2",
     "wdio-docker-service": "^1.5.0",
-    "webdriverio": "^5.13.1",
+    "webdriverio": "^5.13.2",
     "xmldom": "^0.1.27",
     "xpath": "0.0.27"
   },
diff --git a/website/static/css/custom.css b/website/static/css/custom.css
index 06e6aeb4..54db3d74 100755
--- a/website/static/css/custom.css
+++ b/website/static/css/custom.css
@@ -340,6 +340,11 @@ background: #2ecc71;
   color: #333;
 }
 
+.navSearchWrapper:before {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
 
 @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
 }


Test patch:
diff --git a/docs/helpers/TestCafe.md b/docs/helpers/TestCafe.md
index f0929e55..885abb69 100644
--- a/docs/helpers/TestCafe.md
+++ b/docs/helpers/TestCafe.md
@@ -231,9 +231,9 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 Verifies that the specified checkbox is not checked.
 
 ```js
-I.dontSeeeCheckboxIsChedcked('#agree'); // located by ID
-I.dontSeeeCheckboxIsChedcked('I agree to terms'); // located by label
-I.dontSeeeCheckboxIsChedcked('agree'); // located by name
+I.dontSeeCheckboxIsChecked('#agree'); // located by ID
+I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
+I.dontSeeCheckboxIsChecked('agree'); // located by name
 ```
 
 #### Parameters
@@ -312,7 +312,7 @@ Checks that current url does not contain a provided fragment.
 
 ### dontSeeInField
 
-Checks that value of input field or textare doesn't equal to given value
+Checks that value of input field or textarea doesn't equal to given value
 Opposite to `seeInField`.
 
 ```js
diff --git a/test/data/app/view/form/aria.php b/test/data/app/view/form/aria.php
new file mode 100644
index 00000000..3a9a5649
--- /dev/null
+++ b/test/data/app/view/form/aria.php
@@ -0,0 +1,136 @@
+<!doctype html>
+<html>
+    <head>
+      <title>Accessibility</title>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+    </head>
+    <body>
+    <form action="/form/complex" method="POST">
+    <div id="myBillingId">Billing</div>
+
+<div>
+    <div id="myNameId">Name</div>
+    <input type="text" name="my-form-name" aria-labelledby="myBillingId myNameId"/>
+</div>
+<div>
+    <div id="myAddressId">Address</div>
+    <input type="text" aria-label="My Address" name="my-form-address" aria-labelledby="myBillingId myAddressId"/>
+</div>
+
+<div id="myPhoneId">Phone</div>
+    <input type="text" name="my-form-phone" aria-labelledby="myPhoneId"/>
+    <input type="submit" value="Submit" />
+    </form>
+
+<div>
+    <div aria-label="get info" onclick="window.location.href='/info'">&raquo;</div>
+</div>
+
+    </body>
+</html>
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index 730552a5..0fd2c4ef 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -126,7 +126,7 @@ services:
       - node_modules:/node_modules
 
   selenium.chrome:
-    image: selenium/standalone-chrome:3.9.1-actinium
+    image: selenium/standalone-chrome:3.141.59-oxygen
     shm_size: 2g
     ports:
       - 4444:4444
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 69fdcc8a..909a7ce3 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -427,6 +427,157 @@ describe('Puppeteer', function () {
     });
   });
 
+  describe('#pressKey, #pressKeyDown, #pressKeyUp', () => {
+    it('should be able to send special keys to element', async () => {
+      await I.amOnPage('/form/field');
+      await I.appendField('Name', '-');
+
+      await I.pressKey(['Right Shift', 'Home']);
+      await I.pressKey('Delete');
+
+      // Sequence only executes up to first non-modifier key ('Digit1')
+      await I.pressKey(['SHIFT_RIGHT', 'Digit1', 'Digit4']);
+      await I.pressKey('1');
+      await I.pressKey('2');
+      await I.pressKey('3');
+      await I.pressKey('ArrowLeft');
+      await I.pressKey('Left Arrow');
+      await I.pressKey('arrow_left');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('a');
+      await I.pressKey('KeyB');
+      await I.pressKeyUp('ShiftLeft');
+      await I.pressKey('C');
+      await I.seeInField('Name', '!ABC123');
+    });
+
+    it('should not change operator modifier key', 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.pressKey(['Right Meta', 'a']); // Select all ('Meta' is changed to 'Control')
+      await I.pressKey('Backspace');
+      await I.dontSeeInField('Name', 'OLD_VALUE');
+    });
+
+    it('should show correct numpad or punctuation key when Shift modifier is active', async () => {
+      await I.amOnPage('/form/field');
+      await I.fillField('Name', '');
+
+      await I.pressKey(';');
+      await I.pressKey(['Shift', ';']);
+      await I.pressKey(['Shift', 'Semicolon']);
+      await I.pressKey('=');
+      await I.pressKey(['Shift', '=']);
+      await I.pressKey(['Shift', 'Equal']);
+      await I.pressKey('*');
+      await I.pressKey(['Shift', '*']);
+      await I.pressKey(['Shift', 'Multiply']);
+      await I.pressKey('+');
+      await I.pressKey(['Shift', '+']);
+      await I.pressKey(['Shift', 'Add']);
+      await I.pressKey(',');
+      await I.pressKey(['Shift', ',']);
+      await I.pressKey(['Shift', 'Comma']);
+      await I.pressKey(['Shift', 'NumpadComma']);
+      await I.pressKey(['Shift', 'Separator']);
+      await I.pressKey('-');
+      await I.pressKey(['Shift', '-']);
+      await I.pressKey(['Shift', 'Subtract']);
+      await I.pressKey('.');
+      await I.pressKey(['Shift', '.']);
+      await I.pressKey(['Shift', 'Decimal']);
+      await I.pressKey(['Shift', 'Period']);
+      await I.pressKey('/');
+      await I.pressKey(['Shift', '/']);
+      await I.pressKey(['Shift', 'Divide']);
+      await I.pressKey(['Shift', 'Slash']);
+
+      await I.seeInField('Name', ';::=++***+++,<<<<-_-.>.>/?/?');
+    });
+
+    it('should show correct number key when Shift modifier is active', async () => {
+      await I.amOnPage('/form/field');
+      await I.fillField('Name', '');
+
+      await I.pressKey('0');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('0');
+      await I.pressKey('Digit0');
+      await I.pressKey('Numpad0');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('1');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('1');
+      await I.pressKey('Digit1');
+      await I.pressKey('Numpad1');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('2');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('2');
+      await I.pressKey('Digit2');
+      await I.pressKey('Numpad2');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('3');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('3');
+      await I.pressKey('Digit3');
+      await I.pressKey('Numpad3');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('4');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('4');
+      await I.pressKey('Digit4');
+      await I.pressKey('Numpad4');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('5');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('5');
+      await I.pressKey('Digit5');
+      await I.pressKey('Numpad5');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('6');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('6');
+      await I.pressKey('Digit6');
+      await I.pressKey('Numpad6');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('7');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('7');
+      await I.pressKey('Digit7');
+      await I.pressKey('Numpad7');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('8');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('8');
+      await I.pressKey('Digit8');
+      await I.pressKey('Numpad8');
+      await I.pressKeyUp('Shift');
+
+      await I.pressKey('9');
+      await I.pressKeyDown('Shift');
+      await I.pressKey('9');
+      await I.pressKey('Digit9');
+      await I.pressKey('Numpad9');
+      await I.pressKeyUp('Shift');
+
+      await I.seeInField('Name', '0))01!!12@@23##34$$45%%56^^67&&78**89((9');
+    });
+  });
 
   describe('#waitForEnabled', () => {
     it('should wait for input text field to be enabled', () => I.amOnPage('/form/wait_enabled')
@@ -586,6 +737,29 @@ describe('Puppeteer', function () {
     });
   });
 
+  describe('#grabElementBoundingRect', () => {
+    it('should get the element bounding rectangle', async () => {
+      await I.amOnPage('https://www.google.com');
+      const size = await I.grabElementBoundingRect('#hplogo');
+      expect(size.x).is.greaterThan(0);
+      expect(size.y).is.greaterThan(0);
+      expect(size.width).is.greaterThan(0);
+      expect(size.height).is.greaterThan(0);
+    });
+
+    it('should get the element width', async () => {
+      await I.amOnPage('https://www.google.com');
+      const width = await I.grabElementBoundingRect('#hplogo', 'width');
+      expect(width).is.greaterThan(0);
+    });
+
+    it('should get the element height', async () => {
+      await I.amOnPage('https://www.google.com');
+      const height = await I.grabElementBoundingRect('#hplogo', 'height');
+      expect(height).is.greaterThan(0);
+    });
+  });
+
   describe('#handleDownloads', () => {
     before(() => {
       // create download folder;
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 0ea020de..2b4e7134 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -1,4 +1,5 @@
 const assert = require('assert');
+const expect = require('chai').expect;
 const path = require('path');
 const fs = require('fs');
 
@@ -165,15 +166,145 @@ describe('WebDriver', function () {
     });
   });
 
-  describe('#pressKey', () => {
+  describe('#pressKey, #pressKeyDown, #pressKeyUp', () => {
     it('should be able to send special keys to element', async () => {
       await wd.amOnPage('/form/field');
       await wd.appendField('Name', '-');
-      await wd.pressKey(['Control', 'a']);
+
+      await wd.pressKey(['Right Shift', 'Home']);
       await wd.pressKey('Delete');
-      await wd.pressKey(['Shift', '111']);
+
+      // Sequence only executes up to first non-modifier key ('Digit1')
+      await wd.pressKey(['SHIFT_RIGHT', 'Digit1', 'Digit4']);
       await wd.pressKey('1');
-      await wd.seeInField('Name', '!!!1');
+      await wd.pressKey('2');
+      await wd.pressKey('3');
+      await wd.pressKey('ArrowLeft');
+      await wd.pressKey('Left Arrow');
+      await wd.pressKey('arrow_left');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('a');
+      await wd.pressKey('KeyB');
+      await wd.pressKeyUp('ShiftLeft');
+      await wd.pressKey('C');
+      await wd.seeInField('Name', '!ABC123');
+    });
+
+    it('should not change operator modifier key', 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.pressKey(['Right Meta', 'a']); // Select all ('Meta' is changed to 'Control')
+      await wd.pressKey('Backspace');
+      await wd.dontSeeInField('Name', 'OLD_VALUE');
+    });
+
+    it('should show correct numpad or punctuation key when Shift modifier is active', async () => {
+      await wd.amOnPage('/form/field');
+      await wd.fillField('Name', '');
+
+      await wd.pressKey(';');
+      await wd.pressKey(['Shift', ';']);
+      await wd.pressKey(['Shift', 'Semicolon']);
+      await wd.pressKey('=');
+      await wd.pressKey(['Shift', '=']);
+      await wd.pressKey(['Shift', 'Equal']);
+      await wd.pressKey('*');
+      await wd.pressKey(['Shift', '*']);
+      await wd.pressKey(['Shift', 'Multiply']);
+      await wd.pressKey('+');
+      await wd.pressKey(['Shift', '+']);
+      await wd.pressKey(['Shift', 'Add']);
+      await wd.pressKey(',');
+      await wd.pressKey(['Shift', ',']);
+      await wd.pressKey(['Shift', 'Comma']);
+      await wd.pressKey(['Shift', 'NumpadComma']);
+      await wd.pressKey(['Shift', 'Separator']);
+      await wd.pressKey('-');
+      await wd.pressKey(['Shift', '-']);
+      await wd.pressKey(['Shift', 'Subtract']);
+      await wd.pressKey('.');
+      await wd.pressKey(['Shift', '.']);
+      await wd.pressKey(['Shift', 'Decimal']);
+      await wd.pressKey(['Shift', 'Period']);
+      await wd.pressKey('/');
+      await wd.pressKey(['Shift', '/']);
+      await wd.pressKey(['Shift', 'Divide']);
+      await wd.pressKey(['Shift', 'Slash']);
+
+      await wd.seeInField('Name', ';::=++***+++,<<<<-_-.>.>/?/?');
+    });
+
+    it('should show correct number key when Shift modifier is active', async () => {
+      await wd.amOnPage('/form/field');
+      await wd.fillField('Name', '');
+
+      await wd.pressKey('0');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('0');
+      await wd.pressKey('Digit0');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('1');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('1');
+      await wd.pressKey('Digit1');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('2');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('2');
+      await wd.pressKey('Digit2');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('3');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('3');
+      await wd.pressKey('Digit3');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('4');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('4');
+      await wd.pressKey('Digit4');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('5');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('5');
+      await wd.pressKey('Digit5');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('6');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('6');
+      await wd.pressKey('Digit6');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('7');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('7');
+      await wd.pressKey('Digit7');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('8');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('8');
+      await wd.pressKey('Digit8');
+      await wd.pressKeyUp('Shift');
+
+      await wd.pressKey('9');
+      await wd.pressKeyDown('Shift');
+      await wd.pressKey('9');
+      await wd.pressKey('Digit9');
+      await wd.pressKeyUp('Shift');
+
+      await wd.seeInField('Name', '0))1!!2@@3##4$$5%%6^^7&&8**9((');
     });
   });
 
@@ -905,4 +1036,27 @@ describe('WebDriver', function () {
       assert.equal(geoLocation.longitude, -122.0748, 'The longitude is not properly set');
     });
   });
+
+  describe('#grabElementBoundingRect', () => {
+    it('should get the element size', async () => {
+      await wd.amOnPage('https://www.google.com');
+      const size = await wd.grabElementBoundingRect('#hplogo');
+      expect(size.x).is.greaterThan(0);
+      expect(size.y).is.greaterThan(0);
+      expect(size.width).is.greaterThan(0);
+      expect(size.height).is.greaterThan(0);
+    });
+
+    it('should get the element width', async () => {
+      await wd.amOnPage('https://www.google.com');
+      const width = await wd.grabElementBoundingRect('#hplogo', 'width');
+      expect(width).is.greaterThan(0);
+    });
+
+    it('should get the element height', async () => {
+      await wd.amOnPage('https://www.google.com');
+      const height = await wd.grabElementBoundingRect('#hplogo', 'height');
+      expect(height).is.greaterThan(0);
+    });
+  });
 });
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index ff0c2698..a7855296 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -213,6 +213,12 @@ module.exports.tests = function () {
       assert.ok(err);
     });
 
+    it('should should click by aria-label', async () => {
+      await I.amOnPage('/form/aria');
+      await I.click('get info');
+      await I.seeInCurrentUrl('/info');
+    });
+
     it('should click link with inner span', async () => {
       await I.amOnPage('/form/example7');
       await I.click('Buy Chocolate Bar');
@@ -455,6 +461,15 @@ module.exports.tests = function () {
       assert.equal(formContents('description'), 'Nothing special');
     });
 
+    it('should fill input by aria-label and aria-labelledby', async () => {
+      await I.amOnPage('/form/aria');
+      await I.fillField('My Address', 'Home Sweet Home');
+      await I.fillField('Phone', '123456');
+      await I.click('Submit');
+      assert.equal(formContents('my-form-phone'), '123456');
+      assert.equal(formContents('my-form-address'), 'Home Sweet Home');
+    });
+
     it('should fill textarea by overwritting the existing value', async () => {
       await I.amOnPage('/form/textarea');
       await I.fillField('Description', 'Nothing special');
diff --git a/test/unit/config_test.js b/test/unit/config_test.js
new file mode 100644
index 00000000..30ce1c15
--- /dev/null
+++ b/test/unit/config_test.js
@@ -0,0 +1,64 @@
+const config = require('../../lib/config');
+const { expect } = require('chai');
+
+describe('Config', () => {
+  beforeEach(() => config.reset());
+
+  it('should be created', () => {
+    const cfg = config.create({
+      output: './report',
+    });
+    expect(cfg).to.contain.keys(['helpers', 'plugins', 'include']);
+    expect(config.get()).to.eql(cfg);
+    expect(cfg.output).to.eql('./report');
+    expect(config.get('output')).to.eql('./report');
+    expect(config.get('output', './other')).to.eql('./report');
+    expect(config.get('tests', '**_test.js')).to.eql('**_test.js');
+  });
+
+
+  it('should be completely reset', () => {
+    config.addHook((cfg) => {
+      cfg.helpers.Puppeteer.show = true;
+    });
+    config.create({
+      tests: '**tests',
+      helpers: {
+        Puppeteer: {},
+      },
+    });
+    config.append({
+      output: './other',
+    });
+    expect(config.get('helpers').Puppeteer.show).to.eql(true);
+    config.reset();
+    expect(config.get().output).to.not.eql('./other');
+    expect(config.get()).to.not.contain.key('tests');
+    expect(config.get('helpers')).to.not.contain.key('Puppeteer');
+    config.create({
+      helpers: {
+        Puppeteer: {},
+      },
+    });
+    expect(config.get('helpers').Puppeteer.show).to.not.eql(true);
+  });
+
+  it('can be updated', () => {
+    config.create();
+    config.append({
+      output: './other',
+    });
+    expect(config.get('output')).to.eql('./other');
+  });
+
+  it('should use config hooks to enhance configs', () => {
+    config.addHook((cfg) => {
+      cfg.additionalValue = true;
+    });
+    const cfg = config.create({
+      additionalValue: false,
+    });
+    expect(cfg).to.contain.key('additionalValue');
+    expect(cfg.additionalValue).to.eql(true);
+  });
+});