Repository: Codeception/CodeceptJS
Pull request #1921 (2019-10-02 08:20:15)

update the docker image to include run-workers
- include the `run-workers`
- update README 


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/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/Dockerfile b/Dockerfile
index a0356ce6..a8dedf95 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -50,6 +50,7 @@ WORKDIR /tests
 # Allow to pass argument to codecept run via env variable
 ENV CODECEPT_ARGS=""
 ENV RUN_MULTIPLE=false
+ENV NO_OF_WORKERS=""
 
 # Set HOST ENV variable for Selenium Server
 ENV HOST=selenium
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/docker/README.md b/docker/README.md
index 0495a410..44362a70 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -94,6 +94,12 @@ docker build -t codeception/codeceptjs .
 * tests directory is expected to be mounted as `/tests`
 * `codeceptjs` is a synlink to `/codecept/bin/codecept.js`
 
+To build this image with your desired Node version:
+
+```sh
+docker build -t codeception/codeceptjs . --build-arg NODE_VERSION=12.10.0
+```
+
 ### Passing Options
 
 Options can be passed by calling `codeceptjs`:
@@ -115,3 +121,17 @@ services:
     volumes:
       - .:/tests
 ```
+
+You can also use `run-workers`to run tests by passing `NO_OF_WORKERS`, additionally, you can pass more params like showing the debug info as the following example:
+
+```yaml
+version: '2'
+services:
+  codeceptjs:
+    image: codeception/codeceptjs
+    environment:
+      - NO_OF_WORKERS=3
+      - CODECEPT_ARGS=--debug
+    volumes:
+      - .:/tests
+```
diff --git a/docker/run.sh b/docker/run.sh
index fb5c1942..cd9c41bc 100755
--- a/docker/run.sh
+++ b/docker/run.sh
@@ -4,21 +4,25 @@ source /codecept/docker/help.sh
 
 # Check if tests are correctly mounted
 if [[ -d "/tests/" ]]; then
-		echo "CodeceptJS directory has been found."
+        echo "CodeceptJS directory has been found."
 
-		# Run the tests
-		cd /tests/
-		if [ "$RUN_MULTIPLE" = true ]; then
-			echo "Tests are split into chunks and executed in multiple processes."
-			if [ ! "$CODECEPT_ARGS" ]; then
-				echo "No CODECEPT_ARGS provided. Tests will procceed with --all option to run all configured runs"
-				codeceptjs run-multiple --all
-			else		
-				codeceptjs run-multiple $CODECEPT_ARGS
-			fi
-		else
-			codeceptjs run $CODECEPT_ARGS
+	# Run the tests
+	cd /tests/ || exit
+	if [ "$RUN_MULTIPLE" = true ]; then
+		echo "Tests are split into chunks and executed in multiple processes."
+		if [ ! "$CODECEPT_ARGS" ]; then
+			echo "No CODECEPT_ARGS provided. Tests will procceed with --all option to run all configured runs"
+			codeceptjs run-multiple --all
+		else		
+			codeceptjs run-multiple $CODECEPT_ARGS
 		fi
 	else
-		display_usage
+		if [ ! "$NO_OF_WORKERS" ]; then
+			codeceptjs run $CODECEPT_ARGS
+		else
+		    codeceptjs run-workers $NO_OF_WORKERS $CODECEPT_ARGS
+		fi
+	fi
+else
+	display_usage
 fi
diff --git a/docs/angular.md b/docs/angular.md
index 702a11ac..d933744f 100644
--- a/docs/angular.md
+++ b/docs/angular.md
@@ -7,22 +7,22 @@ title: Testing with Protractor
 
 ## Introduction
 
-CodeceptJS is an acceptance testing framework. In diversified world of JavaScript testing libraries it aims to create a unified high level API for end-to-end testing, powered by different backends.
-CodeceptJS allows you to write a test and switch in config execution drivers: will it be *wedriverio*, *puppeteer*, or *protractor* depends on you.
-This way you aren't be bound to implementation, and your acceptance tests will work no matter of framework running them.
+CodeceptJS is an acceptance testing framework. In the diversified world of JavaScript testing libraries, it aims to create a unified high-level API for end-to-end testing, powered by a variety of backends.
+CodeceptJS allows you to write a test and switch the execution driver via config: whether it's *wedriverio*, *puppeteer*, or *protractor* depends on you.
+This way you aren't bound to a specific implementation, and your acceptance tests will work no matter what framework is running them.
 
-As you know, [Protractor](http://www.protractortest.org/#/) is an official tool for testing AngularJS applications.
-CodeceptJS should not be considered as alternative to Protractor but a testing framework utilizing this powerful library.
+[Protractor](http://www.protractortest.org/#/) is an official tool for testing AngularJS applications.
+CodeceptJS should not be considered as alternative to Protractor, but rather a testing framework that leverages this powerful library.
 
 ![angular-protractor](https://codecept.io/img/angular-protractor.png)
 
-So there is no magic in testing of AngularJS application in CodeceptJS.
-You just execute regular Protractor commands, packed in a simple high-level API.
+There is no magic in testing of AngularJS application in CodeceptJS.
+You just execute regular Protractor commands, packaged into a simple, high-level API.
 
 ![todo-mvc](https://codecept.io/img/todo.png)
 
-As an example we will use popular [TodoMVC application](http://todomvc.com/examples/angularjs/#/).
-How would we test creating a new todo item in CodeceptJS?
+As an example, we will use the popular [TodoMVC application](http://todomvc.com/examples/angularjs/#/).
+How would we test creating a new todo item using CodeceptJS?
 
 ```js
 Scenario('create todo item', (I) => {
@@ -35,7 +35,7 @@ Scenario('create todo item', (I) => {
 });
 ```
 
-The similar test written in native syntax of Protractor (inherited from selenium-webdriver) would look like this:
+A similar test written using Protractor's native syntax (inherited from selenium-webdriver) would look like this:
 
 ```js
 it('should create todo item', (I) => {
@@ -50,16 +50,15 @@ it('should create todo item', (I) => {
 });
 ```
 
-Comparing to the API proposed by CodeceptJS, this code looks a bit more complicated.
-But what the more important, it's really really hard to read and follow its logic.
-Readability is the most crucial part in acceptance testing.
-You should easily change tests when changes specification or design.
-Probably, only a person who writes Protractor tests in your company,
-could do those changes, while CodeceptJS allows anyone to work with tests.
-Contrary, CodeceptJS provides CodeceptJS provides scenario-driven approach, so test is just a step-by-step representation of real user actions.
-This way you can easily read, and follow test scenario, and edit it when you need it to be changed.
+Compared to the API proposed by CodeceptJS, the Protractor code looks more complicated.
+Even more important, it's harder to read and follow the logic of the Protractor test.
+Readability is a crucial part of acceptance testing.
+Tests should be easy to modify when there are changes in the specification or design.
+If the test is written in Protractor, it would likely require someone familiar with Protractor to make the change, whereas CodeceptJS allows anyone to understand and modify the test.
+CodeceptJS provides scenario-driven approach, so a test is just a step-by-step representation of real user actions.
+This means you can easily read and understand the steps in a test scenario, and edit the steps when the test needs to be changed.
 
-In this way CodeceptJS is more similar to Cucumber, so if you run a test with `--steps` option you will see this output:
+In this way, CodeceptJS is similar to Cucumber. If you run a test with `--steps` option you will see this output:
 
 ```bash
 TodoMvc --
@@ -73,24 +72,24 @@ TodoMvc --
  ✓ OK in 968ms
 ```
 
-Unlike Cucumber, CodeceptJS is not about writing test scenarios above for business rules.
-To say it again, its **goal is to provide standard action steps you can use for testing applications**.
-Surely, it can't cover 100% of cases but it aims for 90%, for others you can write your own steps inside a [custom Helper](http://codecept.io/helpers/) using API of Protractor
+Unlike Cucumber, CodeceptJS is not about writing test scenarios to satisfy business rules or requirements.
+Instead, its **goal is to provide standard action steps you can use for testing applications**.
+Although it can't cover 100% of use cases, CodeceptJS aims for 90%. For the remainder, you can write your own steps inside a [custom Helper](http://codecept.io/helpers/) using Protractor's API.
 
 ### Setting up CodeceptJS with Protractor
 
-To start using CodeceptJS you will need to install it via NPM and initialize it in directory with tests.
+To start using CodeceptJS you will need to install it via NPM and initialize it in a directory with tests.
 
 ```bash
 npm install codeceptjs --save
 npx codeceptjs init
 ```
 
-You will be asked questions about initial configuration, make sure you select Protractor helper.
-If you didn't have Protractor library it **will be installed**.
-Please agree to extend steps, and use `http://todomvc.com/examples/angularjs/` as a url for Protractor helper.
+You will be asked questions about the initial configuration, make sure you select the Protractor helper.
+If your project didn't already have the Protractor library, it **will be installed** as part of this process.
+Please agree to extend steps, and use `http://todomvc.com/examples/angularjs/` as the url for Protractor helper.
 
-For TodoMVC application you will have following config created in `codecept.conf.js` file:
+For TodoMVC application, you will have following config created in the `codecept.conf.js` file:
 
 ```js
 exports.config = { tests: './*_test.js',
@@ -109,15 +108,15 @@ exports.config = { tests: './*_test.js',
 }
 ```
 
-First test can be generated with `gt` command:
+Your first test can be generated with the `gt` command:
 
 ```bash
 npx codeceptjs gt
 ```
 
-After that you can start writing your first CodeceptJS/Angular tests.
-Please look into the reference of [Protractor helper](http://codecept.io/helpers/Protractor/) for all available actions.
-You can also run `list` command to see methods of I:
+After that, you can start writing your first CodeceptJS/Angular tests.
+Please refer to the [Protractor helper](http://codecept.io/helpers/Protractor/) documentation for a list of all available actions.
+You can also run the `list` command to see methods of I:
 
 ```bash
 npx codeceptjs list
@@ -131,12 +130,12 @@ Protractor requires Selenium Server to be started and running. To start and stop
 npm i @wdio/selenium-standalone-service --save
 ```
 
-Enable it in config inside plugins section:
+Enable it in the `codecept.conf.js` file, inside the plugins section:
 
 ```js
 exports.config = {
   // ...
-  // inside condecept.conf.js
+  // inside codecept.conf.js
   plugins: {
     wdio: {
         enabled: true,
@@ -148,7 +147,7 @@ exports.config = {
 
 ## Testing non-Angular Applications
 
-Sure, Protractor can be used to test applications built without AngularJS. In this case you need to disable angular synchronization feature in config:
+Protractor can also be used to test applications built without AngularJS. In this case, you need to disable the angular synchronization feature inside the config:
 
 ```js
 helpers: {
@@ -161,12 +160,11 @@ helpers: {
 }
 ```
 
-## Writing First Test
+## Writing Your First Test
 
-Test scenario should always use `I` object to execute commands.
-This is important as all methods of `I` are running in global promise chain, this way CodeceptJS makes sure everything is executed in right order.
-At first a page should be opened to proceed, we use `amOnPage` command for that. As we already specified full URL to TodoMVC app,
-we can pass relative path into it instead of absolute url:
+Your test scenario should always use the `I` object to execute commands.
+This is important, as all methods of `I` are running in the global promise chain. This way, CodeceptJS makes sure everything is executed in right order.
+To start with opening a webpage, use the `amOnPage` command for. Since we already specified the full URL to the TodoMVC app, we can pass the relative path for our url, instead of the absolute url:
 
 ```js
 Feature('Todo MVC');
@@ -176,7 +174,7 @@ Scenario('create todo item', (I) => {
 });
 ```
 
-All scenarios should describe actions on site and assertions taken in the end. In CodeceptJS assertion commands have `see` or `dontSee` prefix:
+All scenarios should describe actions on the site, with assertions at the end. In CodeceptJS, assertion commands have the `see` or `dontSee` prefix:
 
 ```js
 Feature('Todo MVC');
@@ -187,7 +185,7 @@ Scenario('create todo item', (I) => {
 });
 ```
 
-A test can be executed with `run` command, we recommend to use `--steps` options to follow step-by-step execution:
+A test can be executed with the `run` command, we recommend using the `--steps` option to print out the step-by-step execution:
 
 ```sh
 npx codeceptjs run --steps
@@ -205,11 +203,10 @@ TodoMvc --
 
 ## Running Several Scenarios
 
-By writing a test in similar manner we will have a test shown in the beginning of this guide. Probably we would like not to finish with one test,
-but have more, for testing editing of todo items, checking todo items, and more.
+By now, you should have a test similar to the one shown in the beginning of this guide. We probably want to have multiple tests though, like testing the editing of todo items, checking todo items, etc.
 
-Let's prepare our test for multiple scenarios. All test scenarios will need to open main page of application, so `amOnPage` can be moved into the `Before` hook:
-Scenarios will probably deal with created todo items, so we can move logic of crating new todo into a function.
+Let's prepare our test to contain multiple scenarios. All of our test scenarios will need to to start with with the main page of application open, so `amOnPage` can be moved into the `Before` hook:
+Our scenarios will also probably deal with created todo items, so we can move the logic of creating a new todo into a function.
 
 ```js
 Feature('TodoMvc');
@@ -231,7 +228,7 @@ Scenario('create todo item', (I) => {
 });
 ```
 
-and so we can add even more tests!
+and now we can add even more tests!
 
 ```js
 Scenario('edit todo', (I) => {
@@ -257,9 +254,8 @@ Scenario('check todo item', (I) => {
 
 ## Locators
 
-Like you may have noticed, CodeceptJS doesn't use `by.*` locators similar to Protractor or Selenium Webdriver.
-Instead most of methods expect you to pass valid CSS or XPath. In case you don't want CodeceptJS to guess the type of locator,
-you can specify them using so-called strict locators. This is an absolute analogy of `by`, so you can use angular specific locators (like models, repeaters, bindings, etc) in it:
+You may have noticed that CodeceptJS doesn't use `by.*` locators which are common in Protractor or Selenium Webdriver.
+Instead, most methods expect you to pass valid CSS selectors or XPath. If you don't want CodeceptJS to guess the locator type, then you can specify the type using *strict locators*. This is the CodeceptJS version of `by`, so you can also reuse your angular specific locators (like models, repeaters, bindings, etc):
 
 ```sh
 {css: 'button'}
@@ -267,17 +263,17 @@ you can specify them using so-called strict locators. This is an absolute analog
 {binding: 'latest'}
 ```
 
-When we deal with clicks, CodeceptJS can take a text and search a web page for a valid clickable element with that text.
-So links and buttons can be searched by their text.
+When dealing with clicks, we can specify a text value. CodeceptJS will use that value to search the web page for a valid clickable element containing our specified text.
+This enables us to search for links and buttons by their text.
 
-Same thing happens for form fields: they are searched by field names, labels, and so on.
+The same is true for form fields: they can be searched by field name, label, and so on.
 
-Using such smart locators makes tests easy to write, however, searching an element by text is slower than using CSS|XPath and much slower than using strict locators.
+Using smart locators makes tests easier to write, however searching an element by text is slower than searching via CSS|XPath, and is much slower than using strict locators.
 
 ## Refactoring
 
-In previous examples, we've moved actions into `createTodo` function. Is there a more elegant way of refactoring?
-Can we have something like `I.createTodo()` to be used in code? Sure, we can do so by editing `steps_file.js` created by init command.
+In the previous examples, we moved actions into the `createTodo` function. Is there a more elegant way of refactoring?
+Can we instead write a function like `I.createTodo()` which we can reuse? In fact, we can do so by editing the `steps_file.js` file created by the init command.
 
 ```js
 // in this file you can append custom step methods to 'I' object
@@ -292,7 +288,7 @@ module.exports = function() {
 }
 ```
 
-And that's all, method is available to use as `I.createTodo(title)`:
+That's it, our method is now available to use as `I.createTodo(title)`:
 
 ```js
 Scenario('create todo item', (I) => {
@@ -308,11 +304,10 @@ To learn more about refactoring options in CodeceptJS read [PageObjects guide](h
 
 ## Extending
 
-What if CodeceptJS doesn't provide some of Protractor functionality you actually need? Sure its API is to general,
-and this case is possible. If you don't know how to do something with CodeceptJS - revert back to Protractor syntax!
+What if CodeceptJS doesn't provide some specific Protractor functionality you need? If you don't know how to do something with CodeceptJS, you can simply revert back to using Protractor syntax!
 
-Create custom helper, define methods for it, and use it inside the I object. Your Helper can access `browser` from Protractor
-by accessing Protractor helper:
+Create a custom helper, define methods for it, and use it inside the I object. Your Helper can access `browser` from Protractor
+by accessing the Protractor helper:
 
 ```js
 let browser = this.helpers['Protractor'].browser;
@@ -324,7 +319,7 @@ or use global `element` and `by` variables to locate elements:
 element.all(by.repeater('result in memory'));
 ```
 
-This way we recommend to implement all custom logic using low-level Protractor syntax and using it inside scenario tests.
-Please see an [example of such helper](http://codecept.io/helpers/#protractor-example).
+This is the recommended way to implement all custom logic using low-level Protractor syntax in order to reuse it inside of test scenarios.
+For more information, see an [example of such a helper](http://codecept.io/helpers/#protractor-example).
 
 
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/commands.md b/docs/commands.md
index e674a880..88ae0015 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -110,7 +110,7 @@ npx codeceptjs dry-run --steps -p allure
 To enable bootstrap script in dry-run mode, pass in `--bootstrap` option when running with `--steps` or `--debug`
 
 ```
-nox codeceptjs dry-run --steps --bootstrap
+npx codeceptjs dry-run --steps --bootstrap
 ```
 
 ## Run Multiple
diff --git a/docs/configuration.md b/docs/configuration.md
index 7ec0bf07..a5bd8414 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -17,7 +17,7 @@ Here is an overview of available options with their defaults:
 * **output**: `"./output"` - where to store failure screenshots, etc
 * **helpers**: `{}` - list of enabled helpers
 * **mocha**: `{}` - mocha options, [reporters](http://codecept.io/reports/) can be configured here
-* **multiple**: `{}` - multiple options, see [Multiple Execution](http://codecept.io/advanced/#multiple-browsers-execution)
+* **multiple**: `{}` - multiple options, see [Multiple Execution](http://codecept.io/parallel#multiple-browsers-execution)
 * **bootstrap**: `"./bootstrap.js"` - an option to run code _before_ tests are run. See [Hooks](http://codecept.io/hooks/#bootstrap-teardown)).
 * **bootstrapAll**: `"./bootstrap.js"` - an option to run code _before_ all test suites are run when using the run-multiple mode. See [Hooks](http://codecept.io/hooks/#bootstrap-teardown)).
 * **teardown**: - an option to run code _after_  all test suites are run when using the run-multiple mode. See [Hooks](http://codecept.io/hooks/#bootstrap-teardown).
@@ -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/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..292d9a7e 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.
 
@@ -834,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.
@@ -1039,54 +1068,106 @@ 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']);
+```
+
+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
+I.pressKey(['CommandOrControl', 'Z']);
+```
+
+Some of the supported key names are:
+
+-   `'AltLeft'` or `'Alt'`
+-   `'AltRight'`
+-   `'ArrowDown'`
+-   `'ArrowLeft'`
+-   `'ArrowRight'`
+-   `'ArrowUp'`
+-   `'Backspace'`
+-   `'Clear'`
+-   `'ControlLeft'` or `'Control'`
+-   `'ControlRight'`
+-   `'Command'`
+-   `'CommandOrControl'`
+-   `'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.
     
+_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`][17]).
 
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+I.pressKeyUp('Control');
+```
+
+#### Parameters
+
+-   `key` [string][8] name of key to press down.
+    
 
 
-[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'`.
+### 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`][17]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+I.pressKeyUp('Control');
+```
+
+#### Parameters
+
+-   `key` [string][8] name of key to release.
+    
+
 
 ### refreshPage
 
@@ -1151,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.
     
 
 
@@ -1767,7 +1848,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 +1961,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
+[16]: https://github.com/GoogleChrome/puppeteer/issues/1313
+
+[17]: #click
+
+[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[17]: https://codecept.io/react
+[19]: https://codecept.io/react
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 15be4e0d..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.
@@ -592,9 +591,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 +678,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
@@ -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 
@@ -1136,59 +1165,106 @@ 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']);
+```
+
+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
-I.pressKey('Enter');
-I.pressKey(['Control','a']);
+I.pressKey(['CommandOrControl', 'Z']);
 ```
 
+Some of the supported key names are:
+
+-   `'AltLeft'` or `'Alt'`
+-   `'AltRight'`
+-   `'ArrowDown'`
+-   `'ArrowLeft'`
+-   `'ArrowRight'`
+-   `'ArrowUp'`
+-   `'Backspace'`
+-   `'Clear'`
+-   `'ControlLeft'` or `'Control'`
+-   `'ControlRight'`
+-   `'Command'`
+-   `'CommandOrControl'`
+-   `'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.
     
+_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`][28]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+I.pressKeyUp('Control');
+```
+
+#### Parameters
 
+-   `key` [string][19] name of key to press down.
     
 
 
-[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`][28]).
+
+```js
+I.pressKeyDown('Control');
+I.click('#element');
+I.pressKeyUp('Control');
+```
+
+#### Parameters
+
+-   `key` [string][19] name of key to release.
+    
+
 
 ### refreshPage
 
@@ -1278,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.
     
  
 
@@ -1635,7 +1711,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 +2092,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
+[28]: #click
+
+[29]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[29]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
+[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/hooks.md b/docs/hooks.md
index 2fdc8c9e..4c9099c0 100644
--- a/docs/hooks.md
+++ b/docs/hooks.md
@@ -11,7 +11,7 @@ CodeceptJS provides API to run custom code before and after the test and inject
 In case you need to execute arbitrary code before or after the tests,
 you can use `bootstrap` and `teardown` config. Use it to start and stop webserver, Selenium, etc.
 
-When using the [Multiple Execution](http://codecept.io/advanced/#multiple-execution) mode, there are two additional hooks available; `bootstrapAll` and `teardownAll`. See [BootstrapAll & TeardownAll](#bootstrapall--teardownall) for more information.
+When using the [Multiple Execution](http://codecept.io/advanced/#multiple-execution) mode, there are two additional hooks available; `bootstrapAll` and `teardownAll`. See [BootstrapAll & TeardownAll](#bootstrapall-teardownall) for more information.
 
 There are different ways to define bootstrap and teardown functions:
 
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/visual.md b/docs/visual.md
index 8fa71fec..f9934497 100644
--- a/docs/visual.md
+++ b/docs/visual.md
@@ -183,3 +183,91 @@ Scenario('Compare CodeceptIO Home Page @visual-test', async (I, adminPage) => {
 Depending of your configuration this test will fail if no baseline exists and log the link to the image to accept or automatically accept the first run as baseline.
 > You can accept the first image as baseline automatically via ```autoBaseline: true``` _default is false_
 
+## Using Applitools
+
+Applitools helps Test Automation engineers, DevOps, and FrontEnd Developers continuously test and validate visually perfect mobile, web, and native apps. Now it can be used with CodeceptJS.
+
+### Setup
+
+Create an account at [Applitools](https://applitools.com/users/register) and install the npm packages
+
+```
+npm i codeceptjs-applitoolshelper --save
+npm i webdriverio@5 --save
+```
+
+### Configuring
+
+```js
+...
+  helpers: {
+    WebDriver: {
+      url: 'https://applitools.com/helloworld',
+      browser: 'chrome',
+      desiredCapabilities: {
+        chromeOptions: {
+          args: [ '--headless', '--disable-extensions', '--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage']
+        }
+      },
+      windowSize: '1920x600',
+      smartWait: 5000,
+      timeouts: {
+        'script': 60000,
+        'page load': 10000
+      },
+    },
+    ApplitoolsHelper: {
+      require: 'codeceptjs-applitoolshelper',
+      applitoolsKey: 'YOUR_API_KEY'
+    }
+  },
+...
+```
+
+#### To use this helper you need to provide the following info:
+
+- applitoolsKey (Required): You can find your API key under the user menu located at the right hand side of the test manager toolbar
+- windowSize (Optional): the windows size as for instance 1440x700, if not provided, the default 1920x600 will be used. The windowSize will follow this precedence: ApplitoolsHelper, Webdriver.
+- appName (Optional): you can either provide your desired application name, if not provided, the default 'Application Under Test' will be used.
+
+### Usage
+
+```javascript
+/**
+ * @param pageToCheck {string} Name of page you want to check
+ */
+I.eyeCheck(pageToCheck);
+
+```
+
+The first time you run this test a new baseline will be created, and subsequent test runs will be compared to this baseline. If any screenshot mismatch its baseline image in a perceptible way, there will be a `DiffsFoundException` which includes a URL that points to a detailed report where you can see the detected differences and take appropriate actions such as reporting bugs, updating the baseline and more.
+
+```js
+-- FAILURES:
+
+  1) Applitools functionality
+       Check home page @test:
+     Test 'Applitools functionality' of 'Application Under Test' detected differences!. See details at: https://eyes.applitools.com/app/batches/00000251831777088983/00000251831777088717?accountId=KO-Oh9tXI0e8VF8Ha_GLVA~~
+```
+
+> You can find the latest documentation here [Applitools Docs](https://applitools.com/tutorials/webdriverio5.html#run-your-first-test)
+
+### Example
+
+Lets consider visual testing for [CodeceptJS Home](http://codecept.io).
+You can also find example repo here: https://github.com/PeterNgTr/codeceptjs-applitoolshelper
+
+```js
+const { I } = inject();
+
+Feature('Applitools functionality');
+
+Before(() => {
+    I.amOnPage('https://applitools.com/helloworld');
+});
+
+Scenario('Check home page @test', async () => {
+    await I.eyeCheck('Homepage');
+});
+```
+
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..1773bde9
--- /dev/null
+++ b/docs/webapi/grabElementBoundingRect.mustache
@@ -0,0 +1,20 @@
+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
+```
+@param {string|object} locator element located by CSS|XPath|strict locator.
+@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
new file mode 100644
index 00000000..d3bd5a41
--- /dev/null
+++ b/docs/webapi/pressKeyDown.mustache
@@ -0,0 +1,11 @@
+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.
diff --git a/docs/webapi/pressKeyUp.mustache b/docs/webapi/pressKeyUp.mustache
new file mode 100644
index 00000000..36daa9dd
--- /dev/null
+++ b/docs/webapi/pressKeyUp.mustache
@@ -0,0 +1,11 @@
+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.
diff --git a/docs/webapi/pressKeyWithKeyNormalization.mustache b/docs/webapi/pressKeyWithKeyNormalization.mustache
new file mode 100644
index 00000000..f7ec2a6d
--- /dev/null
+++ b/docs/webapi/pressKeyWithKeyNormalization.mustache
@@ -0,0 +1,59 @@
+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']);
+```
+
+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
+I.pressKey(['CommandOrControl', 'Z']);
+```
+
+Some of the supported key names are:
+- `'AltLeft'` or `'Alt'`
+- `'AltRight'`
+- `'ArrowDown'`
+- `'ArrowLeft'`
+- `'ArrowRight'`
+- `'ArrowUp'`
+- `'Backspace'`
+- `'Clear'`
+- `'ControlLeft'` or `'Control'`
+- `'ControlRight'`
+- `'Command'`
+- `'CommandOrControl'`
+- `'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.
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..e1f916b6 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) {
+    key = getNormalizedKey.call(this, key);
+    await this.page.keyboard.down(key);
+    return this._waitForAction();
+  }
+
+  /**
+   * {{> pressKeyUp }}
+   */
+  async pressKeyUp(key) {
+    key = getNormalizedKey.call(this, key);
+    await this.page.keyboard.up(key);
+    return this._waitForAction();
+  }
+
+  /**
+   * {{> pressKeyWithKeyNormalization }}
    *
-   * {{ keys }}
+   * _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
    */
   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];
+    const modifiers = [];
+    if (Array.isArray(key)) {
+      for (let k of key) {
+        k = getNormalizedKey.call(this, k);
+        if (isModifierKey(k)) {
+          modifiers.push(k);
+        } else {
+          key = k;
+          break;
+        }
+      }
+    } else {
+      key = getNormalizedKey.call(this, key);
+    }
+    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);
     }
-    if (modifier) await this.page.keyboard.down(modifier);
-    await await this.page.keyboard.press(key);
-    if (modifier) 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,17 @@ class Puppeteer extends Helper {
   async grabDataFromPerformanceTiming() {
     return perfTiming;
   }
+
+  /**
+   * {{> grabElementBoundingRect }}
+   */
+  async grabElementBoundingRect(locator, prop) {
+    const els = await this._locate(locator);
+    assertElementExists(els, locator);
+    const rect = await els[0].boundingBox();
+    if (prop) return rect[prop];
+    return rect;
+  }
 }
 
 module.exports = Puppeteer;
@@ -2300,3 +2339,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) {
+  const normalizedKey = getNormalizedKeyAttributeValue(key);
+  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/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 d2b57c64..faf01e34 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,
@@ -42,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.
@@ -379,7 +380,6 @@ class WebDriver extends Helper {
   _validateConfig(config) {
     const defaults = {
       logLevel: 'silent',
-
       // codeceptjs
       remoteFileUpload: true,
       smartWait: 0,
@@ -1491,27 +1491,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.
-   *
+   * {{> pressKeyDown }}
+   */
+  async pressKeyDown(key) {
+    key = getNormalizedKey.call(this, key);
+    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) {
+    key = getNormalizedKey.call(this, key);
+    if (!this.browser.isW3C) {
+      return this.browser.sendKeys([key]);
+    }
+    return this.browser.performActions([{
+      type: 'key',
+      id: 'keyboard',
+      actions: [{
+        type: 'keyUp',
+        value: key,
+      }],
+    }]);
+  }
+
+  /**
+   * {{> pressKeyWithKeyNormalization }}
    *
-   * ```js
-   * I.pressKey('Control');
-   * I.click('#someelement');
-   * I.pressKey('Control');
-   * ```
+   * _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) {
-    let modifier;
-    const modifiers = ['Control', 'Command', 'Shift', 'Alt'];
-    if (Array.isArray(key) && modifiers.indexOf(key[0]) > -1) {
-      modifier = key[0];
+    const modifiers = [];
+    if (Array.isArray(key)) {
+      for (let k of key) {
+        k = getNormalizedKey.call(this, k);
+        if (isModifierKey(k)) {
+          modifiers.push(k);
+        } else {
+          key = k;
+          break;
+        }
+      }
+    } else {
+      key = getNormalizedKey.call(this, key);
+    }
+    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 +2110,19 @@ class WebDriver extends Helper {
     return this.browser.getGeoLocation();
   }
 
+  /**
+   * {{> grabElementBoundingRect }}
+   */
+  async grabElementBoundingRect(locator, prop) {
+    const res = await this._locate(locator, true);
+    assertElementExists(res, locator);
+    const el = usingFirstElement(res);
+
+    const rect = Object.assign({}, await el.getLocation(), await el.getSize());
+    if (prop) return rect[prop];
+    return rect;
+  }
+
   /**
    * Placeholder for ~ locator only test case write once run on both Appium and WebDriver.
    */
@@ -2190,7 +2258,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 +2407,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) {
+  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) {
+    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/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/plugin/puppeteerCoverage.js b/lib/plugin/puppeteerCoverage.js
index 083288e4..87569be8 100644
--- a/lib/plugin/puppeteerCoverage.js
+++ b/lib/plugin/puppeteerCoverage.js
@@ -56,7 +56,7 @@ function buildFileName(test, uniqueFileName) {
  *
  * 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!
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..9475128e 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,46 @@ 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) {
+  // 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|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(/^(Cmd|Command|Os|Super)/, 'Meta');
+  key = key.replace('Ctrl', 'Control');
+  key = key.replace('Option', 'Alt');
+  key = key.replace(/^(NumpadComma|Separator)$/, 'Comma');
+  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..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",
@@ -73,10 +73,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 +86,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 +99,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/lib/helper/TestCafe.js b/lib/helper/TestCafe.js
index e001ed6f..acf959d9 100644
--- a/lib/helper/TestCafe.js
+++ b/lib/helper/TestCafe.js
@@ -86,7 +86,6 @@ class TestCafe extends Helper {
   constructor(config) {
     super(config);
 
-    this.iteration = 1;
     this.testcafe = undefined; // testcafe instance
     this.t = undefined; // testcafe test controller
     this.dummyTestcafeFile; // generated testcafe test file
@@ -136,7 +135,7 @@ class TestCafe extends Helper {
 
     this.iteration += 2; // Use different ports for each test run
     // @ts-ignore
-    this.testcafe = await createTestCafe('localhost', 1338 + this.iteration, 1339 + this.iteration);
+    this.testcafe = await createTestCafe('localhost', null, null);
 
     this.debugSection('_before', 'Starting testcafe browser...');
 
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..b0f3dd14 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -427,6 +427,152 @@ 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 use modifier key based on operating system', async () => {
+      await I.amOnPage('/form/field');
+      await I.fillField('Name', 'value that is cleared using select all shortcut');
+
+      await I.pressKey(['ControlOrCommand', 'a']);
+      await I.pressKey('Backspace');
+      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 () => {
+      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 +732,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..99ca7092 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');
 
@@ -26,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(),
@@ -165,15 +165,140 @@ 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 use modifier key based on operating system', async () => {
+      await wd.amOnPage('/form/field');
+      await wd.fillField('Name', 'value that is cleared using select all shortcut');
+
+      await wd.pressKey(['CommandOrControl', 'A']);
+      await wd.pressKey('Backspace');
+      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 () => {
+      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 +1030,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);
+  });
+});
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();
+    });
+  });
 });