Repository: Codeception/CodeceptJS
Pull request #2362 (2020-04-24 23:26:34)

added conditional actions plugin
## Motivation/Description of the PR

Added `tryTo` function which won't fail a test on step failure

```js
const isSeen = await tryTo(() => {
  I.see('Some text');
});
```

This also helps when you need to click a button on a condition.

```js
// we are not sure if cookie bar is displayed, but if so - accept cookies
tryTo(() => I.click('Accept', '.cookies'));
```

Applicable helpers:

- [ ] WebDriver
- [ ] Puppeteer
- [ ] Nightmare
- [ ] REST
- [ ] FileHelper
- [ ] Appium
- [ ] Protractor
- [ ] TestCafe
- [ ] Playwright

Applicable plugins:

- [ ] allure
- [ ] autoDelay
- [ ] autoLogin
- [ ] customLocator
- [ ] pauseOnFail
- [ ] puppeteerCoverage
- [ ] retryFailedStep
- [ ] screenshotOnFail
- [ ] selenoid
- [ ] stepByStepReport
- [ ] wdio

## Type of change

- [ ] :fire: Breaking changes
- [ ] :rocket: New functionality
- [ ] :bug: Bug fix
- [ ] :clipboard: Documentation changes/updates
- [ ] :hotsprings: Hot fix
- [ ] :hammer: Markdown files fix - not related to source code
- [ ] :nail_care: Polish code

## Checklist:

- [ ] Tests have been added
- [ ] Documentation has been added (Run `npm run docs`)
- [ ] Lint checking (Run `npm run lint`)
- [ ] Local tests are passed (Run `npm test`)



Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b27d52d2..2eb6dc65 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,10 +7,6 @@ defaults: &defaults
   steps:
     - checkout
     - run: .circleci/build.sh
-    - run:
-        command: docker-compose run --rm test-unit
-        working_directory: test
-        when: always
     - run:
         command: docker-compose run --rm test-rest
         working_directory: test
diff --git a/.eslintrc.json b/.eslintrc.json
index c218d4aa..33d9c6e9 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,7 +6,7 @@
   "rules": {
     "func-names": 0,
     "no-use-before-define": 0,
-    "no-unused-vars": 2,
+    "no-unused-vars": 0,
     "no-underscore-dangle": 0,
     "no-undef": 0,
     "prefer-destructuring": 0,
diff --git a/.github/workflows/appium.yml b/.github/workflows/appium.yml
new file mode 100644
index 00000000..ae181295
--- /dev/null
+++ b/.github/workflows/appium.yml
@@ -0,0 +1,55 @@
+name: Appium Tests
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - '**'
+
+env:
+  CI: true
+  # Force terminal colors. @see https://www.npmjs.com/package/colors
+  FORCE_COLOR: 1
+
+jobs:
+  appium1:
+    runs-on: ubuntu-18.04
+
+    strategy:
+      matrix:
+        node-version: [12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - run: npm install
+    - run: 'npm run test:appium-quick'
+      env: # Or as an environment variable
+        SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
+        SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
+
+
+  appium2:
+
+    runs-on: ubuntu-18.04
+
+    strategy:
+      matrix:
+        node-version: [12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - run: npm install
+    - run: 'npm run test:appium-other'
+      env: # Or as an environment variable
+        SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
+        SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index ebec574b..43cb4b1f 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -6,7 +6,7 @@ on:
       - master
   pull_request:
     branches:
-      - master
+      - '**'
 
 env:
   CI: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5424910..cc746355 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## 2.6.3
+
+* [stepByStepReport plugin] Fixed when using plugin with BeforeSuite. Fixes #2337 by @mirao
+* [allure plugin] Fixed reporting of tests skipped by failure in before hook. Refer to #2349 & #2354. Fix by @koushikmohan1996
+
+## 2.6.2
+
+* [WebDriver][Puppeteer] Added `forceClick` method to emulate click event instead of using native events.
+* [Playwright] Updated to 0.14
+* [Puppeteer] Updated to Puppeteer v3.0
+* [wdio] Fixed undefined output directory for wdio plugns. Fix By @PeterNgTr
+* [Playwright] Introduced `handleDownloads` method to download file. Please note, this method has slightly different API than the same one in Puppeteer.
+* [allure] Fixed undefined output directory for allure plugin on using custom runner. Fix by @charliepradeep
+* [WebDriver] Fixed `waitForEnabled` fix for webdriver 6. Fix by @dsharapkou
+* Workers: Fixed negative failure result if use scenario with the same names. Fix by @Vorobeyko
+* [MockRequest] Updated documentation to match new helper version
+* Fixed: skipped tests are not reported if a suite failed in `before`. Refer #2349 & #2354. Fix by @koushikmohan1996
+
 ## 2.6.1
 
 * [screenshotOnFail plugin] Fixed saving screenshot of active session.
diff --git a/README.md b/README.md
index b696d269..b44ccb62 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ A simple test that verifies the "Welcome" text is present on a main page of a si
 ```js
 Feature('CodeceptJS demo');
 
-Scenario('check Welcome page on site', (I) => {
+Scenario('check Welcome page on site', ({ I }) => {
   I.amOnPage('/');
   I.see('Welcome');
 });
@@ -100,7 +100,7 @@ Let's see how we can handle basic form testing:
 ```js
 Feature('CodeceptJS Demonstration');
 
-Scenario('test some forms', (I) => {
+Scenario('test some forms', ({ I }) => {
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
   I.fillField('Email', 'hello@world.com');
   I.fillField('Password', '123456');
@@ -186,7 +186,7 @@ const assert = require('assert');
 
 Feature('CodeceptJS Demonstration');
 
-Scenario('test page title', async (I) => {
+Scenario('test page title', async ({ I }) => {
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
   const title = await I.grabTitle();
   assert.equal(title, 'Example application with SimpleForm and Twitter Bootstrap');
@@ -202,17 +202,17 @@ Common preparation steps like opening a web page, logging in a user, can be plac
 ```js
 Feature('CodeceptJS Demonstration');
 
-Before((I) => { // or Background
+Before(({ I }) => { // or Background
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
 });
 
-Scenario('test some forms', (I) => {
+Scenario('test some forms', ({ I }) => {
   I.click('Create User');
   I.see('User is valid');
   I.dontSeeInCurrentUrl('/documentation');
 });
 
-Scenario('test title', (I) => {
+Scenario('test title', ({ I }) => {
   I.seeInTitle('Example application');
 });
 ```
@@ -252,7 +252,7 @@ You can easily inject it to test by providing its name in test arguments:
 ```js
 Feature('CodeceptJS Demonstration');
 
-Before((I) => { // or Background
+Before(({ I }) => { // or Background
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
 });
 
diff --git a/docs/basics.md b/docs/basics.md
index 9e02721f..f97f441f 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -50,6 +50,8 @@ Refer to following guides to more information on:
 To list all available commands for the current configuration run `codeceptjs list`
 or enable [auto-completion by generating TypeScript definitions](#intellisense).
 
+> 🤔 It is possible to access API of a backend you use inside a test or a [custom helper](/helpers/#extending-codeceptjs-with-custom-helpers). For instance, to use Puppeteer API inside a test use [`I.usePuppeteerTo`](/helpers/Puppeteer/#usepuppeteerto) inside a test. Similar methods exist for each helper.
+
 
 ## Writing Tests
 
@@ -147,24 +149,31 @@ I.click('#signup');
 I.click('//dev[@test-id="myid"]');
 ```
 
+> ℹ If click doesn't work in a test but works for user, it is possible that frontend application is not designed for automated testing. To overcome limitation of standard click in this edgecase use `forceClick` method. It will emulate click instead of sending native event. This command will click an element no matter if this element is visible or animating. It will send JavaScript "click" event to it.
+
 ### Filling Fields
 
 Clicking the links is not what takes the most time during testing a web site. If your site consists only of links you can skip test automation. The most waste of time goes into the testing of forms. CodeceptJS provides several ways of doing that.
 
 Let's submit this sample form for a test:
 
+![](https://user-images.githubusercontent.com/220264/80355863-494a8280-8881-11ea-9b41-ba1f07abf094.png)
+
 ```html
 <form method="post" action="/update" id="update_form">
      <label for="user_name">Name</label>
-     <input type="text" name="user[name]" id="user_name" />
+     <input type="text" name="user[name]" id="user_name" /><br>
      <label for="user_email">Email</label>
-     <input type="text" name="user[email]" id="user_email" />
-     <label for="user_gender">Gender</label>
-     <select id="user_gender" name="user[gender]">
-          <option value="m">Male</option>
-          <option value="f">Female</option>
-     </select>
-     <input type="submit" name="submitButton" value="Update" />
+     <input type="text" name="user[email]" id="user_email" /><br>
+     <label for="user_role">Role</label>
+     <select id="user_role" name="user[role]">
+          <option value="0">Admin</option>
+          <option value="1">User</option>
+     </select><br>
+     <input type="checkbox" id="accept" /> <label for="accept">Accept changes</label>
+     <div>
+     <input type="submit" name="submitButton" class="btn btn-primary" value="Save" />
+     </div>
 </form>
 ```
 
@@ -176,11 +185,16 @@ I.fillField('Name', 'Miles');
 // we can use input name
 I.fillField('user[email]','miles@davis.com');
 // select element by label, choose option by text
-I.selectOption('Gender','Male');
-// click 'Update' button, found by text
-I.click('Update');
+I.selectOption('Role','Admin');
+// click 'Save' button, found by text
+I.checkOption('Accept');
+I.click('Save');
 ```
 
+> ℹ `selectOption` works only with standard `<select>` <select placeholder="select"></select> HTML elements. If your selectbox is created by React, Vue, or as a component of any other framework, this method potentially won't work with it. Use `click` to manipulate it.
+
+> ℹ `checkOption` also works only with standard `<input type="checkbox">` <input type="checkbox"> HTML elements. If your checkbox is created by React, Vue, or as a component of any other framework, this method potentially won't work with it. Use `click` to manipulate it.
+
 Alternative scenario:
 
 ```js
@@ -188,12 +202,12 @@ Alternative scenario:
 I.fillField('#user_name', 'Miles');
 I.fillField('#user_email','miles@davis.com');
 // select element by label, option by value
-I.selectOption('#user_gender','m');
+I.selectOption('#user_role','1');
 // click 'Update' button, found by name
 I.click('submitButton', '#update_form');
 ```
 
-To fill in sensitive data use the `secret` function:
+To fill in sensitive data use the `secret` function, it won't expose actual value in logs.
 
 ```js
 I.fillField('password', secret('123456'));
@@ -234,6 +248,8 @@ I.seeInTitle('My Website');
 
 To see all possible assertions, check the helper's reference.
 
+> ℹ If you need custom assertions, you can install an assertion libarary like `chai`, use grabbers to obtain information from a browser and perform assertions. However, it is recommended to put custom assertions into a helper for further reuse.
+
 ### Grabbing
 
 Sometimes you need to retrieve data from a page to use it in the following steps of a scenario.
@@ -273,7 +289,6 @@ I.waitForElement('#agree_button', 30); // secs
 // clicks a button only when it is visible
 I.click('#agree_button');
 ```
-
 > ℹ See [helpers reference](/reference) for a complete list of all available commands for the helper you use.
 
 ## How It Works
@@ -435,7 +450,7 @@ This launches the interactive console where you can call any action from the `I`
  - Press ENTER to run the next step
  - Press TAB twice to see all available commands
  - Type exit + Enter to exit the interactive shell
- - Prefix => to run js commands 
+ - Prefix => to run js commands
  I.
 ```
 
diff --git a/docs/changelog.md b/docs/changelog.md
index 570c0fba..f625445b 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,26 @@ layout: Section
 
 # Releases
 
+## 2.6.2
+
+* [WebDriver][Puppeteer] Added `forceClick` method to emulate click event instead of using native events.
+* **[Playwright]** Updated to 0.14
+* **[Puppeteer]** Updated to Puppeteer v3.0
+* **[wdio]** Fixed undefined output directory for wdio plugns. Fix By **[PeterNgTr](https://github.com/PeterNgTr)**
+* **[Playwright]** Introduced `handleDownloads` method to download file. Please note, this method has slightly different API than the same one in Puppeteer.
+* **[allure]** Fixed undefined output directory for allure plugin on using custom runner. Fix by **[charliepradeep](https://github.com/charliepradeep)**
+* **[WebDriver]** Fixed `waitForEnabled` fix for webdriver 6. Fix by **[dsharapkou](https://github.com/dsharapkou)**
+* Workers: Fixed negative failure result if use scenario with the same names. Fix by **[Vorobeyko](https://github.com/Vorobeyko)**
+* **[MockRequest]** Updated documentation to match new helper version
+* Fixed: skipped tests are not reported if a suite failed in `before`. Refer [#2349](https://github.com/Codeception/CodeceptJS/issues/2349) & [#2354](https://github.com/Codeception/CodeceptJS/issues/2354). Fix by **[koushikmohan1996](https://github.com/koushikmohan1996)**
+
+## 2.6.1
+
+* [screenshotOnFail plugin] Fixed saving screenshot of active session.
+* [screenshotOnFail plugin] Fix issue [#2301](https://github.com/Codeception/CodeceptJS/issues/2301) when having the flag `uniqueScreenshotNames`=true results in `undefined` in screenshot file name by **[PeterNgTr](https://github.com/PeterNgTr)**
+* **[WebDriver]** Fixed `waitForElement` not applying the optional second argument to override the default timeout in webdriverio 6. Fix by **[Mooksc](https://github.com/Mooksc)**
+* **[WebDriver]** Updated `waitUntil` method which is used by all of the wait* functions. This updates the `waitForElement` by the same convention used to update `waitForVisible` and `waitInUrl` to be compatible with both WebDriverIO v5 & v6. See [#2313](https://github.com/Codeception/CodeceptJS/issues/2313) by **[Mooksc](https://github.com/Mooksc)**
+
 ## 2.6.0
 
 * **[Playwright] Updated to Playwright 0.12** by **[Georgegriff](https://github.com/Georgegriff)**.
@@ -29,6 +49,7 @@ I.executeScript(({x, y}) => x + y, {x, y});
   * `click` - automatically waits for element to become clickable (visible, not animated) and waits for navigation.
   * `clickLink` - deprecated
   * `waitForClickable` - deprecated
+  * `forceClick` - added
   * Added support for custom locators. See [#2277](https://github.com/Codeception/CodeceptJS/issues/2277)
   * Introduced [device emulation](/playwright/#device-emulation):
     * globally via `emulate` config option
diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md
index 82e78c61..aabf6aa0 100644
--- a/docs/helpers/Appium.md
+++ b/docs/helpers/Appium.md
@@ -1107,6 +1107,27 @@ I.waitForText('Thank you, form has been submitted', 5, '#modal');
 -   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait (optional, default `1`)
 -   `context` **([string][4] \| [object][6])?** (optional) element located by CSS|XPath|strict locator. (optional, default `null`)
 
+### useWebDriverTo
+
+Use [webdriverio][17] API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`browser`][17]) } object from WebDriver API is available.
+
+```js
+I.useWebDriverTo('open multiple windows', async ({ browser }) {
+   // create new window
+   await browser.newWindow('https://webdriver.io');
+});
+```
+
+#### Parameters
+
+-   `description` **[string][4]** used to show in logs.
+-   `fn` **[function][18]** async functuion that executed with WebDriver helper as argument
+
 ### \_isShadowLocator
 
 Check if locator is type of "Shadow"
@@ -1162,12 +1183,14 @@ this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').
 Find a clickable element by providing human readable text:
 
 ```js
-this.helpers['WebDriver']._locateClickable('Next page').then // ...
+const els = await this.helpers.WebDriver._locateClickable('Next page');
+const els = await this.helpers.WebDriver._locateClickable('Next page', '.pages');
 ```
 
 #### Parameters
 
 -   `locator` **([string][4] \| [object][6])** element located by CSS|XPath|strict locator.
+-   `context`  
 
 ### \_locateFields
 
@@ -1183,7 +1206,7 @@ this.helpers['WebDriver']._locateFields('Your email').then // ...
 
 ### defineTimeout
 
-Set [WebDriver timeouts][17] in realtime.
+Set [WebDriver timeouts][19] in realtime.
 
 Timeouts are expected to be passed as object:
 
@@ -1211,6 +1234,38 @@ I.amOnPage('/login'); // opens a login page
 
 -   `url` **[string][4]** url path or global url.
 
+### forceClick
+
+Perform an emulated click on a link or a button, given by a locator.
+Unlike normal click instead of sending native event, emulates a click with JavaScript.
+This works on hidden, animated or inactive elements as well.
+
+If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
+For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
+For images, the "alt" attribute and inner text of any parent links are searched.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+// simple link
+I.forceClick('Logout');
+// button of form
+I.forceClick('Submit');
+// CSS button
+I.forceClick('#form input[type=submit]');
+// XPath
+I.forceClick('//form/*[@type=submit]');
+// link in context
+I.forceClick('Logout', '#nav');
+// using strict locator
+I.forceClick({css: 'nav a.login'});
+```
+
+#### Parameters
+
+-   `locator` **([string][4] \| [object][6])** clickable link or button located by text, or any element located by CSS|XPath|strict locator.
+-   `context` **([string][4]? | [object][6])** (optional, `null` by default) element to search in CSS|XPath|Strict locator.{{ react }} (optional, default `null`)
+
 ### doubleClick
 
 Performs a double-click on an element matched by link|button|label|CSS or XPath.
@@ -1402,7 +1457,7 @@ let logs = await I.grabBrowserLogs();
 console.log(JSON.stringify(logs))
 ```
 
-Returns **[Promise][13]&lt;([string][4] \| [undefined][18])>** 
+Returns **[Promise][13]&lt;([string][4] \| [undefined][20])>** 
 
 ### dontSeeInSource
 
@@ -1525,7 +1580,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
 ### type
 
 Types out the given string or the array of keys provided.
-_Note:_ Should only be used when using [`fillField`][19] is not an option.
+_Note:_ Should only be used when using [`fillField`][21] is not an option.
 
 ```js
 // When passing in a string
@@ -1746,8 +1801,12 @@ Returns **[object][6]** Element bounding rectangle
 
 [16]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
 
-[17]: https://webdriver.io/docs/timeouts.html
+[17]: https://webdriver.io/docs/api.html
+
+[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
+
+[19]: https://webdriver.io/docs/timeouts.html
 
-[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
+[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
 
-[19]: #fillfield
+[21]: #fillfield
diff --git a/docs/helpers/MockRequest.md b/docs/helpers/MockRequest.md
index 4177d4a7..c589802e 100644
--- a/docs/helpers/MockRequest.md
+++ b/docs/helpers/MockRequest.md
@@ -1,45 +1,166 @@
 ---
 permalink: /helpers/MockRequest
-editLink: false
 sidebar: auto
 title: MockRequest
 ---
 
+# MockRequest
+
+
 <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
 
 ## MockRequest
 
-**Extends Helper**
-
 This helper allows to **mock requests while running tests in Puppeteer or WebDriver**.
 For instance, you can block calls to 3rd-party services like Google Analytics, CDNs.
 Another way of using is to emulate requests from server by passing prepared data.
 
-### Installations
+MockRequest helper works in these [modes][1]:
 
-Requires [Polly.js][1] library by Netflix installed
+-   passthrough (default) - mock prefefined HTTP requests
+-   record - record all requests into a file
+-   replay - replay all recorded requests from a file
+
+Combining record/replay modes allows testing websites with large datasets. 
+
+To use in passthrough mode set rules to mock requests and they will be automatically intercepted and replaced:
+
+```js
+// default mode
+I.mockRequest('GET', '/api/users', '[]');
+```
+
+In record-replay mode start mocking to make HTTP requests recorded/replayed, and stop when you don't need to block requests anymore:
+
+```js
+// record or replay all XHR for /users page
+I.startMocking();
+I.amOnPage('/users');
+I.stopMocking();
+```
+
+### Installations
 
-    npm i @pollyjs/core @pollyjs/adapter-puppeteer --save-dev
+    npm i @codeceptjs/mock-request --save-dev
 
 Requires Puppeteer helper or WebDriver helper enabled
 
 ### Configuration
 
-Just enable helper in config file:
+#### With Puppeteer
+
+Enable helper in config file:
 
 ```js
 helpers: {
    Puppeteer: {
      // regular Puppeteer config here
    },
-   MockRequest: {}
+   MockRequestHelper: {
+     require: '@codeceptjs/mock-request',
+   }
 }
 ```
 
-> Partially works with WebDriver helper
-
 [Polly config options][2] can be passed as well:
 
+```js
+// sample options
+helpers: {
+  MockRequestHelper: {
+     require: '@codeceptjs/mock-request',
+     mode: record,
+     recordIfMissing: true,
+     recordFailedRequests: false,
+     expiresIn: null,
+     persisterOptions: {
+       keepUnusedRequests: false
+       fs: {
+         recordingsDir: './data/requests',
+       },
+    },
+  }
+}
+```
+
+* * *
+
+**TROUBLESHOOTING**: Puppeteer does not mock requests in headless mode: 
+
+Problem: request mocking does not work and in debug mode you see this in output:
+
+    Access to fetch at {url} from origin {url} has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
+
+Solution: update Puppeteer config to include `--disable-web-security` arguments:
+
+```js
+ Puppeteer: {
+   show: false,
+   chrome: {
+     args: [
+       '--disable-web-security',
+     ],
+   },
+ },
+```
+
+* * *
+
+#### With WebDriver
+
+This helper partially works with WebDriver. It can intercept and mock requests **only on already loaded page**.
+
+```js
+helpers: {
+   WebDriver: {
+     // regular WebDriver config here
+   },
+   MockRequestHelper: {
+     require: '@codeceptjs/mock-request',
+   }
+}
+```
+
+> Record/Replay mode is not tested in WebDriver but technically can work with [REST Persister][3]
+
+## Usage
+
+### 👻 Mock Requests
+
+To intercept API requests and mock them use following API
+
+-   [startMocking()][4] - to enable request interception
+-   [mockRequest()][5] - to define mock in a simple way
+-   [mockServer()][6] - to use PollyJS server API to define complex mocks
+-   [stopMocking()][7] - to stop intercepting requests and disable mocks.
+
+Calling `mockRequest` or `mockServer` will start mocking, if it was not enabled yet.
+
+```js
+I.startMocking(); // optionally
+I.mockRequest('/google-analytics/*path', 200);
+// return an empty successful response 
+I.mockRequest('GET', '/api/users', 200);
+// mock users api
+I.mockServer(server => {
+  server.get('https://server.com/api/users*').
+    intercept((req, res) => { res.status(200).json(users);
+  });
+});
+I.click('Get users);
+I.stopMocking();
+```
+
+### 📼 Record & Replay
+
+> At this moment works only with Puppeteer
+
+Record & Replay mode allows you to record all xhr & fetch requests and save them to file.
+On next runs those requests can be replayed. 
+By default, it stores all passed requests, but this behavior can be customized with `I.mockServer`
+
+Set mode via enironment variable, `replay` mode by default:
+
 ```js
 // enable replay mode
 helpers: {
@@ -47,39 +168,52 @@ helpers: {
    // regular Puppeteer config here
  },
  MockRequest: {
-    mode: 'replay',
+    require: '@codeceptjs/mock-request',
+    mode: process.env.MOCK_MODE || 'replay',
  },
 }
 ```
 
-### Usage
+Interactions between `I.startMocking()` and `I.stopMocking()` will be recorded and saved to `data/requests` directory.
 
-Use `I.mockRequest` to intercept and mock requests.
+```js
+I.startMocking() // record requests under 'Test' name
+I.startMocking('users') // record requests under 'users' name
+```
 
-### Parameters
+Use `I.mockServer()` to customize which requests should be recorded and under which name:
 
--   `config`  
+```js
+I.startMocking();
+I.mockServer((server) => {
+  // mock request only from ap1.com and api2.com and
+  // store recording into two different files
+  server.any('https://api1.com/*').passthrough(false).recordingName('api1');
+  server.any('https://api2.com/*').passthrough(false).recordingName('api2');
+});
+```
 
-### _checkAndStartMocking
+To stop request recording/replaying use `I.stopMocking()`.
 
-Starts mocking if it's not started yet.
+🎥 To record HTTP interactions execute tests with MOCK_MODE environment variable set as "record":
 
-### _connectPuppeteer
+    MOCK_MODE=record npx codeceptjs run --debug
 
-Creates a polly instance by registering puppeteer adapter with the instance
+📼 To replay them launch tests without environment variable:
 
-#### Parameters
+    npx codeceptjs run --debug
 
--   `title` **any** 
+### Parameters
 
-### _connectWebDriver
+-   `config`  
 
-Creates polly object in the browser window context using xhr and fetch adapters,
-after loading PollyJs and adapter scripts.
+### flushMocking
 
-#### Parameters
+Waits for all requests handled by MockRequests to be resolved:
 
--   `title` **any** 
+```js
+I.flushMocking();
+```
 
 ### mockRequest
 
@@ -100,33 +234,142 @@ I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
 
 #### Parameters
 
--   `method` **[string][3]** request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
--   `oneOrMoreUrls` **([string][3] | [Array][4]&lt;[string][3]>)** url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
--   `dataOrStatusCode` **([number][5] | [string][3] | [object][6])** status code when number provided. A response body otherwise
--   `additionalData` **([string][3] | [object][6])** response body when a status code is set by previous parameter. 
+-   `method` **[string][8]** request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
+-   `oneOrMoreUrls` **([string][8] \| [Array][9]&lt;[string][8]>)** url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
+-   `dataOrStatusCode` **([number][10] \| [string][8] \| [object][11])** status code when number provided. A response body otherwise
+-   `additionalData` **([string][8] \| [object][11])** response body when a status code is set by previous parameter. (optional, default `null`)
+
+### mockServer
+
+Use PollyJS [Server Routes API][12] to declare mocks via callback function:
+
+```js
+// basic usage
+server.get('/api/v2/users').intercept((req, res) => {
+  res.sendStatus(200).json({ users });
+});
+
+// passthrough requests to "/api/v2"
+server.get('/api/v1').passthrough();
+```
+
+In record replay mode you can define which routes should be recorded and where to store them:
+
+```js
+I.startMocking('mock');
+I.mockServer((server) => {
+
+  // record requests from cdn1.com and save them to data/recording/xml
+  server.any('https://cdn1.com/*').passthrough(false).recordingName('xml');
+  
+  // record requests from cdn2.com and save them to data/recording/svg
+  server.any('https://cdn2.com/*').passthrough(false).recordingName('svg');
+
+  // record requests from /api and save them to data/recording/mock (default)
+  server.any('/api/*').passthrough(false);
+});
+```
+
+#### Parameters
+
+-   `configFn`  
+
+### passthroughMocking
+
+Forces passthrough mode for mocking.
+Requires mocking to be started.
+
+```js
+I.passthroughMocking();
+```
+
+### recordMocking
+
+Forces record mode for mocking.
+Requires mocking to be started.
+
+```js
+I.recordMocking();
+```
+
+### replayMocking
+
+Forces replay mode for mocking.
+Requires mocking to be started.
+
+```js
+I.replayMocking();
+```
 
 ### startMocking
 
 Starts mocking of http requests.
-Used by mockRequest method to automatically start
-mocking requests.
+In record mode starts recording of all requests.
+In replay mode blocks all requests and replaces them with saved.
+
+If inside one test you plan to record/replay requests in several places, provide [recording name][13] as the parameter.
+
+```js
+// start mocking requests for a test
+I.startMocking(); 
+
+// start mocking requests for main page
+I.startMocking('main-page');
+// do actions
+I.stopMocking();
+I.startMocking('login-page');
+```
+
+To update [PollyJS configuration][14] use secon argument:
+
+```js
+// change mode
+I.startMocking('comments', { mode: 'replay' });
+
+// override config
+I.startMocking('users-loaded', {
+   recordFailedRequests: true
+})
+```
 
 #### Parameters
 
--   `title` **any**  
+-   `title` **any**  (optional, default `'Test'`)
+-   `config`   (optional, default `{}`)
 
 ### stopMocking
 
 Stops mocking requests.
+Must be called to save recorded requests into faile.
 
-[1]: https://netflix.github.io/pollyjs/#/
+```js
+I.stopMocking();
+```
+
+[1]: https://netflix.github.io/pollyjs/#/configuration?id=mode
 
 [2]: https://netflix.github.io/pollyjs/#/configuration?id=configuration
 
-[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
+[3]: https://netflix.github.io/pollyjs/#/examples?id=rest-persister
+
+[4]: #startMocking
+
+[5]: #mockRequest
+
+[6]: #mockServer
+
+[7]: #stopMocking
+
+[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
+
+[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
+
+[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
+
+[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
 
-[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
+[12]: https://netflix.github.io/pollyjs/#/server/overview
 
-[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
+[13]: https://netflix.github.io/pollyjs/#/api?id=recordingname
 
-[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
+[14]: https://netflix.github.io/pollyjs/#/configuration
diff --git a/docs/helpers/Nightmare.md b/docs/helpers/Nightmare.md
index dcc74f48..ce03ab10 100644
--- a/docs/helpers/Nightmare.md
+++ b/docs/helpers/Nightmare.md
@@ -357,8 +357,8 @@ let val = await I.executeAsyncScript(function(url, done) {
 
 #### Parameters
 
--   `fn` **([string][3] | [function][7])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][3] | [function][7])** function to be executed in browser context.
 
 Returns **[Promise][8]&lt;any>** Wrapper for asynchronous [evaluate][9].
 Unlike NightmareJS implementation calling `done` will return its first argument.
@@ -391,8 +391,8 @@ let date = await I.executeScript(function(el) {
 
 #### Parameters
 
--   `fn` **([string][3] | [function][7])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][3] | [function][7])** function to be executed in browser context.
 
 Returns **[Promise][8]&lt;any>** Wrapper for synchronous [evaluate][9]
 
@@ -451,6 +451,22 @@ assert(cookie.value, '123456');
 
 Returns **[Promise][8]&lt;[string][3]>** attribute valueCookie in JSON format. If name not passed returns all cookies for this domain.Multiple cookies can be received by passing query object `I.grabCookie({ secure: true});`. If you'd like get all cookies for all urls, use: `.grabCookie({ url: null }).`
 
+### grabCssPropertyFrom
+
+Grab CSS property for given locator
+Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+const value = await I.grabCssPropertyFrom('h3', 'font-weight');
+```
+
+#### Parameters
+
+-   `locator` **([string][3] | [object][4])** element located by CSS|XPath|strict locator.
+-   `cssProperty` **[string][3]** CSS property name.
+
+Returns **[Promise][8]&lt;[string][3]>** CSS value
+
 ### grabCurrentUrl
 
 Get current URL from browser.
diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md
index 500337d5..7655695f 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -38,7 +38,7 @@ This helper should be configured in codecept.json or codecept.conf.js
 -   `keepBrowserState`:  - keep browser state between tests when `restart` is set to false.
 -   `keepCookies`:  - keep cookies between tests when `restart` is set to false.
 -   `waitForAction`: (optional) how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
--   `waitForNavigation`: . When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Playwright API][2]. Array values are accepted as well.
+-   `waitForNavigation`: . When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. Choose one of those options is possible. See [Playwright API][2].
 -   `pressKeyDelay`: . Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
 -   `getPageTimeout`  config option to set maximum navigation time in milliseconds.
 -   `waitForTimeout`: (optional) default wait* timeout in ms. Default: 1000.
@@ -63,7 +63,7 @@ This helper should be configured in codecept.json or codecept.conf.js
 }
 ```
 
-#### Example #2: Wait for DOMContentLoaded event and 0 network connections
+#### Example #2: Wait for DOMContentLoaded event
 
 ```js
 {
@@ -71,7 +71,7 @@ This helper should be configured in codecept.json or codecept.conf.js
      Playwright : {
        url: "http://localhost",
        restart: false,
-       waitForNavigation: [ "domcontentloaded", "networkidle0" ],
+       waitForNavigation: "domcontentloaded",
        waitForAction: 500
      }
    }
@@ -164,7 +164,7 @@ await browserContext.cookies(); // get current browser context
 
 ### Parameters
 
--   `config`  
+-   `config`
 
 ### _addPopupListener
 
@@ -172,7 +172,7 @@ Add the 'dialog' event listener to a page
 
 #### Parameters
 
--   `page`  
+-   `page`
 
 ### _getPageUrl
 
@@ -189,7 +189,7 @@ const elements = await this.helpers['Playwright']._locate({name: 'password'});
 
 #### Parameters
 
--   `locator`  
+-   `locator`
 
 ### _locateCheckable
 
@@ -202,8 +202,8 @@ this.helpers['Playwright']._locateCheckable('I agree with terms and conditions')
 
 #### Parameters
 
--   `locator`  
--   `providedContext`   
+-   `locator`
+-   `providedContext`
 
 ### _locateClickable
 
@@ -215,7 +215,7 @@ this.helpers['Playwright']._locateClickable('Next page').then // ...
 
 #### Parameters
 
--   `locator`  
+-   `locator`
 
 ### _locateFields
 
@@ -227,7 +227,7 @@ this.helpers['Playwright']._locateFields('Your email').then // ...
 
 #### Parameters
 
--   `locator`  
+-   `locator`
 
 ### _setPage
 
@@ -330,7 +330,7 @@ I.checkOption('agree', '//form');
 #### Parameters
 
 -   `field` **([string][7] | [object][5])** checkbox located by label | name | CSS | XPath | strict locator.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator.
 
 ### clearCookie
 
@@ -344,7 +344,7 @@ I.clearCookie('test');
 
 #### Parameters
 
--   `cookie` **[string][7]?** (optional, `null` by default) cookie name 
+-   `cookie` **[string][7]?** (optional, `null` by default) cookie name
 
 ### clearField
 
@@ -358,7 +358,7 @@ I.clearField('#email');
 
 #### Parameters
 
--   `field`  
+-   `field`
 -   `editable` **([string][7] | [object][5])** field located by label|name|CSS|XPath|strict locator.
 
 ### click
@@ -388,7 +388,7 @@ I.click({css: 'nav a.login'});
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** clickable link or button located by text, or any element located by CSS|XPath|strict locator.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element to search in CSS|XPath|Strict locator. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element to search in CSS|XPath|Strict locator.
 
 ### clickLink
 
@@ -396,8 +396,8 @@ Clicks link and waits for navigation (deprecated)
 
 #### Parameters
 
--   `locator`  
--   `context`   
+-   `locator`
+-   `context`
 
 ### closeCurrentTab
 
@@ -428,7 +428,7 @@ I.dontSee('Login', '.nav'); // no login inside .nav element
 #### Parameters
 
 -   `text` **[string][7]** which is not present.
--   `context` **([string][7] | [object][5])?** (optional) element located by CSS|XPath|strict locator in which to perfrom search. 
+-   `context` **([string][7] | [object][5])?** (optional) element located by CSS|XPath|strict locator in which to perfrom search.
 
 ### dontSeeCheckboxIsChecked
 
@@ -527,7 +527,7 @@ I.dontSeeInSource('<!--'); // no comments in source
 
 #### Parameters
 
--   `text`  
+-   `text`
 -   `value` **[string][7]** to check.
 
 ### dontSeeInTitle
@@ -557,7 +557,7 @@ I.doubleClick('.btn.edit');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** clickable link or button located by text, or any element located by CSS|XPath|strict locator.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element to search in CSS|XPath|Strict locator. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element to search in CSS|XPath|Strict locator.
 
 ### dragAndDrop
 
@@ -585,7 +585,7 @@ I.dragSlider('#slider', -70);
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** located by label|name|CSS|XPath|strict locator.
--   `offsetX` **[number][8]** position to drag. 
+-   `offsetX` **[number][8]** position to drag.
 
 ### executeScript
 
@@ -612,8 +612,8 @@ If a function returns a Promise it will wait for its resolution.
 
 #### Parameters
 
--   `fn`  
--   `arg`  
+-   `fn`
+-   `arg`
 
 ### fillField
 
@@ -647,8 +647,8 @@ I.forceClick('Click me', '#hidden');
 
 #### Parameters
 
--   `locator`  
--   `context`   
+-   `locator`
+-   `context`
 
 ### grabAttributeFrom
 
@@ -676,7 +676,7 @@ let logs = await I.grabBrowserLogs();
 console.log(JSON.stringify(logs))
 ```
 
-Returns **[Promise][9]&lt;[Array][10]&lt;any>>** 
+Returns **[Promise][9]&lt;[Array][10]&lt;any>>**
 
 ### grabCookie
 
@@ -691,7 +691,7 @@ assert(cookie.value, '123456');
 
 #### Parameters
 
--   `name` **[string][7]?** cookie name. 
+-   `name` **[string][7]?** cookie name.
 
 Returns **[Promise][9]&lt;[string][7]>** attribute valueReturns cookie in JSON format. If name not passed returns all cookies for this domain.
 
@@ -769,7 +769,7 @@ const width = await I.grabElementBoundingRect('h3', 'width');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `prop`  
+-   `prop`
 -   `elementSize` **[string][7]** x, y, width or height of the given element.
 
 Returns **[object][5]** Element bounding rectangle
@@ -786,7 +786,7 @@ let postHTML = await I.grabHTMLFrom('#post');
 
 #### Parameters
 
--   `locator`  
+-   `locator`
 -   `element` **([string][7] | [object][5])** located by CSS|XPath|strict locator.
 
 Returns **[Promise][9]&lt;[string][7]>** HTML code for an element
@@ -834,7 +834,7 @@ Grab the text within the popup. If no popup is visible then it will return null
 await I.grabPopupText();
 ```
 
-Returns **[Promise][9]&lt;([string][7] | null)>** 
+Returns **[Promise][9]&lt;([string][7] | null)>**
 
 ### grabSource
 
@@ -890,6 +890,24 @@ let email = await I.grabValueFrom('input[name=email]');
 
 Returns **[Promise][9]&lt;[string][7]>** attribute value
 
+### handleDownloads
+
+Handles a file download.Aa file name is required to save the file on disk.
+Files are saved to "output" directory.
+
+Should be used with [FileSystem helper][11] to check that file were downloaded correctly.
+
+```js
+I.handleDownloads('downloads/avatar.jpg');
+I.click('Download Avatar');
+I.amInPath('output/downloads');
+I.waitForFile('downloads/avatar.jpg', 5);
+```
+
+#### Parameters
+
+-   `fileName` **[string][7]?** set filename for downloaded file
+
 ### haveRequestHeaders
 
 Set headers for all next requests
@@ -917,8 +935,8 @@ I.moveCursorTo('#submit', 5,5);
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** located by CSS|XPath|strict locator.
--   `offsetX` **[number][8]** (optional, `0` by default) X-axis offset. 
--   `offsetY` **[number][8]** (optional, `0` by default) Y-axis offset. 
+-   `offsetX` **[number][8]** (optional, `0` by default) X-axis offset.
+-   `offsetY` **[number][8]** (optional, `0` by default) Y-axis offset.
 
 ### openNewTab
 
@@ -928,7 +946,7 @@ Open new tab and switch to it
 I.openNewTab();
 ```
 
-You can pass in [page options][11] to emulate device on this page
+You can pass in [page options][12] to emulate device on this page
 
 ```js
 // enable mobile
@@ -937,13 +955,13 @@ I.openNewTab({ isMobile: true });
 
 #### Parameters
 
--   `options`  
+-   `options`
 
 ### pressKey
 
 Presses a key in the browser (on a focused element).
 
-_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][12].
+_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][13].
 
 ```js
 I.pressKey('Backspace');
@@ -1002,13 +1020,13 @@ Some of the supported key names are:
 
 #### Parameters
 
--   `key` **([string][7] | [Array][10]&lt;[string][7]>)** key or array of keys to press._Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Playwright#1313][13]).
+-   `key` **([string][7] | [Array][10]&lt;[string][7]>)** key or array of keys to press._Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Playwright#1313][14]).
 
 ### 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`][14]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][15]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1024,7 +1042,7 @@ I.pressKeyUp('Control');
 
 Releases a key in the browser which was previously set to a down state.
 
-To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][14]).
+To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][15]).
 
 ```js
 I.pressKeyDown('Control');
@@ -1076,7 +1094,7 @@ I.rightClick('Click me', '.context');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** clickable element located by CSS|XPath|strict locator.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|XPath|strict locator. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|XPath|strict locator.
 
 ### saveScreenshot
 
@@ -1092,7 +1110,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
 #### Parameters
 
 -   `fileName` **[string][7]** file name to save.
--   `fullPage` **[boolean][15]** (optional, `false` by default) flag to enable fullscreen screenshot mode. 
+-   `fullPage` **[boolean][16]** (optional, `false` by default) flag to enable fullscreen screenshot mode.
 
 ### scrollPageToBottom
 
@@ -1123,8 +1141,8 @@ I.scrollTo('#submit', 5, 5);
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** located by CSS|XPath|strict locator.
--   `offsetX` **[number][8]** (optional, `0` by default) X-axis offset. 
--   `offsetY` **[number][8]** (optional, `0` by default) Y-axis offset. 
+-   `offsetX` **[number][8]** (optional, `0` by default) X-axis offset.
+-   `offsetY` **[number][8]** (optional, `0` by default) Y-axis offset.
 
 ### see
 
@@ -1140,7 +1158,7 @@ I.see('Register', {css: 'form.register'}); // use strict locator
 #### Parameters
 
 -   `text` **[string][7]** expected on page.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text.
 
 ### seeAttributesOnElements
 
@@ -1340,7 +1358,7 @@ I.seeTextEquals('text', 'h1');
 #### Parameters
 
 -   `text` **[string][7]** element value to check.
--   `context` **([string][7] | [object][5]?)** element located by CSS|XPath|strict locator. 
+-   `context` **([string][7] | [object][5]?)** element located by CSS|XPath|strict locator.
 
 ### seeTitleEquals
 
@@ -1352,7 +1370,7 @@ I.seeTitleEquals('Test title.');
 
 #### Parameters
 
--   `text`  
+-   `text`
 
 ### selectOption
 
@@ -1411,7 +1429,7 @@ I.switchTo(); // switch back to main page
 
 #### Parameters
 
--   `locator` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|XPath|strict locator. 
+-   `locator` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS|XPath|strict locator.
 
 ### switchToNextTab
 
@@ -1424,7 +1442,7 @@ I.switchToNextTab(2);
 
 #### Parameters
 
--   `num` **[number][8]**  
+-   `num` **[number][8]**
 
 ### switchToPreviousTab
 
@@ -1437,7 +1455,7 @@ I.switchToPreviousTab(2);
 
 #### Parameters
 
--   `num` **[number][8]**  
+-   `num` **[number][8]**
 
 ### uncheckOption
 
@@ -1455,7 +1473,27 @@ I.uncheckOption('agree', '//form');
 #### Parameters
 
 -   `field` **([string][7] | [object][5])** checkbox located by label | name | CSS | XPath | strict locator.
--   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. 
+-   `context` **([string][7]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator.
+
+### usePlaywrightTo
+
+Use Playwright API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`page`][16], [`context`][17] [`browser`][18] } objects from Playwright API are available.
+
+```js
+I.usePlaywrightTo('emulate offline mode', async ({ context }) {
+  await context.setOffline(true);
+});
+```
+
+#### Parameters
+
+-   `description` **[string][7]** used to show in logs.
+-   `fn` **[function][19]** async functuion that executed with Playwright helper as argument
 
 ### wait
 
@@ -1482,7 +1520,7 @@ I.waitForClickable('.btn.continue', 5); // wait for 5 secs
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `waitTimeout`  
+-   `waitTimeout`
 -   `sec` **[number][8]?** (optional, `1` by default) time in seconds to wait
 
 ### waitForDetached
@@ -1497,7 +1535,7 @@ I.waitForDetached('#popup');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitForElement
 
@@ -1522,7 +1560,7 @@ Element can be located by CSS or XPath.
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `sec` **[number][8]** (optional) time in seconds to wait, 1 by default. 
+-   `sec` **[number][8]** (optional) time in seconds to wait, 1 by default.
 
 ### waitForFunction
 
@@ -1541,9 +1579,9 @@ I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and
 
 #### Parameters
 
--   `fn` **([string][7] | [function][16])** to be executed in browser context.
--   `argsOrSec` **([Array][10]&lt;any> | [number][8])?** (optional, `1` by default) arguments for function or seconds. 
--   `sec` **[number][8]?** (optional, `1` by default) time in seconds to wait 
+-   `fn` **([string][7] | [function][19])** to be executed in browser context.
+-   `argsOrSec` **([Array][10]&lt;any> | [number][8])?** (optional, `1` by default) arguments for function or seconds.
+-   `sec` **[number][8]?** (optional, `1` by default) time in seconds to wait
 
 ### waitForInvisible
 
@@ -1557,17 +1595,17 @@ I.waitForInvisible('#popup');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitForNavigation
 
 Waits for navigation to finish. By default takes configured `waitForNavigation` option.
 
-See [Pupeteer's reference][2]
+See [Pupeteer's reference][18]
 
 #### Parameters
 
--   `opts` **any**  
+-   `opts` **any**
 
 ### waitForRequest
 
@@ -1580,8 +1618,8 @@ I.waitForRequest(request => request.url() === 'http://example.com' && request.me
 
 #### Parameters
 
--   `urlOrPredicate` **([string][7] | [function][16])** 
--   `sec` **[number][8]?** seconds to wait 
+-   `urlOrPredicate` **([string][7] | [function][17])**
+-   `sec` **[number][8]?** seconds to wait
 
 ### waitForResponse
 
@@ -1594,8 +1632,8 @@ I.waitForResponse(request => request.url() === 'http://example.com' && request.m
 
 #### Parameters
 
--   `urlOrPredicate` **([string][7] | [function][16])** 
--   `sec` **[number][8]?** number of seconds to wait 
+-   `urlOrPredicate` **([string][7] | [function][19])**
+-   `sec` **[number][8]?** number of seconds to wait
 
 ### waitForText
 
@@ -1611,8 +1649,8 @@ I.waitForText('Thank you, form has been submitted', 5, '#modal');
 #### Parameters
 
 -   `text` **[string][7]** to wait for.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
--   `context` **([string][7] | [object][5])?** (optional) element located by CSS|XPath|strict locator. 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
+-   `context` **([string][7] | [object][5])?** (optional) element located by CSS|XPath|strict locator.
 
 ### waitForValue
 
@@ -1626,7 +1664,7 @@ I.waitForValue('//input', "GoodValue");
 
 -   `field` **([string][7] | [object][5])** input field.
 -   `value` **[string][7]** expected value.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitForVisible
 
@@ -1640,7 +1678,7 @@ I.waitForVisible('#popup');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to waitThis method accepts [React selectors][17]. 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to waitThis method accepts [React selectors][20].
 
 ### waitInUrl
 
@@ -1653,7 +1691,7 @@ I.waitInUrl('/info', 2);
 #### Parameters
 
 -   `urlPart` **[string][7]** value to check.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitNumberOfVisibleElements
 
@@ -1667,7 +1705,7 @@ I.waitNumberOfVisibleElements('a', 3);
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
 -   `num` **[number][8]** number of elements.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitToHide
 
@@ -1681,7 +1719,7 @@ I.waitToHide('#popup');
 #### Parameters
 
 -   `locator` **([string][7] | [object][5])** element located by CSS|XPath|strict locator.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 ### waitUntil
 
@@ -1694,10 +1732,10 @@ I.waitUntil(() => window.requests == 0, 5);
 
 #### Parameters
 
--   `fn` **([function][16] | [string][7])** function which is executed in browser context.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
--   `timeoutMsg` **[string][7]** message to show in case of timeout fail. 
--   `interval` **[number][8]?**  
+-   `fn` **([function][19] | [string][7])** function which is executed in browser context.
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
+-   `timeoutMsg` **[string][7]** message to show in case of timeout fail.
+-   `interval` **[number][8]?**
 
 ### waitUrlEquals
 
@@ -1711,11 +1749,11 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 #### Parameters
 
 -   `urlPart` **[string][7]** value to check.
--   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait 
+-   `sec` **[number][8]** (optional, `1` by default) time in seconds to wait
 
 [1]: https://github.com/microsoft/playwright
 
-[2]: https://github.com/GoogleChrome/Playwright/blob/master/docs/api.md#pagewaitfornavigationoptions
+[2]: https://github.com/microsoft/playwright/blob/master/docs/api.md#pagewaitfornavigationoptions
 
 [3]: https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
 
@@ -1733,16 +1771,26 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
 
-[11]: https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md#browsernewpageoptions
+[11]: https://codecept.io/helpers/FileSystem
+
+[12]: https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md#browsernewpageoptions
+
+[13]: #fillfield
+
+[14]: https://github.com/GoogleChrome/Playwright/issues/1313
+
+[15]: #click
+
+[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[12]: #fillfield
+[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
 
-[13]: https://github.com/GoogleChrome/Playwright/issues/1313
+[16]: https://github.com/microsoft/playwright/blob/master/docs/api.md#class-page
 
-[14]: #click
+[17]: https://github.com/microsoft/playwright/blob/master/docs/api.md#class-context
 
-[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+[18]: https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browser
 
-[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
+[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
 
-[17]: https://codecept.io/react
+[20]: https://codecept.io/react
diff --git a/docs/helpers/Protractor.md b/docs/helpers/Protractor.md
index 3e0ec644..5477408c 100644
--- a/docs/helpers/Protractor.md
+++ b/docs/helpers/Protractor.md
@@ -36,7 +36,7 @@ This helper should be configured in codecept.json or codecept.conf.js
 -   `waitForTimeout`: (optional) sets default wait time in _ms_ for all `wait*` functions. 1000 by default.
 -   `scriptsTimeout`: (optional) timeout in milliseconds for each script run on the browser, 10000 by default.
 -   `windowSize`: (optional) default window size. Set to `maximize` or a dimension in the format `640x480`.
--   `manualStart`  - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriverIO"]._startBrowser()`
+-   `manualStart`  - do not start browser before a test, start it manually inside a helper with `this.helpers.WebDriver._startBrowser()`
 -   `capabilities`: {} - list of [Desired Capabilities][5]
 -   `proxy`: set proxy settings
 
@@ -1282,6 +1282,26 @@ I.uncheckOption('agree', '//form');
 -   `field` **([string][9] | [object][10])** checkbox located by label | name | CSS | XPath | strict locator.
 -   `context` **([string][9]? | [object][10])** (optional, `null` by default) element located by CSS | XPath | strict locator. 
 
+### useProtractorTo
+
+Use [Protractor][18] API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`browser`][19]) } object from Protractor API is available.
+
+```js
+I.useProtractorTo('change url via in-page navigation', async ({ browser }) {
+   await browser.setLocation('api');
+});
+```
+
+#### Parameters
+
+-   `description` **[string][9]** used to show in logs.
+-   `fn` **[function][12]** async functuion that executed with Protractor helper as argument
+
 ### wait
 
 Pauses execution for a number of seconds.
@@ -1534,3 +1554,7 @@ just press button if no selector is given
 [16]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value
 
 [17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+
+[18]: https://www.protractortest.org/#/api
+
+[19]: https://www.protractortest.org/#/api?view=ProtractorBrowser
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 9a3092ab..f123604c 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -633,8 +633,8 @@ let val = await I.executeAsyncScript(function(url, done) {
 
 #### Parameters
 
--   `fn` **([string][8] | [function][12])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][8] | [function][12])** function to be executed in browser context.
 
 Returns **[Promise][13]&lt;any>** Asynchronous scripts can also be executed with `executeScript` if a function returns a Promise.
 
@@ -666,8 +666,8 @@ let date = await I.executeScript(function(el) {
 
 #### Parameters
 
--   `fn` **([string][8] | [function][12])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][8] | [function][12])** function to be executed in browser context.
 
 Returns **[Promise][13]&lt;any>** If a function returns a Promise It will wait for it resolution.
 
@@ -696,6 +696,42 @@ I.fillField({css: 'form#login input[name=username]'}, 'John');
 This action supports [React locators](https://codecept.io/react#locators)
 
 
+### forceClick
+
+Perform an emulated click on a link or a button, given by a locator.
+Unlike normal click instead of sending native event, emulates a click with JavaScript.
+This works on hidden, animated or inactive elements as well.
+
+If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
+For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
+For images, the "alt" attribute and inner text of any parent links are searched.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+// simple link
+I.forceClick('Logout');
+// button of form
+I.forceClick('Submit');
+// CSS button
+I.forceClick('#form input[type=submit]');
+// XPath
+I.forceClick('//form/*[@type=submit]');
+// link in context
+I.forceClick('Logout', '#nav');
+// using strict locator
+I.forceClick({css: 'nav a.login'});
+```
+
+#### Parameters
+
+-   `locator` **([string][8] | [object][6])** clickable link or button located by text, or any element located by CSS|XPath|strict locator.
+-   `context` **([string][8]? | [object][6])** (optional, `null` by default) element to search in CSS|XPath|Strict locator.
+
+
+This action supports [React locators](https://codecept.io/react#locators)
+ 
+
 ### grabAttributeFrom
 
 Retrieves an attribute from an element located by CSS or XPath and returns it to test.
@@ -1562,6 +1598,26 @@ I.uncheckOption('agree', '//form');
 -   `field` **([string][8] | [object][6])** checkbox located by label | name | CSS | XPath | strict locator.
 -   `context` **([string][8]? | [object][6])** (optional, `null` by default) element located by CSS | XPath | strict locator. 
 
+### usePuppeteerTo
+
+Use Puppeteer API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`page`][20], [`browser`][21] } from Puppeteer API are available.
+
+```js
+I.usePuppeteerTo('emulate offline mode', async ({ page }) {
+  await page.setOfflineMode(true);
+});
+```
+
+#### Parameters
+
+-   `description` **[string][8]** used to show in logs.
+-   `fn` **[function][12]** async function that is executed with Puppeteer as argument
+
 ### wait
 
 Pauses execution for a number of seconds.
@@ -1750,7 +1806,7 @@ I.waitForVisible('#popup');
 #### Parameters
 
 -   `locator` **([string][8] | [object][6])** element located by CSS|XPath|strict locator.
--   `sec` **[number][10]** (optional, `1` by default) time in seconds to waitThis method accepts [React selectors][20]. 
+-   `sec` **[number][10]** (optional, `1` by default) time in seconds to waitThis method accepts [React selectors][22]. 
 
 ### waitInUrl
 
@@ -1866,4 +1922,8 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[20]: https://codecept.io/react
+[20]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-page
+
+[21]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-browser
+
+[22]: https://codecept.io/react
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 60f0f3c9..62cc4d77 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -391,12 +391,14 @@ this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').
 Find a clickable element by providing human readable text:
 
 ```js
-this.helpers['WebDriver']._locateClickable('Next page').then // ...
+const els = await this.helpers.WebDriver._locateClickable('Next page');
+const els = await this.helpers.WebDriver._locateClickable('Next page', '.pages');
 ```
 
 #### Parameters
 
 -   `locator` **([string][19] | [object][18])** element located by CSS|XPath|strict locator.
+-   `context`  
 
 ### _locateFields
 
@@ -810,8 +812,8 @@ let val = await I.executeAsyncScript(function(url, done) {
 
 #### Parameters
 
--   `fn` **([string][19] | [function][24])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][19] | [function][24])** function to be executed in browser context.
 
 Returns **[Promise][25]&lt;any>** 
 
@@ -843,8 +845,8 @@ let date = await I.executeScript(function(el) {
 
 #### Parameters
 
--   `fn` **([string][19] | [function][24])** function to be executed in browser context.
 -   `args` **...any** to be passed to function.
+-   `fn` **([string][19] | [function][24])** function to be executed in browser context.
 
 Returns **[Promise][25]&lt;any>** Wraps [execute][26] command.
 
@@ -873,6 +875,42 @@ I.fillField({css: 'form#login input[name=username]'}, 'John');
 This action supports [React locators](https://codecept.io/react#locators)
 
 
+### forceClick
+
+Perform an emulated click on a link or a button, given by a locator.
+Unlike normal click instead of sending native event, emulates a click with JavaScript.
+This works on hidden, animated or inactive elements as well.
+
+If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
+For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
+For images, the "alt" attribute and inner text of any parent links are searched.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+// simple link
+I.forceClick('Logout');
+// button of form
+I.forceClick('Submit');
+// CSS button
+I.forceClick('#form input[type=submit]');
+// XPath
+I.forceClick('//form/*[@type=submit]');
+// link in context
+I.forceClick('Logout', '#nav');
+// using strict locator
+I.forceClick({css: 'nav a.login'});
+```
+
+#### Parameters
+
+-   `locator` **([string][19] | [object][18])** clickable link or button located by text, or any element located by CSS|XPath|strict locator.
+-   `context` **([string][19]? | [object][18])** (optional, `null` by default) element to search in CSS|XPath|Strict locator.
+
+
+This action supports [React locators](https://codecept.io/react#locators)
+ 
+
 ### grabAllWindowHandles
 
 Get all Window Handles.
@@ -1773,6 +1811,27 @@ I.uncheckOption('agree', '//form');
 -   `context` **([string][19]? | [object][18])** (optional, `null` by default) element located by CSS | XPath | strict locator.
     Appium: not tested 
 
+### useWebDriverTo
+
+Use [webdriverio][34] API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`browser`][34]) } object from WebDriver API is available.
+
+```js
+I.useWebDriverTo('open multiple windows', async ({ browser }) {
+   // create new window
+   await browser.newWindow('https://webdriver.io');
+});
+```
+
+#### Parameters
+
+-   `description` **[string][19]** used to show in logs.
+-   `fn` **[function][24]** async functuion that executed with WebDriver helper as argument
+
 ### wait
 
 Pauses execution for a number of seconds.
@@ -2056,3 +2115,5 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 [32]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
 
 [33]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
+
+[34]: https://webdriver.io/docs/api.html
diff --git a/docs/parallel.md b/docs/parallel.md
index 7f5115af..e4dd53f2 100644
--- a/docs/parallel.md
+++ b/docs/parallel.md
@@ -74,7 +74,7 @@ const testGroups = workers.createGroupsOfSuites(2);
 const browsers = ['firefox', 'chrome'];
 
 const configs = browsers.map(browser => {
-  return helpers: { 
+  return helpers: {
     WebDriver: { browser }
   }
 });
@@ -127,7 +127,7 @@ If you want your tests to split according to your need this method is suited for
 ```js
 
 /*
- Define a function to split your tests, Here its hardcoded, But you write a logic to split your tests 
+ Define a function to split your tests.
 
  function should return an array with this format [[file1, file2], [file3], ...]
 
diff --git a/docs/playwright.md b/docs/playwright.md
index 0de3b4d0..29c067bf 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -261,9 +261,24 @@ Scenario('website looks nice on iPhone', () => {
 });
 ```
 
-## Extending
+## Accessing Playwright API
 
-Playwright has a very [rich and flexible API](https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md). Sure, you can extend your test suites to use the methods listed there. CodeceptJS already prepares some objects for you and you can use them from your you helpers.
+To get [Playwright API](https://github.com/microsoft/playwright/blob/master/docs/api.md) inside a test use `I.usePlaywrightTo` method with a callback.
+To keep test readable provide a description of a callback inside the first parameter.
+
+```js
+I.usePlaywrightTo('emulate offline mode', async ({ browser, context, page }) => {
+  // use browser, page, context objects inside this function
+  await context.setOffline(true);
+});
+```
+
+Because all Playwright commands are asynchronous callback function must be async.
+
+
+## Extending Helper
+
+To create custom `I.*` commands using Playwright API you need to create a custom helper.
 
 Start with creating an `MyPlaywright` helper using `generate:helper` or `gh` command:
 
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index a8a88ba5..0f9d52d1 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -199,6 +199,7 @@ CodeceptJS allows you to implement custom actions like `I.createTodo` or use **P
 
 > [▶ Demo project is available on GitHub](https://github.com/DavertMik/codeceptjs-todomvc-puppeteer)
 
+
 ## Mocking Requests
 
 Web application sends various requests to local services (Rest API, GraphQL) or to 3rd party services (CDNS, Google Analytics, etc).
@@ -247,9 +248,23 @@ npx codeceptjs def
 Mocking rules will be kept while a test is running. To stop mocking use `I.stopMocking()` command
 
 
-## Extending
+## Accessing Puppeteer API
+
+To get Puppeteer API inside a test use [`I.usePupepteerTo`](/helpers/Puppeteer/#usepuppeteerto) method with a callback.
+To keep test readable provide a description of a callback inside the first parameter.
+
+```js
+I.usePuppeteerTo('emulate offline mode', async ({ page, browser }) => {
+  await page.setOfflineMode(true);
+});
+```
+
+Because all Puppeteer commands are asynchronous callback function must be async.
+
+
+## Extending Helper
 
-Puppeteer has a very [rich and flexible API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md). Sure, you can extend your test suites to use the methods listed there. CodeceptJS already prepares some objects for you and you can use them from your you helpers.
+To create custom `I.*` commands using Playwright API you need to create a custom helper.
 
 Start with creating an `MyPuppeteer` helper using `generate:helper` or `gh` command:
 
diff --git a/docs/reports.md b/docs/reports.md
index 59bf4384..ce13a639 100644
--- a/docs/reports.md
+++ b/docs/reports.md
@@ -199,6 +199,18 @@ Allure reports can also be generated for `dry-run` command. So you can get the f
 npx codeceptjs dry-run --debug -p allure
 ```
 
+## ReportPortal
+
+Allure is a great reportin tool, however, if you are running tests on different machines it is hard to merge its XML result files to build a proper report. So, for enterprise grade reporting we recommend using [ReportPortal](https://reportportal.io).
+
+![](https://camo.githubusercontent.com/6550c0365f1d0ce1e29c53f1860b12957d1fc529/68747470733a2f2f692e6962622e636f2f516d353247306e2f53637265656e73686f742d323031392d30342d31312d61742d31352d35372d34302e706e67)
+
+[ReportPortal](https://reportportal.io) is open-source self-hosted service for aggregating test execution reports.
+Think of it as Kibana but for test reports.
+
+Use official [CodeceptJS Agent for ReportPortal](https://github.com/reportportal/agent-js-codecept/) to start publishing your test results.
+
+
 ## XML
 
 Use default xunit reporter of Mocha to print xml reports. Provide `--reporter xunit` to get the report to screen.
diff --git a/docs/visual.md b/docs/visual.md
index b50ea6de..85cf728a 100644
--- a/docs/visual.md
+++ b/docs/visual.md
@@ -112,79 +112,6 @@ MisMatch Percentage Calculated is 2.85
 1) `seeVisualDiff` which can be used to compare two images and calculate the misMatch percentage.
 2) `seeVisualDiffForElement` which can be used to compare elements on the two images and calculate misMatch percentage.
 
-## Using Visual Knight
-
-Visual Knight is a SaaS product which strongly supports CodeceptJS with multiple use cases. It provides an user interface to handle mismatches, statistics and more. It was designed to support Designer, Product Owner and other roles which are not familiar with coding and all this tools. All captured images are saved in a secure cloud system to not mess up your git repository.
-
-### Setup
-
-Create an account at [Visual Knight](https://www.visual-knight.io) and install the npm package
-
-```
-npm install @visual-knight/codeceptjs -D
-```
-
-### Configuring
-
-```json
-{
-    "helpers": {
-        "VisualKnight": {
-            "require": "@visual-knight/codeceptjs",
-            "key": "YOUR_API_KEY",
-            "project": "YOUR_PROJECT_ID OR PROJECT_NAME"
-        }
-    }
-}
-```
-
-### Usage
-
-```javascript
-/**
- * @param testName {string} Is provided to visual knight (must be unique)
- * @param options {ScreenshotOptions} Contains additional settings
- */
-I.compareFullpageScreenshot(testName, options)
-/**
- * @param testName {string} Is provided to visual knight (must be unique)
- * @param options {ScreenshotOptions} Contains additional settings
- */
-I.compareViewportScreenshot(testName, options)
-/**
- * @param cssSelector {string} Is provided to visual knight
- * @param testName {string} Is provided to visual knight (must be unique)
- * @param options {ScreenshotOptions} Contains additional settings
- */
-I.compareElementScreenshot(cssSelector, testName, options)
-
-/*
-ScreenshotOptions {
-  hide?: string[] // Array of css selectors which gets hidden by "opacity: 0",
-  remove?: string[] // Array of css selectors which gets hidden by "display: none",
-  additional?: object // Data is saved as relation to the variation. (Future: can be used for filtering)
-}
-*/
-```
-
-> You can find the latest documentation here [CodeceptJS helper page](https://doc.visual-knight.io/adapters/codeceptjs)
-
-### Example
-
-Lets consider visual testing for [CodeceptJS Home](http://codecept.io)
-
-```js
-Feature('To test screen comparison with Visual Knight Example test');
-
-Scenario('Compare CodeceptIO Home Page @visual-test', async (I, adminPage) => {
-    I.amOnPage("/");
-    I.compareFullpageScreenshot("CodeceptIO Home Page")
-});
-```
-
-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.
diff --git a/docs/webapi/forceClick.mustache b/docs/webapi/forceClick.mustache
new file mode 100644
index 00000000..548c21d6
--- /dev/null
+++ b/docs/webapi/forceClick.mustache
@@ -0,0 +1,27 @@
+Perform an emulated click on a link or a button, given by a locator.
+Unlike normal click instead of sending native event, emulates a click with JavaScript.
+This works on hidden, animated or inactive elements as well.
+
+If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
+For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
+For images, the "alt" attribute and inner text of any parent links are searched.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+// simple link
+I.forceClick('Logout');
+// button of form
+I.forceClick('Submit');
+// CSS button
+I.forceClick('#form input[type=submit]');
+// XPath
+I.forceClick('//form/*[@type=submit]');
+// link in context
+I.forceClick('Logout', '#nav');
+// using strict locator
+I.forceClick({css: 'nav a.login'});
+```
+
+@param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
+@param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
diff --git a/docs/webapi/grabAttributeFrom.mustache b/docs/webapi/grabAttributeFrom.mustache
index 640fbb97..2b9ee1ba 100644
--- a/docs/webapi/grabAttributeFrom.mustache
+++ b/docs/webapi/grabAttributeFrom.mustache
@@ -1,10 +1,10 @@
 Retrieves an attribute from an element located by CSS or XPath and returns it to test.
-An array as a result will be returned if there are more than one matched element.
 Resumes test execution, so **should be used inside async with `await`** operator.
+If more than one element is found - attribute of first element is returned.
 
 ```js
 let hint = await I.grabAttributeFrom('#tooltip', 'title');
 ```
 @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
 @param {string} attr attribute name.
-@returns {Promise<string>} attribute value
\ No newline at end of file
+@returns {Promise<string>} attribute value
diff --git a/docs/webapi/grabAttributeFromAll.mustache b/docs/webapi/grabAttributeFromAll.mustache
new file mode 100644
index 00000000..3299843c
--- /dev/null
+++ b/docs/webapi/grabAttributeFromAll.mustache
@@ -0,0 +1,9 @@
+Retrieves an array of attributes from elements located by CSS or XPath and returns it to test.
+Resumes test execution, so **should be used inside async with `await`** operator.
+
+```js
+let hints = await I.grabAttributeFromAll('.tooltip', 'title');
+```
+@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
+@param {string} attr attribute name.
+@returns {Promise<string[]>} attribute value
diff --git a/docs/webapi/grabCssPropertyFrom.mustache b/docs/webapi/grabCssPropertyFrom.mustache
index b3c09a98..bd705d4a 100644
--- a/docs/webapi/grabCssPropertyFrom.mustache
+++ b/docs/webapi/grabCssPropertyFrom.mustache
@@ -1,5 +1,6 @@
 Grab CSS property for given locator
 Resumes test execution, so **should be used inside an async function with `await`** operator.
+If more than one element is found - value of first element is returned.
 
 ```js
 const value = await I.grabCssPropertyFrom('h3', 'font-weight');
@@ -7,4 +8,4 @@ const value = await I.grabCssPropertyFrom('h3', 'font-weight');
 
 @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
 @param {string} cssProperty CSS property name.
-@returns {Promise<string>} CSS value
\ No newline at end of file
+@returns {Promise<string>} CSS value
diff --git a/docs/webapi/grabCssPropertyFromAll.mustache b/docs/webapi/grabCssPropertyFromAll.mustache
new file mode 100644
index 00000000..de981fcb
--- /dev/null
+++ b/docs/webapi/grabCssPropertyFromAll.mustache
@@ -0,0 +1,10 @@
+Grab array of CSS properties for given locator
+Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+const values = await I.grabCssPropertyFromAll('h3', 'font-weight');
+```
+
+@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
+@param {string} cssProperty CSS property name.
+@returns {Promise<string[]>} CSS value
diff --git a/docs/webapi/grabHTMLFrom.mustache b/docs/webapi/grabHTMLFrom.mustache
index 4f68b21d..516bbe03 100644
--- a/docs/webapi/grabHTMLFrom.mustache
+++ b/docs/webapi/grabHTMLFrom.mustache
@@ -1,10 +1,10 @@
 Retrieves the innerHTML from an element located by CSS or XPath and returns it to test.
 Resumes test execution, so **should be used inside async function with `await`** operator.
-If more than one element is found - an array of HTMLs returned.
+If more than one element is found - HTML of first element is returned.
 
 ```js
 let postHTML = await I.grabHTMLFrom('#post');
 ```
 
 @param {CodeceptJS.LocatorOrString} element located by CSS|XPath|strict locator.
-@returns {Promise<string>} HTML code for an element
\ No newline at end of file
+@returns {Promise<string>} HTML code for an element
diff --git a/docs/webapi/grabHTMLFromAll.mustache b/docs/webapi/grabHTMLFromAll.mustache
new file mode 100644
index 00000000..c4a5f3e1
--- /dev/null
+++ b/docs/webapi/grabHTMLFromAll.mustache
@@ -0,0 +1,9 @@
+Retrieves all the innerHTML from elements located by CSS or XPath and returns it to test.
+Resumes test execution, so **should be used inside async function with `await`** operator.
+
+```js
+let postHTMLs = await I.grabHTMLFromAll('.post');
+```
+
+@param {CodeceptJS.LocatorOrString} element located by CSS|XPath|strict locator.
+@returns {Promise<string[]>} HTML code for an element
diff --git a/docs/webapi/grabTextFrom.mustache b/docs/webapi/grabTextFrom.mustache
index e9be59b6..3d995f55 100644
--- a/docs/webapi/grabTextFrom.mustache
+++ b/docs/webapi/grabTextFrom.mustache
@@ -4,7 +4,7 @@ Resumes test execution, so **should be used inside async with `await`** operator
 ```js
 let pin = await I.grabTextFrom('#pin');
 ```
-If multiple elements found returns an array of texts.
+If multiple elements found returns first element.
 
 @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
-@returns {Promise<string|string[]>} attribute value
\ No newline at end of file
+@returns {Promise<string>} attribute value
diff --git a/docs/webapi/grabTextFromAll.mustache b/docs/webapi/grabTextFromAll.mustache
new file mode 100644
index 00000000..4c26eb0e
--- /dev/null
+++ b/docs/webapi/grabTextFromAll.mustache
@@ -0,0 +1,9 @@
+Retrieves all texts from an element located by CSS or XPath and returns it to test.
+Resumes test execution, so **should be used inside async with `await`** operator.
+
+```js
+let pins = await I.grabTextFromAll('#pin li');
+```
+
+@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
+@returns {Promise<string[]>} attribute value
diff --git a/docs/webapi/grabValueFrom.mustache b/docs/webapi/grabValueFrom.mustache
index c66034e3..8c1258b4 100644
--- a/docs/webapi/grabValueFrom.mustache
+++ b/docs/webapi/grabValueFrom.mustache
@@ -1,8 +1,9 @@
 Retrieves a value from a form element located by CSS or XPath and returns it to test.
 Resumes test execution, so **should be used inside async function with `await`** operator.
+If more than one element is found - value of first element is returned.
 
 ```js
 let email = await I.grabValueFrom('input[name=email]');
 ```
 @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
-@returns {Promise<string>} attribute value
\ No newline at end of file
+@returns {Promise<string>} attribute value
diff --git a/docs/webapi/grabValueFromAll.mustache b/docs/webapi/grabValueFromAll.mustache
new file mode 100644
index 00000000..ee2a5898
--- /dev/null
+++ b/docs/webapi/grabValueFromAll.mustache
@@ -0,0 +1,8 @@
+Retrieves an array of value from a form located by CSS or XPath and returns it to test.
+Resumes test execution, so **should be used inside async function with `await`** operator.
+
+```js
+let inputs = await I.grabValueFromAll('//form/input');
+```
+@param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
+@returns {Promise<string[]>} attribute value
diff --git a/docs/webapi/waitForVisible.mustache b/docs/webapi/waitForVisible.mustache
index 1e97e17f..b4e05e46 100644
--- a/docs/webapi/waitForVisible.mustache
+++ b/docs/webapi/waitForVisible.mustache
@@ -6,4 +6,4 @@ I.waitForVisible('#popup');
 ```
 
 @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
-@param {number} [sec=1] (optional, `1` by default) time in seconds to wait
\ No newline at end of file
+@param {number} [sec=1] (optional, `1` by default) time in seconds to wait
diff --git a/docs/webdriver.md b/docs/webdriver.md
index 5487d490..542a14bc 100644
--- a/docs/webdriver.md
+++ b/docs/webdriver.md
@@ -53,6 +53,8 @@ exports.config = {
 }
 ```
 
+> ⚠ It is not recommended to use wdio plugin & selenium-standalone when running tests in parallel. Consider **switching to Selenoid** if you need parallel run or using cloud services.
+
 
 ## Configuring WebDriver
 
@@ -104,7 +106,7 @@ path: '/',
 
 > If you face issues connecting to WebDriver, please check that corresponding server is running on a specified port. If host is other than `localhost` or port is other than `4444`, update the configuration.
 
-### Selenium in Docker
+### Selenium in Docker (Selenoid)
 
 Browsers can be executed in Docker containers. This is useful when testing on Continous Integration server.
 We recommend using [Selenoid](https://aerokube.com/selenoid/) to run browsers in container.
@@ -465,6 +467,20 @@ npx codeceptjs def
 
 Mocking rules will be kept while a test is running. To stop mocking use `I.stopMocking()` command
 
+
+## Accessing webdriverio API
+
+To get [webdriverio browser API](https://webdriver.io/docs/api.html) inside a test use [`I.useWebDriverTo`](/helpers/WebDriver/#usewebdriverto) method with a callback.
+To keep test readable provide a description of a callback inside the first parameter.
+
+```js
+I.useWebDriverTo('do some things using native webdriverio api', async ({ browser }) => {
+  // use browser object here
+});
+```
+
+Because all webdriverio commands are asynchronous a callback function must be async.
+
 ## Extending WebDriver
 
 CodeceptJS doesn't aim to embrace all possible functionality of WebDriver. At some points you may find that some actions do not exist, however it is easy to add one. You will need to use WebDriver API from [webdriver.io](https://webdriver.io) library.
diff --git a/examples/codecept.conf.js b/examples/codecept.conf.js
index 2ce38117..ca64236d 100644
--- a/examples/codecept.conf.js
+++ b/examples/codecept.conf.js
@@ -31,6 +31,9 @@ exports.config = {
     ],
   },
   plugins: {
+    tryTo: {
+      enabled: true,
+    },
     allure: {
       enabled: false,
     },
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index 04e77ae3..b219009e 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -13,7 +13,6 @@ ${importPaths.join('\n')}
 
 declare namespace CodeceptJS {
   interface SupportObject ${convertMapToType(supportObject)}
-  interface CallbackOrder { ${convertMapToIndexedInterface(supportObject)} }
   ${helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : ''}
   interface I extends ${hasCustomStepsFile ? 'ReturnType<steps_file>' : 'WithTranslation<Methods>'} {}
   namespace Translation {
@@ -123,13 +122,6 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
   return importStrings;
 }
 
-/**
- * @param {Map} map
- */
-function convertMapToIndexedInterface(map) {
-  return [...map.values()].map((value, i) => `[${i}]: ${value}`).join('; ');
-}
-
 /**
  * @param {Map} map
  */
diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js
index 459e2341..952383b1 100644
--- a/lib/command/run-workers.js
+++ b/lib/command/run-workers.js
@@ -42,7 +42,7 @@ module.exports = function (workerCount, options) {
     output.test.passed(successTest);
   });
 
-  workers.on(event.all.result, (_status, _completedTest, workerStats) => {
+  workers.on(event.all.result, () => {
     workers.printResults();
   });
 };
diff --git a/lib/event.js b/lib/event.js
index e73a2cd9..1b0b7fc5 100644
--- a/lib/event.js
+++ b/lib/event.js
@@ -24,6 +24,7 @@ module.exports = {
    * @property {'test.passed'} passed
    * @property {'test.failed'} failed
    * @property {'test.finish'} finished
+   * @property {'test.skipped'} skipped
    */
   test: {
     started: 'test.start', // sync
@@ -32,6 +33,7 @@ module.exports = {
     passed: 'test.passed', // sync
     failed: 'test.failed', // sync
     finished: 'test.finish', // sync
+    skipped: 'test.skipped', // sync
   },
   /**
    * @type {object}
diff --git a/lib/helper.js b/lib/helper.js
index 8f115d68..fc438223 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -1,6 +1,6 @@
 const container = require('./container');
 const output = require('./output');
-
+const { isAsyncFunction } = require('./utils');
 /**
  * @inner
  * Abstract class.
@@ -154,6 +154,15 @@ class Helper {
 
   }
 
+  /**
+  * Abstract method to provide common interface to accessing helpers internals inside a test.
+  */
+  _useTo(description, fn) {
+    if (!isAsyncFunction(fn)) throw new Error(`Not async function!\n${fn}\nNative helpers API is asynchronous, please update this function be async`);
+    fn.toString = 'fn()';
+    return fn(this);
+  }
+
   /**
    * Access another configured helper: `this.helpers['AnotherHelper']`
    *
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 2d4e9e1c..b3c9c212 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -1338,6 +1338,15 @@ class Appium extends Webdriver {
     return super.fillField(parseLocator.call(this, field), value);
   }
 
+  /**
+   * {{> grabTextFromAll }}
+   *
+   */
+  async grabTextFromAll(locator) {
+    if (this.isWeb) return super.grabTextFromAll(locator);
+    return super.grabTextFromAll(parseLocator.call(this, locator));
+  }
+
   /**
    * {{> grabTextFrom }}
    *
@@ -1347,6 +1356,15 @@ class Appium extends Webdriver {
     return super.grabTextFrom(parseLocator.call(this, locator));
   }
 
+  /**
+   * {{> grabValueFromAll }}
+   *
+   */
+  async grabValueFromAll(locator) {
+    if (this.isWeb) return super.grabValueFromAll(locator);
+    return super.grabValueFromAll(parseLocator.call(this, locator));
+  }
+
   /**
    * {{> grabValueFrom }}
    *
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index f46c2f13..615ae8ed 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -778,63 +778,125 @@ class Nightmare extends Helper {
   }
 
   /**
-   * {{> grabTextFrom }}
+   * {{> grabTextFromAll }}
    */
-  async grabTextFrom(locator) {
+  async grabTextFromAll(locator) {
     locator = new Locator(locator, 'css');
     const els = await this.browser.findElements(locator.toStrict());
-    assertElementExists(els[0], locator);
     const texts = [];
     const getText = el => window.codeceptjs.fetchElement(el).innerText;
     for (const el of els) {
       texts.push(await this.browser.evaluate(getText, el));
     }
-    if (texts.length === 1) return texts[0];
     return texts;
   }
 
+  /**
+   * {{> grabTextFrom }}
+   */
+  async grabTextFrom(locator) {
+    locator = new Locator(locator, 'css');
+    const els = await this.browser.findElement(locator.toStrict());
+    assertElementExists(els, locator);
+    const texts = await this.grabTextFromAll(locator);
+    if (texts.length > 1) {
+      this.debugSection('GrabText', `Using first element out of ${texts.length}`);
+    }
+
+    return texts[0];
+  }
+
+  /**
+   * {{> grabValueFrom }}
+   */
+  async grabValueFromAll(locator) {
+    locator = new Locator(locator, 'css');
+    const els = await this.browser.findElements(locator.toStrict());
+    const values = [];
+    const getValues = el => window.codeceptjs.fetchElement(el).value;
+    for (const el of els) {
+      values.push(await this.browser.evaluate(getValues, el));
+    }
+
+    return values;
+  }
+
   /**
    * {{> grabValueFrom }}
    */
   async grabValueFrom(locator) {
     const el = await findField.call(this, locator);
     assertElementExists(el, locator, 'Field');
-    return this.browser.evaluate(el => window.codeceptjs.fetchElement(el).value, el);
+    const values = await this.grabValueFromAll(locator);
+    if (values.length > 1) {
+      this.debugSection('GrabValue', `Using first element out of ${values.length}`);
+    }
+
+    return values[0];
   }
 
   /**
-   * {{> grabAttributeFrom }}
+   * {{> grabAttributeFromAll }}
    */
-  async grabAttributeFrom(locator, attr) {
+  async grabAttributeFromAll(locator, attr) {
     locator = new Locator(locator, 'css');
     const els = await this.browser.findElements(locator.toStrict());
     const array = [];
 
     for (let index = 0; index < els.length; index++) {
       const el = els[index];
-      assertElementExists(el, locator);
       array.push(await this.browser.evaluate((el, attr) => window.codeceptjs.fetchElement(el).getAttribute(attr), el, attr));
     }
 
-    return array.length === 1 ? array[0] : array;
+    return array;
   }
 
   /**
-   * {{> grabHTMLFrom }}
+   * {{> grabAttributeFrom }}
    */
-  async grabHTMLFrom(locator) {
+  async grabAttributeFrom(locator, attr) {
+    locator = new Locator(locator, 'css');
+    const els = await this.browser.findElement(locator.toStrict());
+    assertElementExists(els, locator);
+
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    if (attrs.length > 1) {
+      this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
+    }
+
+    return attrs[0];
+  }
+
+  /**
+   * {{> grabHTMLFromAll }}
+   */
+  async grabHTMLFromAll(locator) {
     locator = new Locator(locator, 'css');
     const els = await this.browser.findElements(locator.toStrict());
     const array = [];
 
     for (let index = 0; index < els.length; index++) {
       const el = els[index];
-      assertElementExists(el, locator);
       array.push(await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).innerHTML, el));
     }
-    this.debugSection('HTML', array);
+    this.debugSection('GrabHTML', array);
+
+    return array;
+  }
+
+  /**
+   * {{> grabHTMLFrom }}
+   */
+  async grabHTMLFrom(locator) {
+    locator = new Locator(locator, 'css');
+    const els = await this.browser.findElement(locator.toStrict());
+    assertElementExists(els, locator);
+    const html = await this.grabHTMLFromAll(locator);
+    if (html.length > 1) {
+      this.debugSection('GrabHTML', `Using first element out of ${html.length}`);
+    }
 
-    return array.length === 1 ? array[0] : array;
+    return html[0];
   }
 
   /**
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index bdd96e68..f3761ee8 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -1,5 +1,6 @@
 const requireg = require('requireg');
 const path = require('path');
+const fs = require('fs');
 
 const Helper = require('../helper');
 const Locator = require('../locator');
@@ -9,7 +10,6 @@ const { urlEquals } = require('../assert/equal');
 const { equals } = require('../assert/equal');
 const { empty } = require('../assert/empty');
 const { truth } = require('../assert/truth');
-const isElementClickable = require('./scripts/isElementClickable');
 const {
   xpathLocator,
   ucfirst,
@@ -69,7 +69,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
  * * `keepBrowserState`: (optional, default: false) - keep browser state between tests when `restart` is set to false.
  * * `keepCookies`: (optional, default: false) - keep cookies between tests when `restart` is set to false.
  * * `waitForAction`: (optional) how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
- * * `waitForNavigation`: (optional, default: 'load'). When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Playwright API](https://github.com/GoogleChrome/Playwright/blob/master/docs/api.md#pagewaitfornavigationoptions). Array values are accepted as well.
+ * * `waitForNavigation`: (optional, default: 'load'). When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. Choose one of those options is possible. See [Playwright API](https://github.com/microsoft/playwright/blob/master/docs/api.md#pagewaitfornavigationoptions).
  * * `pressKeyDelay`: (optional, default: '10'). Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
  * * `getPageTimeout` (optional, default: '0') config option to set maximum navigation time in milliseconds.
  * * `waitForTimeout`: (optional) default wait* timeout in ms. Default: 1000.
@@ -94,7 +94,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
  * }
  * ```
  *
- * #### Example #2: Wait for DOMContentLoaded event and 0 network connections
+ * #### Example #2: Wait for DOMContentLoaded event
  *
  * ```js
  * {
@@ -102,7 +102,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
  *      Playwright : {
  *        url: "http://localhost",
  *        restart: false,
- *        waitForNavigation: [ "domcontentloaded", "networkidle0" ],
+ *        waitForNavigation: "domcontentloaded",
  *        waitForAction: 500
  *      }
  *    }
@@ -393,6 +393,26 @@ class Playwright extends Helper {
     };
   }
 
+  /**
+  * Use Playwright API inside a test.
+  *
+  * First argument is a description of an action.
+  * Second argument is async function that gets this helper as parameter.
+  *
+  * { [`page`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-page), [`context`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-context) [`browser`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browser) } objects from Playwright API are available.
+  *
+  * ```js
+  * I.usePlaywrightTo('emulate offline mode', async ({ context }) {
+  *   await context.setOffline(true);
+  * });
+  * ```
+  *
+  * @param {string} description used to show in logs.
+  * @param {function} fn async functuion that executed with Playwright helper as argument
+  */
+  usePlaywrightTo(description, fn) {
+    return this._useTo(...arguments);
+  }
 
   /**
    * Set the automatic popup response to Accept.
@@ -533,7 +553,7 @@ class Playwright extends Helper {
     this.browser.on('targetchanged', (target) => {
       this.debugSection('Url', target.url());
     });
-    this.browserContext = await this.browser.newContext(this.options.emulate);
+    this.browserContext = await this.browser.newContext({ acceptDownloads: true, ...this.options.emulate });
 
     const existingPages = await this.browserContext.pages();
 
@@ -993,6 +1013,36 @@ class Playwright extends Helper {
     return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'));
   }
 
+  /**
+   * Handles a file download.Aa file name is required to save the file on disk.
+   * Files are saved to "output" directory.
+   *
+   * Should be used with [FileSystem helper](https://codecept.io/helpers/FileSystem) to check that file were downloaded correctly.
+   *
+   * ```js
+   * I.handleDownloads('downloads/avatar.jpg');
+   * I.click('Download Avatar');
+   * I.amInPath('output/downloads');
+   * I.waitForFile('downloads/avatar.jpg', 5);
+   *
+   * ```
+   *
+   * @param {string} [fileName] set filename for downloaded file
+   */
+  async handleDownloads(fileName = 'downloads') {
+    this.page.waitForEvent('download').then(async (download) => {
+      const filePath = await download.path();
+      const downloadPath = path.join(global.output_dir, fileName || path.basename(filePath));
+      if (!fs.existsSync(path.dirname(downloadPath))) {
+        fs.mkdirSync(path.dirname(downloadPath), '0777');
+      }
+      fs.copyFileSync(filePath, downloadPath);
+      this.debug('Download completed');
+      this.debugSection('Downloaded From', await download.url());
+      this.debugSection('Downloaded To', downloadPath);
+    });
+  }
+
   /**
    * {{> click }}
    *
@@ -1447,13 +1497,24 @@ class Playwright extends Helper {
    *
    */
   async grabTextFrom(locator) {
+    const texts = await this.grabTextFromAll(locator);
+    assertElementExists(texts, locator);
+    this.debugSection('Text', texts[0]);
+    return texts[0];
+  }
+
+
+  /**
+   * {{> grabTextFromAll }}
+   *
+   */
+  async grabTextFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
     const texts = [];
     for (const el of els) {
       texts.push(await (await el.getProperty('innerText')).jsonValue());
     }
-    if (texts.length === 1) return texts[0];
+    this.debug(`Matched ${els.length} elements`);
     return texts;
   }
 
@@ -1461,22 +1522,38 @@ class Playwright extends Helper {
    * {{> grabValueFrom }}
    */
   async grabValueFrom(locator) {
+    const values = await this.grabValueFromAll(locator);
+    assertElementExists(values, locator);
+    this.debugSection('Value', values[0]);
+    return values[0];
+  }
+
+  /**
+   * {{> grabValueFromAll }}
+   */
+  async grabValueFromAll(locator) {
     const els = await findFields.call(this, locator);
-    assertElementExists(els, locator);
-    return els[0].getProperty('value').then(t => t.jsonValue());
+    this.debug(`Matched ${els.length} elements`);
+    return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
   }
 
   /**
    * {{> grabHTMLFrom }}
    */
   async grabHTMLFrom(locator) {
+    const html = await this.grabHTMLFromAll(locator);
+    assertElementExists(html, locator);
+    this.debugSection('HTML', html[0]);
+    return html[0];
+  }
+
+  /**
+   * {{> grabHTMLFromAll }}
+   */
+  async grabHTMLFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
-    const values = await Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
-    if (Array.isArray(values) && values.length === 1) {
-      return values[0];
-    }
-    return values;
+    this.debug(`Matched ${els.length} elements`);
+    return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
   }
 
   /**
@@ -1484,14 +1561,23 @@ class Playwright extends Helper {
    *
    */
   async grabCssPropertyFrom(locator, cssProperty) {
+    const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty);
+    assertElementExists(cssValues, locator);
+    this.debugSection('CSS', cssValues[0]);
+    return cssValues[0];
+  }
+
+  /**
+   * {{> grabCssPropertyFromAll }}
+   *
+   */
+  async grabCssPropertyFromAll(locator, cssProperty) {
     const els = await this._locate(locator);
+    this.debug(`Matched ${els.length} elements`);
     const res = await Promise.all(els.map(el => el.$eval('xpath=.', el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
     const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
 
-    if (res.length > 0) {
-      return cssValues;
-    }
-    return cssValues[0];
+    return cssValues;
   }
 
   /**
@@ -1588,8 +1674,20 @@ class Playwright extends Helper {
    *
    */
   async grabAttributeFrom(locator, attr) {
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    assertElementExists(attrs, locator);
+    this.debugSection('Attribute', attrs[0]);
+    return attrs[0];
+  }
+
+
+  /**
+   * {{> grabAttributeFromAll }}
+   *
+   */
+  async grabAttributeFromAll(locator, attr) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
+    this.debug(`Matched ${els.length} elements`);
     const array = [];
 
     for (let index = 0; index < els.length; index++) {
@@ -1597,7 +1695,7 @@ class Playwright extends Helper {
       array.push(await a.jsonValue());
     }
 
-    return array.length === 1 ? array[0] : array;
+    return array;
   }
 
   /**
@@ -1720,7 +1818,7 @@ class Playwright extends Helper {
   /**
    * {{> waitForClickable }}
    */
-  async waitForClickable(locator, waitTimeout) {
+  async waitForClickable() {
     console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clikable');
     console.log('Remove usage of this function');
   }
diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js
index b67e7717..44077d43 100644
--- a/lib/helper/Protractor.js
+++ b/lib/helper/Protractor.js
@@ -347,6 +347,28 @@ class Protractor extends Helper {
     };
   }
 
+  /**
+   * Use [Protractor](https://www.protractortest.org/#/api) API inside a test.
+   *
+   * First argument is a description of an action.
+   * Second argument is async function that gets this helper as parameter.
+   *
+   * { [`browser`](https://www.protractortest.org/#/api?view=ProtractorBrowser)) } object from Protractor API is available.
+   *
+   * ```js
+   * I.useProtractorTo('change url via in-page navigation', async ({ browser }) {
+   *    await browser.setLocation('api');
+   * });
+   * ```
+   *
+   * @param {string} description used to show in logs.
+   * @param {function} fn async functuion that executed with Protractor helper as argument
+   */
+  useProtractorTo(description, fn) {
+    return this._useTo(...arguments);
+  }
+
+
   /**
    * Switch to non-Angular mode,
    * start using WebDriver instead of Protractor in this session
@@ -687,72 +709,128 @@ class Protractor extends Helper {
   }
 
   /**
-   * {{> grabTextFrom }}
+   * {{> grabTextFromAll }}
    */
-  async grabTextFrom(locator) {
+  async grabTextFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els);
     const texts = [];
     for (const el of els) {
       texts.push(await el.getText());
     }
-    if (texts.length === 1) return texts[0];
     return texts;
   }
 
   /**
-   * {{> grabHTMLFrom }}
+   * {{> grabTextFrom }}
    */
-  async grabHTMLFrom(locator) {
+  async grabTextFrom(locator) {
+    const texts = await this.grabTextFromAll(locator);
+    assertElementExists(texts);
+    if (texts.length > 1) {
+      this.debugSection('GrabText', `Using first element out of ${texts.length}`);
+    }
+
+    return texts[0];
+  }
+
+  /**
+   * {{> grabHTMLFromAll }}
+   */
+  async grabHTMLFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els);
 
     const html = await Promise.all(els.map((el) => {
       return this.browser.executeScript('return arguments[0].innerHTML;', el);
     }));
 
-    if (html.length === 1) {
-      return html[0];
-    }
     return html;
   }
 
+  /**
+   * {{> grabHTMLFrom }}
+   */
+  async grabHTMLFrom(locator) {
+    const html = await this.grabHTMLFromAll(locator);
+    assertElementExists(html);
+    if (html.length > 1) {
+      this.debugSection('GrabHTMl', `Using first element out of ${html.length}`);
+    }
+
+    return html[0];
+  }
+
+  /**
+   * {{> grabValueFromAll }}
+   */
+  async grabValueFromAll(locator) {
+    const els = await findFields(this.browser, locator);
+    const values = await Promise.all(els.map(el => el.getAttribute('value')));
+
+    return values;
+  }
+
   /**
    * {{> grabValueFrom }}
    */
   async grabValueFrom(locator) {
-    const els = await findFields(this.browser, locator);
-    assertElementExists(els, locator, 'Field');
-    return els[0].getAttribute('value');
+    const values = await this.grabValueFromAll(locator);
+    assertElementExists(values, locator, 'Field');
+    if (values.length > 1) {
+      this.debugSection('GrabValue', `Using first element out of ${values.length}`);
+    }
+
+    return values[0];
   }
 
   /**
-   * {{> grabCssPropertyFrom }}
+   * {{> grabCssPropertyFromAll }}
    */
-  async grabCssPropertyFrom(locator, cssProperty) {
+  async grabCssPropertyFromAll(locator, cssProperty) {
     const els = await this._locate(locator, true);
-    assertElementExists(els, locator);
     const values = await Promise.all(els.map(el => el.getCssValue(cssProperty)));
 
-    if (Array.isArray(values) && values.length === 1) {
-      return values[0];
-    }
     return values;
   }
 
   /**
-   * {{> grabAttributeFrom }}
+   * {{> grabCssPropertyFrom }}
    */
-  async grabAttributeFrom(locator, attr) {
+  async grabCssPropertyFrom(locator, cssProperty) {
+    const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty);
+    assertElementExists(cssValues, locator);
+
+    if (cssValues.length > 1) {
+      this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`);
+    }
+
+    return cssValues[0];
+  }
+
+  /**
+   * {{> grabAttributeFromAll }}
+   */
+  async grabAttributeFromAll(locator, attr) {
     const els = await this._locate(locator);
-    assertElementExists(els);
     const array = [];
 
     for (let index = 0; index < els.length; index++) {
       const el = els[index];
       array.push(await el.getAttribute(attr));
     }
-    return array.length === 1 ? array[0] : array;
+    return array;
+  }
+
+  /**
+   * {{> grabAttributeFrom }}
+   */
+  async grabAttributeFrom(locator, attr) {
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    assertElementExists(attrs, locator);
+    if (attrs.length > 1) {
+      this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
+    }
+
+    return attrs[0];
   }
 
   /**
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index bfcb6410..23c47bac 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -232,7 +232,7 @@ class Puppeteer extends Helper {
     try {
       requireg('puppeteer');
     } catch (e) {
-      return ['puppeteer@^1.6.0'];
+      return ['puppeteer@^3.0.1'];
     }
   }
 
@@ -304,12 +304,12 @@ class Puppeteer extends Helper {
 
   _session() {
     return {
-      start: async (name = '', config) => {
+      start: async (name = '') => {
         this.debugSection('Incognito Tab', 'opened');
         this.activeSessionName = name;
 
         const bc = await this.browser.createIncognitoBrowserContext();
-        const page = await bc.newPage();
+        await bc.newPage();
 
         // Create a new page inside context.
         return bc;
@@ -339,6 +339,26 @@ class Puppeteer extends Helper {
     };
   }
 
+  /**
+  * Use Puppeteer API inside a test.
+  *
+  * First argument is a description of an action.
+  * Second argument is async function that gets this helper as parameter.
+  *
+  * { [`page`](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-page), [`browser`](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-browser) } from Puppeteer API are available.
+  *
+  * ```js
+  * I.usePuppeteerTo('emulate offline mode', async ({ page }) {
+  *   await page.setOfflineMode(true);
+  * });
+  * ```
+  *
+  * @param {string} description used to show in logs.
+  * @param {function} fn async function that is executed with Puppeteer as argument
+  */
+  usePuppeteerTo(description, fn) {
+    return this._useTo(...arguments);
+  }
 
   /**
    * Set the automatic popup response to Accept.
@@ -943,6 +963,36 @@ class Puppeteer extends Helper {
     return proceedClick.call(this, locator, context);
   }
 
+  /**
+   * {{> forceClick }}
+   *
+   * {{ react }}
+   */
+  async forceClick(locator, context = null) {
+    let matcher = await this.context;
+    if (context) {
+      const els = await this._locate(context);
+      assertElementExists(els, context);
+      matcher = els[0];
+    }
+
+    const els = await findClickable.call(this, matcher, locator);
+    if (context) {
+      assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`);
+    } else {
+      assertElementExists(els, locator, 'Clickable element');
+    }
+    const elem = els[0];
+    return this.executeScript((el) => {
+      if (document.activeElement instanceof HTMLElement) {
+        document.activeElement.blur();
+      }
+      const event = document.createEvent('MouseEvent');
+      event.initEvent('click', true, true);
+      return el.dispatchEvent(event);
+    }, elem);
+  }
+
   /**
    * {{> clickLink }}
    *
@@ -1469,56 +1519,104 @@ class Puppeteer extends Helper {
     return this.page.evaluate.apply(this.page, args);
   }
 
-
   /**
-   * {{> grabTextFrom }}
+   * {{> grabTextFromAll }}
    * {{ react }}
    */
-  async grabTextFrom(locator) {
+  async grabTextFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
     const texts = [];
     for (const el of els) {
       texts.push(await (await el.getProperty('innerText')).jsonValue());
     }
-    if (texts.length === 1) return texts[0];
     return texts;
   }
 
+  /**
+   * {{> grabTextFrom }}
+   * {{ react }}
+   */
+  async grabTextFrom(locator) {
+    const texts = await this.grabTextFromAll(locator);
+    assertElementExists(texts, locator);
+    if (texts.length > 1) {
+      this.debugSection('GrabText', `Using first element out of ${texts.length}`);
+    }
+
+    return texts[0];
+  }
+
+  /**
+   * {{> grabValueFromAll }}
+   */
+  async grabValueFromAll(locator) {
+    const els = await findFields.call(this, locator);
+    const values = [];
+    for (const el of els) {
+      values.push(await (await el.getProperty('value')).jsonValue());
+    }
+    return values;
+  }
+
   /**
    * {{> grabValueFrom }}
    */
   async grabValueFrom(locator) {
-    const els = await findFields.call(this, locator);
-    assertElementExists(els, locator);
-    return els[0].getProperty('value').then(t => t.jsonValue());
+    const values = await this.grabValueFromAll(locator);
+    assertElementExists(values, locator);
+    if (values.length > 1) {
+      this.debugSection('GrabValue', `Using first element out of ${values.length}`);
+    }
+
+    return values[0];
   }
 
   /**
-   * {{> grabHTMLFrom }}
+   * {{> grabHTMLFromAll }}
    */
-  async grabHTMLFrom(locator) {
+  async grabHTMLFromAll(locator) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
     const values = await Promise.all(els.map(el => el.executionContext().evaluate(element => element.innerHTML, el)));
-    if (Array.isArray(values) && values.length === 1) {
-      return values[0];
-    }
     return values;
   }
 
   /**
-   * {{> grabCssPropertyFrom }}
+   * {{> grabHTMLFrom }}
+   */
+  async grabHTMLFrom(locator) {
+    const html = await this.grabHTMLFromAll(locator);
+    assertElementExists(html, locator);
+    if (html.length > 1) {
+      this.debugSection('GrabHTML', `Using first element out of ${html.length}`);
+    }
+
+    return html[0];
+  }
+
+  /**
+   * {{> grabCssPropertyFromAll }}
    * {{ react }}
    */
-  async grabCssPropertyFrom(locator, cssProperty) {
+  async grabCssPropertyFromAll(locator, cssProperty) {
     const els = await this._locate(locator);
     const res = await Promise.all(els.map(el => el.executionContext().evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
     const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
 
-    if (res.length > 0) {
-      return cssValues;
+    return cssValues;
+  }
+
+  /**
+   * {{> grabCssPropertyFrom }}
+   * {{ react }}
+   */
+  async grabCssPropertyFrom(locator, cssProperty) {
+    const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty);
+    assertElementExists(cssValues, locator);
+
+    if (cssValues.length > 1) {
+      this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`);
     }
+
     return cssValues[0];
   }
 
@@ -1615,20 +1713,31 @@ class Puppeteer extends Helper {
   }
 
   /**
-   * {{> grabAttributeFrom }}
+   * {{> grabAttributeFromAll }}
    * {{ react }}
    */
-  async grabAttributeFrom(locator, attr) {
+  async grabAttributeFromAll(locator, attr) {
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
     const array = [];
-
     for (let index = 0; index < els.length; index++) {
       const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr);
       array.push(await a.jsonValue());
     }
+    return array;
+  }
+
+  /**
+   * {{> grabAttributeFrom }}
+   * {{ react }}
+   */
+  async grabAttributeFrom(locator, attr) {
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    assertElementExists(attrs, locator);
+    if (attrs.length > 1) {
+      this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
+    }
 
-    return array.length === 1 ? array[0] : array;
+    return attrs[0];
   }
 
   /**
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 482b8569..027d5be0 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -511,6 +511,11 @@ class WebDriver extends Helper {
       if (this.options.multiremote) {
         this.browser = await webdriverio.multiremote(this.options.multiremote);
       } else {
+        // remove non w3c capabilities
+        delete this.options.capabilities.protocol;
+        delete this.options.capabilities.hostname;
+        delete this.options.capabilities.port;
+        delete this.options.capabilities.path;
         this.browser = await webdriverio.remote(this.options);
       }
     } catch (err) {
@@ -608,6 +613,28 @@ class WebDriver extends Helper {
     };
   }
 
+  /**
+  * Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test.
+  *
+  * First argument is a description of an action.
+  * Second argument is async function that gets this helper as parameter.
+  *
+  * { [`browser`](https://webdriver.io/docs/api.html)) } object from WebDriver API is available.
+  *
+  * ```js
+  * I.useWebDriverTo('open multiple windows', async ({ browser }) {
+  *    // create new window
+  *    await browser.newWindow('https://webdriver.io');
+  * });
+  * ```
+  *
+  * @param {string} description used to show in logs.
+  * @param {function} fn async functuion that executed with WebDriver helper as argument
+  */
+  useWebDriverTo(description, fn) {
+    return this._useTo(...arguments);
+  }
+
   async _failed() {
     if (this.context !== this.root) await this._withinEnd();
   }
@@ -760,13 +787,15 @@ class WebDriver extends Helper {
    * Find a clickable element by providing human readable text:
    *
    * ```js
-   * this.helpers['WebDriver']._locateClickable('Next page').then // ...
+   * const els = await this.helpers.WebDriver._locateClickable('Next page');
+   * const els = await this.helpers.WebDriver._locateClickable('Next page', '.pages');
    * ```
    *
    * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
    */
-  async _locateClickable(locator) {
-    return findClickable.call(this, locator, this.$$.bind(this)).then(res => res);
+  async _locateClickable(locator, context) {
+    const locateFn = prepareLocateFn.call(this, context);
+    return findClickable.call(this, locator, locateFn);
   }
 
   /**
@@ -837,6 +866,32 @@ class WebDriver extends Helper {
     return this.browser[clickMethod](getElementId(elem));
   }
 
+  /**
+   * {{> forceClick }}
+   *
+   * {{ react }}
+   */
+  async forceClick(locator, context = null) {
+    const locateFn = prepareLocateFn.call(this, context);
+
+    const res = await findClickable.call(this, locator, locateFn);
+    if (context) {
+      assertElementExists(res, locator, 'Clickable element', `was not found inside element ${new Locator(context)}`);
+    } else {
+      assertElementExists(res, locator, 'Clickable element');
+    }
+    const elem = usingFirstElement(res);
+
+    return this.executeScript((el) => {
+      if (document.activeElement instanceof HTMLElement) {
+        document.activeElement.blur();
+      }
+      const event = document.createEvent('MouseEvent');
+      event.initEvent('click', true, true);
+      return el.dispatchEvent(event);
+    }, elem);
+  }
+
   /**
    * {{> doubleClick }}
    *
@@ -1019,21 +1074,40 @@ class WebDriver extends Helper {
     return this.browser[clickMethod](elementId);
   }
 
+  /**
+   * {{> grabTextFromAll }}
+   *
+   */
+  async grabTextFromAll(locator) {
+    const res = await this._locate(locator, true);
+    const val = await forEachAsync(res, el => this.browser.getElementText(getElementId(el)));
+    this.debugSection('GrabText', String(val));
+    return val;
+  }
+
   /**
    * {{> grabTextFrom }}
    *
    */
   async grabTextFrom(locator) {
-    const res = await this._locate(locator, true);
-    assertElementExists(res, locator);
-    let val;
-    if (res.length > 1) {
-      val = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)));
-    } else {
-      val = await this.browser.getElementText(getElementId(res[0]));
+    const texts = await this.grabTextFromAll(locator);
+    assertElementExists(texts, locator);
+    if (texts.length > 1) {
+      this.debugSection('GrabText', `Using first element out of ${texts.length}`);
     }
-    this.debugSection('Grab', val);
-    return val;
+
+    return texts[0];
+  }
+
+  /**
+   * {{> grabHTMLFromAll }}
+   *
+   */
+  async grabHTMLFromAll(locator) {
+    const elems = await this._locate(locator, true);
+    const html = await forEachAsync(elems, elem => elem.getHTML(false));
+    this.debugSection('GrabHTML', String(html));
+    return html;
   }
 
   /**
@@ -1041,14 +1115,25 @@ class WebDriver extends Helper {
    *
    */
   async grabHTMLFrom(locator) {
-    const elems = await this._locate(locator, true);
-    assertElementExists(elems, locator);
-    const values = await Promise.all(elems.map(elem => elem.getHTML(false)));
-    this.debugSection('Grab', values);
-    if (Array.isArray(values) && values.length === 1) {
-      return values[0];
+    const html = await this.grabHTMLFromAll(locator);
+    assertElementExists(html);
+    if (html.length > 1) {
+      this.debugSection('GrabHTML', `Using first element out of ${html.length}`);
     }
-    return values;
+
+    return html[0];
+  }
+
+  /**
+   * {{> grabValueFromAll }}
+   *
+   */
+  async grabValueFromAll(locator) {
+    const res = await this._locate(locator, true);
+    const val = await forEachAsync(res, el => el.getValue());
+    this.debugSection('GrabValue', String(val));
+
+    return val;
   }
 
   /**
@@ -1056,19 +1141,48 @@ class WebDriver extends Helper {
    *
    */
   async grabValueFrom(locator) {
-    const res = await this._locate(locator, true);
-    assertElementExists(res, locator);
+    const values = await this.grabValueFromAll(locator);
+    assertElementExists(values, locator);
+    if (values.length > 1) {
+      this.debugSection('GrabValue', `Using first element out of ${values.length}`);
+    }
 
-    return forEachAsync(res, async el => el.getValue());
+    return values[0];
+  }
+
+  /**
+   * {{> grabCssPropertyFromAll }}
+   */
+  async grabCssPropertyFromAll(locator, cssProperty) {
+    const res = await this._locate(locator, true);
+    const val = await forEachAsync(res, async el => this.browser.getElementCSSValue(getElementId(el), cssProperty));
+    this.debugSection('Grab', String(val));
+    return val;
   }
 
   /**
    * {{> grabCssPropertyFrom }}
    */
   async grabCssPropertyFrom(locator, cssProperty) {
+    const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty);
+    assertElementExists(cssValues, locator);
+
+    if (cssValues.length > 1) {
+      this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`);
+    }
+
+    return cssValues[0];
+  }
+
+  /**
+   * {{> grabAttributeFromAll }}
+   * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
+   */
+  async grabAttributeFromAll(locator, attr) {
     const res = await this._locate(locator, true);
-    assertElementExists(res, locator);
-    return forEachAsync(res, async el => this.browser.getElementCSSValue(getElementId(el), cssProperty));
+    const val = await forEachAsync(res, async el => el.getAttribute(attr));
+    this.debugSection('GrabAttribute', String(val));
+    return val;
   }
 
   /**
@@ -1076,9 +1190,12 @@ class WebDriver extends Helper {
    * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
    */
   async grabAttributeFrom(locator, attr) {
-    const res = await this._locate(locator, true);
-    assertElementExists(res, locator);
-    return forEachAsync(res, async el => el.getAttribute(attr));
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    assertElementExists(attrs, locator);
+    if (attrs.length > 1) {
+      this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
+    }
+    return attrs[0];
   }
 
   /**
@@ -1854,6 +1971,19 @@ class WebDriver extends Helper {
    */
   async waitForEnabled(locator, sec = null) {
     const aSec = sec || this.options.waitForTimeout;
+    if (isWebDriver5()) {
+      return this.browser.waitUntil(async () => {
+        const res = await this.$$(withStrictLocator(locator));
+        if (!res || res.length === 0) {
+          return false;
+        }
+        const selected = await forEachAsync(res, async el => this.browser.isElementEnabled(getElementId(el)));
+        if (Array.isArray(selected)) {
+          return selected.filter(val => val === true).length > 0;
+        }
+        return selected;
+      }, aSec * 1000, `element (${new Locator(locator)}) still not enabled after ${aSec} sec`);
+    }
     return this.browser.waitUntil(async () => {
       const res = await this.$$(withStrictLocator(locator));
       if (!res || res.length === 0) {
@@ -1864,7 +1994,10 @@ class WebDriver extends Helper {
         return selected.filter(val => val === true).length > 0;
       }
       return selected;
-    }, aSec * 1000, `element (${new Locator(locator)}) still not enabled after ${aSec} sec`);
+    }, {
+      timeout: aSec * 1000,
+      timeoutMsg: `element (${new Locator(locator)}) still not enabled after ${aSec} sec`,
+    });
   }
 
   /**
diff --git a/lib/helper/extras/PlaywrightPropEngine.js b/lib/helper/extras/PlaywrightPropEngine.js
index 8da86260..c74f17f1 100644
--- a/lib/helper/extras/PlaywrightPropEngine.js
+++ b/lib/helper/extras/PlaywrightPropEngine.js
@@ -2,6 +2,7 @@ module.exports.createValueEngine = () => {
   return {
     // Creates a selector that matches given target when queried at the root.
     // Can return undefined if unable to create one.
+    // eslint-disable-next-line no-unused-vars
     create(root, target) {
       return null;
     },
@@ -28,6 +29,7 @@ module.exports.createDisabledEngine = () => {
   return {
     // Creates a selector that matches given target when queried at the root.
     // Can return undefined if unable to create one.
+    // eslint-disable-next-line no-unused-vars
     create(root, target) {
       return null;
     },
diff --git a/lib/parser.js b/lib/parser.js
index e9b93ae7..cbcfa3b9 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -1,15 +1,24 @@
 function _interopDefault(ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex.default : ex; }
-const parser = _interopDefault(require('parse-function'))({ ecmaVersion: 2017 });
+const acorn = require('acorn');
+const parser = _interopDefault(require('parse-function'))({ parse: acorn.parse, ecmaVersion: 11, plugins: ['objectRestSpread'] });
+const { error } = require('./output');
+
+
+parser.use(destructuredArgs);
 
 module.exports.getParamsToString = function (fn) {
-  return getParams(fn).join(', ');
+  const newFn = fn.toString().replace(/^async/, 'async function');
+  return getParams(newFn).join(', ');
 };
 
 function getParams(fn) {
   if (fn.isSinonProxy) return [];
-  const newFn = fn.toString().replace(/^async/, 'async function');
   try {
-    const reflected = parser.parse(newFn);
+    const reflected = parser.parse(fn);
+    if (reflected.args.length > 1 || reflected.args[0] === 'I') {
+      error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up');
+    }
+    if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs];
     const params = reflected.args.map((p) => {
       const def = reflected.defaults[p];
       if (def) {
@@ -19,7 +28,48 @@ function getParams(fn) {
     });
     return params;
   } catch (err) {
-    console.log(`Error in ${newFn.toString()}`);
-    console.error(err);
+    console.log(`Error in ${fn.toString()}`);
+    error(err);
   }
 }
+
+module.exports.getParams = getParams;
+
+function destructuredArgs() {
+  return (node, result) => {
+    result.destructuredArgs = result.destructuredArgs || [];
+
+    if (node.type === 'ObjectExpression' && node.properties.length > 0) {
+      node.properties.forEach((prop) => {
+        if (prop.value && prop.value.params.length > 0) {
+          result.destructuredArgs = parseDestructuredArgs(prop.value);
+        }
+      });
+
+      return result;
+    }
+
+    if (!Array.isArray(node.params)) return result;
+    result.destructuredArgs = parseDestructuredArgs(node);
+
+    return result;
+  };
+}
+
+function parseDestructuredArgs(node) {
+  const destructuredArgs = [];
+  node.params.forEach((param) => {
+    if (
+      param.type === 'ObjectPattern'
+        && param.properties
+        && param.properties.length > 0
+    ) {
+      param.properties.forEach((prop) => {
+        const { name } = prop.value;
+        destructuredArgs.push(name);
+      });
+    }
+  });
+
+  return destructuredArgs;
+}
diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js
index 70a3966a..798e3be0 100644
--- a/lib/plugin/allure.js
+++ b/lib/plugin/allure.js
@@ -72,6 +72,7 @@ const defaultConfig = {
  *
  */
 module.exports = (config) => {
+  defaultConfig.outputDir = global.output_dir;
   config = Object.assign(defaultConfig, config);
 
   const plugin = {};
@@ -107,7 +108,7 @@ module.exports = (config) => {
     }
   };
 
-  plugin.createStep = (name, stepFunc = () => {}) => {
+  plugin.createStep = (name, stepFunc = () => { }) => {
     let result;
     let status = 'passed';
     reporter.startStep(name);
@@ -118,8 +119,8 @@ module.exports = (config) => {
       throw error;
     } finally {
       if (!!result
-          && (typeof result === 'object' || typeof result === 'function')
-          && typeof result.then === 'function'
+        && (typeof result === 'object' || typeof result === 'function')
+        && typeof result.then === 'function'
       ) {
         result.then(() => reporter.endStep('passed'), () => reporter.endStep('broken'));
       } else {
@@ -233,6 +234,18 @@ module.exports = (config) => {
     reporter.endCase('passed');
   });
 
+  event.dispatcher.on(event.test.skipped, (test) => {
+    let loaded = true;
+    if (test.opts.skipInfo.isFastSkipped) {
+      loaded = false;
+      reporter.startSuite(test.parent.fullTitle());
+    }
+    reporter.pendingCase(test.title, null, test.opts.skipInfo);
+    if (!loaded) {
+      reporter.endSuite();
+    }
+  });
+
   event.dispatcher.on(event.step.started, (step) => {
     if (isHookSteps === false) {
       startMetaStep(step.metaStep);
diff --git a/lib/plugin/commentStep.js b/lib/plugin/commentStep.js
index 0077e91d..6a9183f3 100644
--- a/lib/plugin/commentStep.js
+++ b/lib/plugin/commentStep.js
@@ -100,7 +100,7 @@ const defaultGlobalName = '__';
  * ```
  */
 module.exports = function (config) {
-  event.dispatcher.on(event.test.started, (test) => {
+  event.dispatcher.on(event.test.started, () => {
     currentCommentStep = null;
   });
 
diff --git a/lib/plugin/stepByStepReport.js b/lib/plugin/stepByStepReport.js
index 2a7832da..c5f7bc6a 100644
--- a/lib/plugin/stepByStepReport.js
+++ b/lib/plugin/stepByStepReport.js
@@ -131,6 +131,9 @@ module.exports = function (config) {
   async function persistStep(step) {
     if (isStepIgnored(step)) return;
     if (savedStep === step) return; // already saved
+    // Ignore steps from BeforeSuite function
+    if (step.metaStep && step.metaStep.name === 'BeforeSuite') return;
+
     const fileName = `${pad.substring(0, pad.length - stepNum.toString().length) + stepNum.toString()}.png`;
     try {
       stepNum++;
diff --git a/lib/plugin/tryTo.js b/lib/plugin/tryTo.js
new file mode 100644
index 00000000..71391656
--- /dev/null
+++ b/lib/plugin/tryTo.js
@@ -0,0 +1,88 @@
+const recorder = require('../recorder');
+const store = require('../store');
+const { debug } = require('../output');
+
+const defaultConfig = {
+  registerGlobal: true,
+};
+
+/**
+ * Adds global `tryTo` function inside of which all failed steps won't fail a test but will return true/false.
+ *
+ * ```js
+ * const result = await tryTo(() => I.see('Welcome'));
+ *
+ * // if user is on page, result => true
+ * // if user is on page, result => false
+ * ```
+ *
+ * Disables retryFailedStep plugin for steps inside a block;
+ *
+ * Use this plugin if:
+ *
+ * * you need to perform multiple assertions inside a test
+ * * there is A/B testing on a website you test
+ * * there is "Accept Cookie" banner which may surprisingly appear on a page.
+ *
+ * #### Usage
+ *
+ * #### Multiple Conditional Assertions
+ *
+ * ```js
+ * const result1 = await tryTo(() => I.see('Hello, user'));
+ * const result2 = await tryTo(() => I.seeElement('.welcome'));
+ * assert.ok(result1 && result2, 'Assertions were not succesful');
+ * ```
+ *
+ * ##### Optional click
+ *
+ * ```js
+ * I.amOnPage('/');
+ * tryTo(() => I.click('Agree', '.cookies'));
+ * ```
+ *
+ * #### Configuration
+ *
+ * * `registerGlobal` - to register `tryTo` function globally, true by default
+ *
+ * If `registerGlobal` is false you can use tryTo from the plugin:
+ *
+ * ```js
+ * const tryTo = codeceptjs.container.plugins('tryTo');
+ * ```
+ *
+*/
+module.exports = function (config) {
+  config = Object.assign(defaultConfig, config);
+
+  if (config.registerGlobal) {
+    global.tryTo = tryTo;
+  }
+  return tryTo;
+};
+
+function tryTo(callback) {
+  const mode = store.debugMode;
+  let result = false;
+  return recorder.add('tryTo', () => {
+    store.debugMode = true;
+    recorder.session.start('tryTo');
+    callback();
+    recorder.add(() => {
+      result = true;
+      recorder.session.restore('tryTo');
+      return result;
+    });
+    recorder.session.catch((err) => {
+      result = false;
+      const msg = err.inspect ? err.inspect() : err.toString();
+      debug(`Unsuccesful try > ${msg}`);
+      recorder.session.restore('tryTo');
+      return result;
+    });
+    return recorder.add('result', () => {
+      store.debugMode = mode;
+      return result;
+    }, true, false);
+  }, false, false);
+}
diff --git a/lib/plugin/wdio.js b/lib/plugin/wdio.js
index 6490bb58..ed6bccda 100644
--- a/lib/plugin/wdio.js
+++ b/lib/plugin/wdio.js
@@ -1,4 +1,6 @@
 const debug = require('debug')('codeceptjs:plugin:wdio');
+const path = require('path');
+const fs = require('fs');
 
 const container = require('../container');
 const mainConfig = require('../config');
@@ -99,7 +101,14 @@ module.exports = (config) => {
     if (Service) {
       if (Service.launcher && typeof Service.launcher === 'function') {
         const Launcher = Service.launcher;
-        launchers.push(new Launcher(config));
+
+        const version = JSON.parse(fs.readFileSync(path.join(require.resolve('webdriverio'), '/../../', 'package.json')).toString()).version;
+        if (version.indexOf('5') === 0) {
+          launchers.push(new Launcher(config));
+        } else {
+          const options = { logPath: global.output_dir, installArgs: {}, args: {} };
+          launchers.push(new Launcher(options, [config.capabilities], config));
+        }
       }
       if (typeof Service === 'function') {
         services.push(new Service(config, config.capabilities));
diff --git a/lib/reporter/cli.js b/lib/reporter/cli.js
index 47b1170b..934ddd36 100644
--- a/lib/reporter/cli.js
+++ b/lib/reporter/cli.js
@@ -11,11 +11,11 @@ class Cli extends Base {
   constructor(runner, opts) {
     super(runner);
     let level = 0;
+    this.loadedTests = [];
     opts = opts.reporterOptions || opts;
     if (opts.steps) level = 1;
     if (opts.debug) level = 2;
     if (opts.verbose) level = 3;
-
     output.level(level);
     output.print(`CodeceptJS v${require('../codecept').version()}`);
     output.print(`Using test root "${global.codecept_dir}"`);
@@ -37,6 +37,9 @@ class Cli extends Base {
     });
 
     runner.on('fail', (test) => {
+      if (test.ctx.currentTest) {
+        this.loadedTests.push(test.ctx.currentTest.id);
+      }
       if (showSteps && test.steps) {
         return output.scenario.failed(test);
       }
@@ -45,6 +48,7 @@ class Cli extends Base {
     });
 
     runner.on('pending', (test) => {
+      this.loadedTests.push(test.id);
       cursor.CR();
       output.test.skipped(test);
     });
@@ -85,6 +89,29 @@ class Cli extends Base {
       });
     }
 
+    runner.on('suite end', suite => {
+      let skippedCount = 0;
+      const grep = runner._grep;
+      for (const test of suite.tests) {
+        if (!test.state && !this.loadedTests.includes(test.id)) {
+          if (matchTest(grep, test.title)) {
+            if (!test.opts.skipInfo) {
+              test.opts.skipInfo = {};
+            }
+            test.opts.skipInfo.message = 'Skipped due to failure in \'before\' hook';
+            test.opts.skipInfo.isFastSkipped = true;
+            event.emit(event.test.skipped, test);
+            test.state = 'skipped';
+            output.test.skipped(test);
+            skippedCount += 1;
+          }
+        }
+      }
+
+      this.stats.pending += skippedCount;
+      this.stats.tests += skippedCount;
+    });
+
     runner.on('end', this.result.bind(this));
   }
 
@@ -124,6 +151,14 @@ class Cli extends Base {
     output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration));
   }
 }
+
+function matchTest(grep, test) {
+  if (grep) {
+    return grep.test(test);
+  }
+  return true;
+}
+
 module.exports = function (runner, opts) {
   return new Cli(runner, opts);
 };
diff --git a/lib/scenario.js b/lib/scenario.js
index 8007ac49..bf66cf68 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -1,7 +1,8 @@
 const event = require('./event');
 const recorder = require('./recorder');
 const assertThrown = require('./assert/throws');
-const { getParamNames, isAsyncFunction } = require('./utils');
+const { isAsyncFunction } = require('./utils');
+const parser = require('./parser');
 
 const injectHook = function (inject, suite) {
   try {
@@ -53,7 +54,7 @@ module.exports.test = (test) => {
 
     if (isAsyncFunction(testFn)) {
       event.emit(event.test.started, test);
-      testFn.apply(test, getInjectedArguments(testFn, test)).then(() => {
+      testFn.call(test, getInjectedArguments(testFn, test)).then(() => {
         recorder.add('fire test.passed', () => {
           event.emit(event.test.passed, test);
           event.emit(event.test.finished, test);
@@ -75,7 +76,7 @@ module.exports.test = (test) => {
     }
     try {
       event.emit(event.test.started, test);
-      testFn.apply(test, getInjectedArguments(testFn, test));
+      testFn.call(test, getInjectedArguments(testFn, test));
     } catch (err) {
       recorder.throw(err);
     } finally {
@@ -121,7 +122,7 @@ module.exports.injected = function (fn, suite, hookName) {
     this.test.body = fn.toString();
 
     if (isAsyncFunction(fn)) {
-      fn.apply(this, getInjectedArguments(fn)).then(() => {
+      fn.call(this, getInjectedArguments(fn)).then(() => {
         recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
         recorder.add(`finish ${hookName} hook`, () => done());
         recorder.catch();
@@ -137,7 +138,7 @@ module.exports.injected = function (fn, suite, hookName) {
     }
 
     try {
-      fn.apply(this, getInjectedArguments(fn));
+      fn.call(this, getInjectedArguments(fn));
       recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
       recorder.add(`finish ${hookName} hook`, () => done());
       recorder.catch();
@@ -185,21 +186,23 @@ module.exports.suiteTeardown = function (suite) {
 
 const getInjectedArguments = (fn, test) => {
   const container = require('./container');
-  const testArguments = [];
-  const params = getParamNames(fn) || [];
+  const testArgs = {};
+  const params = parser.getParams(fn) || [];
   const objects = container.support();
-  for (const key in params) {
-    const obj = params[key];
-    if (test && test.inject && test.inject[obj]) {
-      testArguments.push(test.inject[obj]);
+  for (const key of params) {
+    testArgs[key] = {};
+    if (test && test.inject && test.inject[key]) {
+      // @FIX: need fix got inject
+      testArgs[key] = test.inject[key];
       continue;
     }
-    if (!objects[obj]) {
-      throw new Error(`Object of type ${obj} is not defined in container`);
+    if (!objects[key]) {
+      throw new Error(`Object of type ${key} is not defined in container`);
     }
-    testArguments.push(container.support(obj));
+    testArgs[key] = container.support(key);
   }
-  return testArguments;
+
+  return testArgs;
 };
 
 module.exports.getInjectedArguments = getInjectedArguments;
diff --git a/lib/workers.js b/lib/workers.js
index 8604cdc7..fe85aa95 100644
--- a/lib/workers.js
+++ b/lib/workers.js
@@ -250,12 +250,10 @@ class Workers extends EventEmitter {
    * @param {Number} numberOfWorkers
    */
   createGroupsOfSuites(numberOfWorkers) {
-    const files = this.codecept.testFiles;
-    const mocha = Container.mocha();
-    mocha.files = files;
-    mocha.loadFiles();
     const groups = populateGroups(numberOfWorkers);
 
+    const mocha = Container.mocha();
+
     mocha.suite.suites.forEach((suite) => {
       const i = indexOfSmallestElement(groups);
       suite.tests.forEach((test) => {
@@ -280,7 +278,7 @@ class Workers extends EventEmitter {
   run() {
     this.stats.start = new Date();
     recorder.startUnlessRunning();
-    recorder.add('bootstrapAll hook', () => new Promise((res, rej) => {
+    recorder.add('bootstrapAll hook', () => new Promise((res) => {
       runHook(this.codecept.config.bootstrapAll, () => {
         res(true);
       }, 'bootstrapAll');
diff --git a/package.json b/package.json
index 144cef55..8adc73b8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "codeceptjs",
-  "version": "2.6.1",
+  "version": "2.6.3",
   "description": "Supercharged End 2 End Testing Framework for NodeJS",
   "keywords": [
     "acceptance",
@@ -75,7 +75,7 @@
     "mocha": "^6.2.2",
     "mocha-junit-reporter": "^1.23.1",
     "ms": "^2.1.2",
-    "parse-function": "^5.5.0",
+    "parse-function": "^5.6.4",
     "promise-retry": "^1.1.1",
     "requireg": "^0.2.2",
     "resq": "^1.7.0",
@@ -84,13 +84,16 @@
   },
   "devDependencies": {
     "@codeceptjs/detox-helper": "^1.0.2",
+    "@codeceptjs/mock-request": "^0.3.0",
     "@pollyjs/adapter-puppeteer": "^2.6.3",
     "@pollyjs/core": "^2.6.3",
     "@types/inquirer": "^0.0.35",
+    "@types/jscodeshift": "^0.6.3",
     "@types/node": "^8.10.54",
     "@wdio/sauce-service": "^5.16.10",
     "@wdio/selenium-standalone-service": "^5.16.10",
     "@wdio/utils": "^5.16.15",
+    "acorn": "^7.1.0",
     "apollo-server-express": "^2.9.16",
     "chai": "^3.4.1",
     "chai-as-promised": "^5.2.0",
@@ -105,15 +108,16 @@
     "faker": "^4.1.0",
     "graphql": "^14.5.8",
     "husky": "^1.2.1",
+    "jscodeshift": "^0.7.0",
     "jsdoc": "^3.6.2",
     "jsdoc-typeof-plugin": "^1.0.0",
     "json-server": "^0.10.1",
     "mocha-parallel-tests": "^2.2.2",
     "nightmare": "^3.0.2",
     "nodemon": "^1.19.4",
-    "playwright": "^0.12.1",
+    "playwright": "^0.14.0",
     "protractor": "^5.4.1",
-    "puppeteer": "^1.20.0",
+    "puppeteer": "^3.0.0",
     "qrcode-terminal": "^0.12.0",
     "rosie": "^1.6.0",
     "runio.js": "^1.0.20",
@@ -136,7 +140,7 @@
   "husky": {
     "hooks": {
       "pre-commit": "npm run lint",
-      "pre-push": "npm run lint && npm run test:unit"
+      "pre-push": "npm run test:unit"
     }
   }
 }
diff --git a/runio.js b/runio.js
index 1d450994..7786c860 100755
--- a/runio.js
+++ b/runio.js
@@ -86,7 +86,7 @@ Our community prepared some valuable recipes for setting up CI systems with Code
   async docsExternalHelpers() {
     // generate documentation for helpers outside of main repo
     console.log('Building @codecepjs/detox helper docs');
-    const helper = 'Detox';
+    let helper = 'Detox';
     replaceInFile(`node_modules/@codeceptjs/detox-helper/${helper}.js`, (cfg) => {
       cfg.replace(/CodeceptJS.LocatorOrString/g, 'string | object');
     });
@@ -100,6 +100,22 @@ Our community prepared some valuable recipes for setting up CI systems with Code
     replaceInFile(`node_modules/@codeceptjs/detox-helper/${helper}.js`, (cfg) => {
       cfg.replace(/string \| object/g, 'CodeceptJS.LocatorOrString');
     });
+
+    console.log('Building @codeceptjs/mock-request');
+    helper = 'MockRequest';
+    replaceInFile('node_modules/@codeceptjs/mock-request/index.js', (cfg) => {
+      cfg.replace(/CodeceptJS.LocatorOrString/g, 'string | object');
+    });
+    await npx(`documentation build node_modules/@codeceptjs/mock-request/index.js -o docs/helpers/${helper}.md -f md --shallow --markdown-toc=false --sort-order=alpha `);
+
+    await writeToFile(`docs/helpers/${helper}.md`, (cfg) => {
+      cfg.line(`---\npermalink: /helpers/${helper}\nsidebar: auto\ntitle: ${helper}\n---\n\n# ${helper}\n\n`);
+      cfg.textFromFile(`docs/helpers/${helper}.md`);
+    });
+
+    replaceInFile('node_modules/@codeceptjs/mock-request/index.js', (cfg) => {
+      cfg.replace(/string \| object/g, 'CodeceptJS.LocatorOrString');
+    });
   },
 
   async docsExternalPlugins() {
@@ -144,7 +160,7 @@ Our community prepared some valuable recipes for setting up CI systems with Code
     // generate documentation for helpers
     const files = fs.readdirSync('lib/helper').filter(f => path.extname(f) === '.js');
 
-    const ignoreList = ['Polly'];
+    const ignoreList = ['Polly', 'MockRequest']; // WebDriverIO won't be documented and should be removed
 
     const partials = fs.readdirSync('docs/webapi').filter(f => path.extname(f) === '.mustache');
     const placeholders = partials.map(file => `{{> ${path.basename(file, '.mustache')} }}`);
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 83040ac8..7e60b025 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -13,7 +13,6 @@ declare namespace CodeceptJS {
   interface I {}
   interface IHook {}
   interface IScenario {}
-  interface CallbackOrder extends Array<any> {}
   interface SupportObject {
     I: CodeceptJS.I;
   }
@@ -44,28 +43,17 @@ declare namespace CodeceptJS {
 
   type LocatorOrString = string | ILocator | Locator;
 
-  interface HookCallback<U extends any[]> { (...args: U): void; }
+  interface HookCallback { (args: SupportObject): void; }
   interface Scenario extends IScenario { only: IScenario, skip: IScenario, todo:  IScenario}
   interface IData { Scenario: IScenario, only: { Scenario: IScenario } }
 
   interface IScenario {
     // Scenario.todo can be called only with a title.
-    <T extends any[] = CallbackOrder>(
-      title: string
-    ): ScenarioConfig;
-    <T extends any[] = CallbackOrder>(
-      title: string,
-      callback: HookCallback<T>
-    ): ScenarioConfig;
-    <T extends any[] = CallbackOrder>(
-      title: string,
-      opts: { [key: string]: any },
-      callback: HookCallback<T>
-    ): ScenarioConfig;
-  }
-  interface IHook {
-    <T extends any[] = CallbackOrder>(callback: HookCallback<T>): void;
+    (title: string): ScenarioConfig;
+    (title: string, callback: HookCallback): ScenarioConfig;
+    (title: string, opts: { [key: string]: any }, callback: HookCallback): ScenarioConfig;
   }
+  interface IHook { (callback: HookCallback): void; }
 
   interface Globals {
     codeceptjs: typeof codeceptjs;


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5818c4c9..83984b50 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,12 @@
 name: Run Unit tests
 
-on: [push]
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - '**'
 
 jobs:
   build:
@@ -9,7 +15,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [10.x, 12.x]
+        node-version: [12.x]
 
     steps:
     - uses: actions/checkout@v1
diff --git a/docs/helpers/TestCafe.md b/docs/helpers/TestCafe.md
index 686fcddb..f229fa77 100644
--- a/docs/helpers/TestCafe.md
+++ b/docs/helpers/TestCafe.md
@@ -901,6 +901,26 @@ I.uncheckOption('agree', '//form');
 -   `field` **([string][4] | [object][5])** checkbox located by label | name | CSS | XPath | strict locator.
 -   `context` **([string][4]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. 
 
+### useTestCafeTo
+
+Use [TestCafe][12] API inside a test.
+
+First argument is a description of an action.
+Second argument is async function that gets this helper as parameter.
+
+{ [`t`][13]) } object from TestCafe API is available.
+
+```js
+I.useTestCafeTo('handle browser dialog', async ({ t }) {
+  await t.setNativeDialogHandler(() => true);
+});
+```
+
+#### Parameters
+
+-   `description` **[string][4]** used to show in logs.
+-   `fn` **[function][6]** async functuion that executed with TestCafe helper as argument
+
 ### wait
 
 Pauses execution for a number of seconds.
@@ -1078,3 +1098,7 @@ Client Functions
 [10]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value
 
 [11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+
+[12]: https://devexpress.github.io/testcafe/documentation/test-api/
+
+[13]: https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller
diff --git a/docs/testcafe.md b/docs/testcafe.md
index 30113bed..d5d26e86 100644
--- a/docs/testcafe.md
+++ b/docs/testcafe.md
@@ -130,6 +130,21 @@ module.exports = {
 
 > [▶ Read more about PageObjects in CodeceptJS](/pageobjects)
 
+
+## Accessing TestCafe API
+
+To get [testController](https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller))) inside a test use [`I.useTestCafeTo`](/helpers/TestCafe/#usetestcafeto) method with a callback.
+To keep test readable provide a description of a callback inside the first parameter.
+
+```js
+I.useTestCafeTo('do some things using native webdriverio api', async ({ t }) => {
+  await t.click() // use testcafe api here
+});
+```
+
+Because all TestCafe commands are asynchronous a callback function must be async.
+
+
 ## Extending
 
 If you want to use TestCafe API inside your tests you can put them into actions of `I` object. To do so you can generate a new helper, access TestCafe helper, and get the test controller.
diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js
index 203c746f..e57a4ca6 100644
--- a/lib/command/workers/runTests.js
+++ b/lib/command/workers/runTests.js
@@ -10,9 +10,10 @@ if (!tty.getWindowSize) {
 const { parentPort, workerData } = require('worker_threads');
 const event = require('../../event');
 const container = require('../../container');
-const { getConfig, getTestRoot } = require('../utils');
+const { getConfig } = require('../utils');
 const { tryOrDefault } = require('../../utils');
 
+// eslint-disable-next-line no-unused-vars
 let stdout = '';
 /* eslint-enable no-unused-vars */
 
@@ -84,6 +85,7 @@ function initializeListeners() {
     return {
       id: test.id,
       workerIndex,
+      retries: test._retries,
       title: test.title,
       status: test.status,
       duration: test.duration || 0,
diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js
index 4171317f..ba4767c6 100644
--- a/lib/helper/TestCafe.js
+++ b/lib/helper/TestCafe.js
@@ -281,6 +281,29 @@ class TestCafe extends Helper {
     if (!this.options.restart && this.isRunning) return this._stopBrowser();
   }
 
+  /**
+   * Use [TestCafe](https://devexpress.github.io/testcafe/documentation/test-api/) API inside a test.
+   *
+   * First argument is a description of an action.
+   * Second argument is async function that gets this helper as parameter.
+   *
+   * { [`t`](https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller)) } object from TestCafe API is available.
+   *
+   * ```js
+   * I.useTestCafeTo('handle browser dialog', async ({ t }) {
+   *   await t.setNativeDialogHandler(() => true);
+   * });
+   * ```
+   *
+   *
+   *
+   * @param {string} description used to show in logs.
+   * @param {function} fn async functuion that executed with TestCafe helper as argument
+   */
+  useTestCafeTo(description, fn) {
+    return this._useTo(...arguments);
+  }
+
   /**
    * Get elements by different locator types, including strict locator
    * Should be used in custom helpers:
@@ -316,7 +339,6 @@ class TestCafe extends Helper {
       .catch(mapError);
   }
 
-
   /**
    * {{> resizeWindow }}
    */
@@ -769,22 +791,46 @@ class TestCafe extends Helper {
     return browserFn();
   }
 
+  /**
+   * {{> grabTextFromAll }}
+   */
+  async grabTextFromAll(locator) {
+    const sel = await findElements.call(this, this.context, locator);
+    const length = await sel.count;
+    const texts = [];
+    for (let i = 0; i < length; i++) {
+      texts.push(await sel.nth(i).innerText);
+    }
+
+    return texts;
+  }
+
   /**
    * {{> grabTextFrom }}
    */
   async grabTextFrom(locator) {
     const sel = await findElements.call(this, this.context, locator);
     assertElementExists(sel);
-    const num = await sel.count;
-    if (num) {
-      const res = [];
-      for (let i = 0; i < num; i++) {
-        res.push(await sel.nth(i).innerText);
-      }
-      return res;
+    const texts = await this.grabTextFromAll(locator);
+    if (texts.length > 1) {
+      this.debugSection('GrabText', `Using first element out of ${texts.length}`);
+    }
+
+    return texts[0];
+  }
+
+  /**
+   * {{> grabAttributeFrom }}
+   */
+  async grabAttributeFromAll(locator, attr) {
+    const sel = await findElements.call(this, this.context, locator);
+    const length = await sel.count;
+    const attrs = [];
+    for (let i = 0; i < length; i++) {
+      attrs.push(await (await sel.nth(i)).getAttribute(attr));
     }
 
-    return sel.nth(0).innerText;
+    return attrs;
   }
 
   /**
@@ -793,7 +839,26 @@ class TestCafe extends Helper {
   async grabAttributeFrom(locator, attr) {
     const sel = await findElements.call(this, this.context, locator);
     assertElementExists(sel);
-    return (await sel.nth(0)).getAttribute(attr);
+    const attrs = await this.grabAttributeFromAll(locator, attr);
+    if (attrs.length > 1) {
+      this.debugSelection('GrabAttribute', `Using first element out of ${attrs.length}`);
+    }
+
+    return attrs[0];
+  }
+
+  /**
+   * {{> grabValueFromAll }}
+   */
+  async grabValueFromAll(locator) {
+    const sel = await findElements.call(this, this.context, locator);
+    const length = await sel.count;
+    const values = [];
+    for (let i = 0; i < length; i++) {
+      values.push(await (await sel.nth(i)).value);
+    }
+
+    return values;
   }
 
   /**
@@ -802,7 +867,12 @@ class TestCafe extends Helper {
   async grabValueFrom(locator) {
     const sel = await findElements.call(this, this.context, locator);
     assertElementExists(sel);
-    return (await sel.nth(0)).value;
+    const values = await this.grabValueFromAll(locator);
+    if (values.length > 1) {
+      this.debugSection('GrabValue', `Using first element out of ${values.length}`);
+    }
+
+    return values[0];
   }
 
   /**
diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js
index b3eb5df8..0d3591ba 100644
--- a/test/acceptance/codecept.Puppeteer.js
+++ b/test/acceptance/codecept.Puppeteer.js
@@ -7,7 +7,7 @@ module.exports.config = {
   helpers: {
     Puppeteer: {
       url: TestHelper.siteUrl(),
-      show: true,
+      show: false,
       chrome: {
         args: [
           '--no-sandbox',
diff --git a/test/acceptance/config_test.js b/test/acceptance/config_test.js
index 48c2d7bc..9869ed46 100644
--- a/test/acceptance/config_test.js
+++ b/test/acceptance/config_test.js
@@ -1,23 +1,23 @@
 Feature('Dynamic Config').config({ url: 'https://google.com' });
 
-Scenario('change config 1 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 1 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.dontSeeInCurrentUrl('github.com');
   I.seeInCurrentUrl('google.com');
 });
 
-Scenario('change config 2 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 2 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.seeInCurrentUrl('github.com');
 }).config({ url: 'https://github.com' });
 
-Scenario('change config 3 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 3 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.dontSeeInCurrentUrl('github.com');
   I.seeInCurrentUrl('google.com');
 });
 
-Scenario('change config 4 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 4 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.seeInCurrentUrl('codecept.io');
 }).config((test) => {
@@ -25,13 +25,13 @@ Scenario('change config 4 @WebDriverIO @Puppeteer @Playwright @Protractor @Night
 });
 
 
-Scenario('change config 5 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 5 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.dontSeeInCurrentUrl('github.com');
   I.seeInCurrentUrl('google.com');
 });
 
-Scenario('change config 6 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('change config 6 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/');
   I.seeInCurrentUrl('github.com');
 }).config(async () => {
diff --git a/test/acceptance/mock_test.js b/test/acceptance/mock_test.js
index 8c24bd4c..f14e9242 100644
--- a/test/acceptance/mock_test.js
+++ b/test/acceptance/mock_test.js
@@ -1,7 +1,7 @@
 Feature('Mocking');
 
 
-Scenario('change statusCode @Puppeteer @WebDriver', (I) => {
+Scenario('change statusCode @Puppeteer @WebDriver', ({ I }) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', 404);
   I.click('GET POSTS');
@@ -9,7 +9,7 @@ Scenario('change statusCode @Puppeteer @WebDriver', (I) => {
   I.stopMocking();
 });
 
-Scenario('change response data @Puppeteer @WebDriver', (I) => {
+Scenario('change response data @Puppeteer @WebDriver', ({ I }) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
     modified: 'This is modified from mocking',
@@ -19,7 +19,7 @@ Scenario('change response data @Puppeteer @WebDriver', (I) => {
   I.stopMocking();
 });
 
-Scenario('change response data for multiple requests @Puppeteer @WebDriver', (I) => {
+Scenario('change response data for multiple requests @Puppeteer @WebDriver', ({ I }) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest(
     'GET',
@@ -44,7 +44,7 @@ Scenario('change response data for multiple requests @Puppeteer @WebDriver', (I)
 // we should replace it with other service - https://jsonplaceholder.typicode.com not works
 xScenario(
   'should request for original data after mocking stopped @Puppeteer @WebDriver',
-  (I) => {
+  ({ I }) => {
     I.amOnPage('/form/fetch_call');
     I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
       comment: 'CUSTOM _uniqueId_u4805sd23',
diff --git a/test/acceptance/react_test.js b/test/acceptance/react_test.js
index 031a6959..c0aff7ea 100644
--- a/test/acceptance/react_test.js
+++ b/test/acceptance/react_test.js
@@ -1,6 +1,6 @@
 Feature('React Selectors');
 
-Scenario('props @WebDriver @Puppeteer', (I) => {
+Scenario('props @WebDriver @Puppeteer', ({ I }) => {
   I.amOnPage('https://ahfarmer.github.io/calculator/');
   I.click('7');
   I.seeElement({ react: 't', props: { name: '5' } });
@@ -11,7 +11,7 @@ Scenario('props @WebDriver @Puppeteer', (I) => {
   I.seeElement({ react: 't', props: { value: '81' } });
 });
 
-Scenario('component name @Puppeteer', (I) => {
+Scenario('component name @Puppeteer', ({ I }) => {
   I.amOnPage('http://negomi.github.io/react-burger-menu/');
   I.click({ react: 'BurgerIcon' });
   I.waitForVisible('#slide', 10);
diff --git a/test/acceptance/session_test.js b/test/acceptance/session_test.js
index 8973d106..15122d8c 100644
--- a/test/acceptance/session_test.js
+++ b/test/acceptance/session_test.js
@@ -4,7 +4,7 @@ const { event } = codeceptjs;
 
 Feature('Session');
 
-Scenario('simple session @WebDriverIO @Protractor @Puppeteer @Playwright', (I) => {
+Scenario('simple session @WebDriverIO @Protractor @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/info');
   session('john', () => {
     I.amOnPage('https://github.com');
@@ -15,7 +15,7 @@ Scenario('simple session @WebDriverIO @Protractor @Puppeteer @Playwright', (I) =
   I.seeInCurrentUrl('/info');
 });
 
-Scenario('screenshots reflect the current page of current session @Puppeteer @Playwright @WebDriver', async (I) => {
+Scenario('screenshots reflect the current page of current session @Puppeteer @Playwright @WebDriver', async ({ I }) => {
   I.amOnPage('/');
   I.saveScreenshot('session_default_1.png');
 
@@ -45,7 +45,7 @@ Scenario('screenshots reflect the current page of current session @Puppeteer @Pl
   assert.notEqual(default1Digest, john1Digest);
 });
 
-Scenario('Different cookies for different sessions @WebDriverIO @Protractor @Playwright @Puppeteer', async (I) => {
+Scenario('Different cookies for different sessions @WebDriverIO @Protractor @Playwright @Puppeteer', async ({ I }) => {
   const cookiePage = 'https://www.microsoft.com/en-au/';
   const cookieName = 'MUID';
   const cookies = {};
@@ -78,7 +78,7 @@ Scenario('Different cookies for different sessions @WebDriverIO @Protractor @Pla
 });
 
 
-Scenario('should save screenshot for active session @WebDriverIO @Puppeteer @Playwright', async function (I) {
+Scenario('should save screenshot for active session @WebDriverIO @Puppeteer @Playwright', async function ({ I }) {
   I.amOnPage('/form/bug1467');
   I.saveScreenshot('original.png');
   I.amOnPage('/');
@@ -99,7 +99,7 @@ Scenario('should save screenshot for active session @WebDriverIO @Puppeteer @Pla
 });
 
 
-Scenario('should throw exception and close correctly @WebDriverIO @Protractor @Puppeteer @Playwright', (I) => {
+Scenario('should throw exception and close correctly @WebDriverIO @Protractor @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/form/bug1467#session1');
   I.checkOption('Yes');
   session('john', () => {
@@ -111,7 +111,7 @@ Scenario('should throw exception and close correctly @WebDriverIO @Protractor @P
   I.amOnPage('/info');
 }).fails();
 
-Scenario('async/await @WebDriverIO @Protractor', (I) => {
+Scenario('async/await @WebDriverIO @Protractor', ({ I }) => {
   I.amOnPage('/form/bug1467#session1');
   I.checkOption('Yes');
   session('john', async () => {
@@ -122,7 +122,7 @@ Scenario('async/await @WebDriverIO @Protractor', (I) => {
   I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
 });
 
-Scenario('exception on async/await @WebDriverIO @Protractor @Puppeteer @Playwright', (I) => {
+Scenario('exception on async/await @WebDriverIO @Protractor @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/form/bug1467#session1');
   I.checkOption('Yes');
   session('john', async () => {
@@ -133,7 +133,7 @@ Scenario('exception on async/await @WebDriverIO @Protractor @Puppeteer @Playwrig
   I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
 }).throws(/to be checked/);
 
-Scenario('should work with within @WebDriverIO @Protractor @Puppeteer @Playwright', (I) => {
+Scenario('should work with within @WebDriverIO @Protractor @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/form/bug1467');
   session('john', () => {
     I.amOnPage('/form/bug1467');
@@ -154,7 +154,7 @@ Scenario('should work with within @WebDriverIO @Protractor @Puppeteer @Playwrigh
   });
 });
 
-Scenario('change page emulation @Playwright', async (I) => {
+Scenario('change page emulation @Playwright', async ({ I }) => {
   const assert = require('assert');
   I.amOnPage('/');
   session('mobile user', {
@@ -167,7 +167,7 @@ Scenario('change page emulation @Playwright', async (I) => {
 });
 
 
-Scenario('emulate iPhone @Playwright', async (I) => {
+Scenario('emulate iPhone @Playwright', async ({ I }) => {
   const { devices } = require('playwright');
   if (process.env.BROWSER === 'firefox') return;
   const assert = require('assert');
@@ -179,7 +179,7 @@ Scenario('emulate iPhone @Playwright', async (I) => {
   });
 });
 
-xScenario('should use different base URL @Protractor @Puppeteer @Playwright', (I) => { // nah, that's broken
+xScenario('should use different base URL @Protractor @Puppeteer @Playwright', ({ I }) => { // nah, that's broken
   I.amOnPage('/');
   I.see('Welcome to test app');
   session('john', { url: 'https://github.com' }, () => {
@@ -190,7 +190,7 @@ xScenario('should use different base URL @Protractor @Puppeteer @Playwright', (I
   I.see('Welcome to test app');
 });
 
-xScenario('should start firefox', async (I) => { // requires firefox :)
+xScenario('should start firefox', async ({ I }) => { // requires firefox :)
   I.amOnPage('/form/bug1467#session1');
   I.checkOption('Yes');
   session('john', { browser: 'firefox' }, async () => {
@@ -205,7 +205,7 @@ xScenario('should start firefox', async (I) => { // requires firefox :)
   assert(isChrome);
 });
 
-Scenario('should return a value in @WebDriverIO @Protractor @Puppeteer @Playwright', async (I) => {
+Scenario('should return a value in @WebDriverIO @Protractor @Puppeteer @Playwright', async ({ I }) => {
   I.amOnPage('/form/textarea');
   const val = await session('john', () => {
     I.amOnPage('/info');
@@ -217,7 +217,7 @@ Scenario('should return a value in @WebDriverIO @Protractor @Puppeteer @Playwrig
 });
 
 
-Scenario('should return a value @WebDriverIO @Protractor @Puppeteer @Playwright in async', async (I) => {
+Scenario('should return a value @WebDriverIO @Protractor @Puppeteer @Playwright in async', async ({ I }) => {
   I.amOnPage('/form/textarea');
   const val = await session('john', async () => {
     I.amOnPage('/info');
diff --git a/test/acceptance/within_test.js b/test/acceptance/within_test.js
index 272f45d5..6b380aae 100644
--- a/test/acceptance/within_test.js
+++ b/test/acceptance/within_test.js
@@ -1,6 +1,6 @@
 Feature('within', { retries: 3 });
 
-Scenario('within on form @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare ', (I) => {
+Scenario('within on form @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare ', ({ I }) => {
   I.amOnPage('/form/bug1467');
   I.see('TEST TEST');
   within({ css: '[name=form2]' }, () => {
@@ -11,7 +11,7 @@ Scenario('within on form @WebDriverIO @Puppeteer @Playwright @Protractor @Nightm
   I.dontSeeCheckboxIsChecked({ css: 'form[name=form1] input[name=first_test_radio]' });
 });
 
-Scenario('switch iframe manually @WebDriverIO @Puppeteer @Playwright @Protractor', (I) => {
+Scenario('switch iframe manually @WebDriverIO @Puppeteer @Playwright @Protractor', ({ I }) => {
   I.amOnPage('/iframe');
 
   I.switchTo('iframe');
@@ -24,7 +24,7 @@ Scenario('switch iframe manually @WebDriverIO @Puppeteer @Playwright @Protractor
   I.dontSee('Email Address');
 });
 
-Scenario('within on iframe @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('within on iframe @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/iframe');
   within({ frame: 'iframe' }, () => {
     I.fillField('rus', 'Updated');
@@ -35,7 +35,7 @@ Scenario('within on iframe @WebDriverIO @Puppeteer @Playwright @Protractor @Nigh
   I.dontSee('Email Address');
 });
 
-Scenario('within on iframe without iframe navigation @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('within on iframe without iframe navigation @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/iframe');
   within({ frame: 'iframe' }, () => {
     I.fillField('rus', 'Updated');
@@ -45,7 +45,7 @@ Scenario('within on iframe without iframe navigation @WebDriverIO @Puppeteer @Pl
   I.dontSee('Sign in!');
 });
 
-Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -55,7 +55,7 @@ Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO
   I.dontSee('Sign in!');
 });
 
-Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', (I) => {
+Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', ({ I }) => {
   I.amOnPage('/iframe');
   within({ frame: ['[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -66,7 +66,7 @@ Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer @Playwright @P
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer @Playwright @Protractor', (I) => {
+Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer @Playwright @Protractor', ({ I }) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -77,7 +77,7 @@ Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer @Playwright @P
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDriverIO @Puppeteer @Playwright @Protractor', (I) => {
+Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDriverIO @Puppeteer @Playwright @Protractor', ({ I }) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['#wrapperId', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -88,7 +88,7 @@ Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDr
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe depth 2 and mixed class and xpath selector @WebDriverIO @Puppeteer @Playwright @Protractor', (I) => {
+Scenario('within on nested iframe depth 2 and mixed class and xpath selector @WebDriverIO @Puppeteer @Playwright @Protractor', ({ I }) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['.wrapperClass', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -99,14 +99,14 @@ Scenario('within on nested iframe depth 2 and mixed class and xpath selector @We
   I.dontSee('Email Address');
 });
 
-Scenario('should throw exception if element not found @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', async (I) => {
+Scenario('should throw exception if element not found @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', async ({ I }) => {
   I.amOnPage('/form/textarea');
   within('#grab-multiple', () => {
     return I.grabTextFrom('#first-link');
   });
 }).throws(/found/);
 
-Scenario('should return a value @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', async (I) => {
+Scenario('should return a value @WebDriverIO @Puppeteer @Playwright @Protractor @Nightmare', async ({ I }) => {
   I.amOnPage('/info');
   const val = await within('#grab-multiple', () => {
     return I.grabTextFrom('#first-link');
diff --git a/test/data/app/view/info.php b/test/data/app/view/info.php
index bb92633b..dc62c98d 100755
--- a/test/data/app/view/info.php
+++ b/test/data/app/view/info.php
@@ -3,6 +3,11 @@
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 </head>
+<style>
+    .span {
+        height: 15px; 
+    }
+</style>
 <body>
 
 <h1>Information</h1>
@@ -48,5 +53,9 @@
 <a href="/login" target="_blank">New tab</a>
 </div>
 
+<div id="grab-css">
+   <span class="span" style="height:12px">Fisrt <span>
+   <span class="span">Second <span>
+</div>
 </body>
 </html>
diff --git a/test/data/inject-fail-example/first_test.js b/test/data/inject-fail-example/first_test.js
index 05a3e750..a65f765d 100644
--- a/test/data/inject-fail-example/first_test.js
+++ b/test/data/inject-fail-example/first_test.js
@@ -1,5 +1,5 @@
 Feature('asdas');
 
-Scenario('qw', async (I, page) => {
+Scenario('qw', async ({ page }) => {
   await page.type('asda');
 });
diff --git a/test/data/sandbox/base_test_session.js b/test/data/sandbox/base_test_session.js
index ff2ccf18..b424acb6 100644
--- a/test/data/sandbox/base_test_session.js
+++ b/test/data/sandbox/base_test_session.js
@@ -1,6 +1,6 @@
 Feature('Session');
 
-Scenario('basic session @1', (I) => {
+Scenario('basic session @1', ({ I }) => {
   I.do('writing');
   session('davert', () => {
     I.do('reading');
@@ -22,7 +22,7 @@ Scenario('basic session @1', (I) => {
   I.do('waving');
 });
 
-Scenario('session defined not used @2', (I) => {
+Scenario('session defined not used @2', ({ I }) => {
   session('davert');
   I.do('writing');
   I.do('playing');
diff --git a/test/data/sandbox/base_test_within.js b/test/data/sandbox/base_test_within.js
index 5bc32c95..732f9b88 100644
--- a/test/data/sandbox/base_test_within.js
+++ b/test/data/sandbox/base_test_within.js
@@ -1,6 +1,6 @@
 Feature('Within');
 
-Scenario('Check within without generator', (I) => {
+Scenario('Check within without generator', ({ I }) => {
   I.smallPromise();
   within('blabla', () => {
     I.smallPromise();
@@ -24,7 +24,7 @@ Scenario('Check within with generator. Should stop test execution after fail in
   throw new Error('fail');
 });
 
-Scenario('Check within with async/await. Await is first in order', async (I) => {
+Scenario('Check within with async/await. Await is first in order', async ({ I }) => {
   I.smallPromise();
   const test = await I.smallYield();
   console.log(test, 'await');
@@ -35,7 +35,7 @@ Scenario('Check within with async/await. Await is first in order', async (I) =>
   });
 });
 
-Scenario('Check within with async/await. Await is second in order', async (I) => {
+Scenario('Check within with async/await. Await is second in order', async ({ I }) => {
   I.smallPromise();
   const test = await I.smallYield();
   console.log(test, 'await');
diff --git a/test/data/sandbox/browser_test.multiple.js b/test/data/sandbox/browser_test.multiple.js
index c37d6ab1..cc27846d 100644
--- a/test/data/sandbox/browser_test.multiple.js
+++ b/test/data/sandbox/browser_test.multiple.js
@@ -1,5 +1,5 @@
 Feature('print browser');
 
-Scenario('print browser info', (I) => {
+Scenario('print browser info', ({ I }) => {
   I.printBrowser();
 });
diff --git a/test/data/sandbox/codecept.beforetest.failure.json b/test/data/sandbox/codecept.beforetest.failure.json
new file mode 100644
index 00000000..62a3d695
--- /dev/null
+++ b/test/data/sandbox/codecept.beforetest.failure.json
@@ -0,0 +1,12 @@
+{
+  "tests": "./*test_before_failure.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "FileSystem": {}
+  },
+  "include": {},
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
\ No newline at end of file
diff --git a/test/data/sandbox/configs/allure/before_suite_test_failed.conf.js b/test/data/sandbox/configs/allure/before_suite_test_failed.conf.js
index 4fb9dc81..f9925896 100644
--- a/test/data/sandbox/configs/allure/before_suite_test_failed.conf.js
+++ b/test/data/sandbox/configs/allure/before_suite_test_failed.conf.js
@@ -9,6 +9,7 @@ exports.config = {
   plugins: {
     allure: {
       enabled: true,
+      output: './output/failed',
     },
   },
   mocha: {},
diff --git a/test/data/sandbox/configs/allure/before_suite_test_failed.js b/test/data/sandbox/configs/allure/before_suite_test_failed.js
index c019a8e1..0945cf0c 100644
--- a/test/data/sandbox/configs/allure/before_suite_test_failed.js
+++ b/test/data/sandbox/configs/allure/before_suite_test_failed.js
@@ -4,10 +4,10 @@ BeforeSuite(async () => {
   throw new Error('the before suite setup failed');
 });
 
-Scenario('failing setup test 1', async I => {
+Scenario('failing setup test 1', async ({ I }) => {
   I.say('Test was fine.');
 });
 
-Scenario('failing setup test 2', async I => {
+Scenario('failing setup test 2', async ({ I }) => {
   I.say('Test was fine.');
 });
diff --git a/test/data/sandbox/configs/allure/failed_ansi_test.js b/test/data/sandbox/configs/allure/failed_ansi_test.js
index a521116c..66be6f1c 100644
--- a/test/data/sandbox/configs/allure/failed_ansi_test.js
+++ b/test/data/sandbox/configs/allure/failed_ansi_test.js
@@ -1,7 +1,7 @@
 
 Feature('Filesystem').tag('main');
 
-Scenario('check error with ansi symbols', (I) => {
+Scenario('check error with ansi symbols', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   throw new Error('message with ANSI symbols \n \u001b[32mgreen\u001b[39m');
diff --git a/test/data/sandbox/configs/allure/success_test.js b/test/data/sandbox/configs/allure/success_test.js
index 6ec6d34a..d6ed9775 100644
--- a/test/data/sandbox/configs/allure/success_test.js
+++ b/test/data/sandbox/configs/allure/success_test.js
@@ -1,6 +1,6 @@
 Feature('Filesystem').tag('main');
 
-Scenario('check error with ansi symbols', (I) => {
+Scenario('check error with ansi symbols', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
 }).tag('fast').tag('@important');
diff --git a/test/data/sandbox/configs/bootstrap/failed_test.js b/test/data/sandbox/configs/bootstrap/failed_test.js
index e42c260f..503b4891 100644
--- a/test/data/sandbox/configs/bootstrap/failed_test.js
+++ b/test/data/sandbox/configs/bootstrap/failed_test.js
@@ -1,12 +1,12 @@
 Feature('Filesystem').tag('main');
 
-Scenario('check current dir', (I) => {
+Scenario('check current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('unknown.js');
 }).tag('slow').tag('@important');
 
-Scenario('check current dir with wait', (I) => {
+Scenario('check current dir with wait', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.waitForFile('unknown.js', 1);
diff --git a/test/data/sandbox/configs/bootstrap/fs_test.js b/test/data/sandbox/configs/bootstrap/fs_test.js
index 6b716c24..4feb17fa 100644
--- a/test/data/sandbox/configs/bootstrap/fs_test.js
+++ b/test/data/sandbox/configs/bootstrap/fs_test.js
@@ -1,13 +1,13 @@
 Feature('Filesystem').tag('main');
 
-Scenario('see content in file', (I) => {
+Scenario('see content in file', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('fs_test.js');
   I.seeFileContentsEqualReferenceFile(__filename);
 }).tag('slow').tag('@important');
 
-Scenario('wait for file in current dir', (I) => {
+Scenario('wait for file in current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.waitForFile('fs_test.js');
diff --git a/test/data/sandbox/configs/bootstrap/invalid_require_test.js b/test/data/sandbox/configs/bootstrap/invalid_require_test.js
index 131bb1ba..f513394b 100644
--- a/test/data/sandbox/configs/bootstrap/invalid_require_test.js
+++ b/test/data/sandbox/configs/bootstrap/invalid_require_test.js
@@ -2,7 +2,7 @@ require('invalidRequire');
 
 Feature('Filesystem');
 
-Scenario('check current dir', (I) => {
+Scenario('check current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('codecept.json');
diff --git a/test/data/sandbox/configs/commentStep/first_test.js b/test/data/sandbox/configs/commentStep/first_test.js
index 1f3a5467..4d479391 100644
--- a/test/data/sandbox/configs/commentStep/first_test.js
+++ b/test/data/sandbox/configs/commentStep/first_test.js
@@ -10,7 +10,7 @@ const pageObject = {
   },
 };
 
-Scenario('global var', (I) => {
+Scenario('global var', ({ I }) => {
   __`Prepare user base`;
   I.print('other thins');
 
@@ -21,7 +21,7 @@ Scenario('global var', (I) => {
   I.print('see everything works');
 });
 
-Scenario('local vars', (I) => {
+Scenario('local vars', ({ I }) => {
   given`Prepare project`;
   I.print('other thins');
 
@@ -32,7 +32,7 @@ Scenario('local vars', (I) => {
   I.print('see everything works');
 });
 
-Scenario('from page object', (I) => {
+Scenario('from page object', ({ I }) => {
   __`Prepare project`;
   I.print('other thins');
   pageObject.metaPrint('login user');
diff --git a/test/data/sandbox/configs/pageObjects/first_test.js b/test/data/sandbox/configs/pageObjects/first_test.js
index ef4520de..7d0f6cf1 100644
--- a/test/data/sandbox/configs/pageObjects/first_test.js
+++ b/test/data/sandbox/configs/pageObjects/first_test.js
@@ -1,12 +1,12 @@
 Feature('PageObject');
 
-Scenario('@ClassPageObject', async (I, classpage) => {
+Scenario('@ClassPageObject', async ({ classpage }) => {
   await classpage.type('Class Page Type');
   await classpage.purgeDomains();
 });
 
 
-Scenario('@NestedClassPageObject', (I, classnestedpage) => {
+Scenario('@NestedClassPageObject', ({ classnestedpage }) => {
   classnestedpage.type('Nested Class Page Type');
   classnestedpage.purgeDomains();
 });
diff --git a/test/data/sandbox/configs/run-rerun/first_ftest.js b/test/data/sandbox/configs/run-rerun/first_ftest.js
index d385a54f..7b9e899f 100644
--- a/test/data/sandbox/configs/run-rerun/first_ftest.js
+++ b/test/data/sandbox/configs/run-rerun/first_ftest.js
@@ -1,13 +1,13 @@
 /* eslint-disable radix */
 Feature('Run Rerun - Command');
 
-Scenario('@RunRerun - Fail all attempt', (I) => {
+Scenario('@RunRerun - Fail all attempt', ({ I }) => {
   I.printMessage('RunRerun');
   throw new Error('Test Error');
 });
 
 
-Scenario('@RunRerun - fail second test', (I) => {
+Scenario('@RunRerun - fail second test', ({ I }) => {
   I.printMessage('RunRerun');
   process.env.FAIL_ATTEMPT = parseInt(process.env.FAIL_ATTEMPT) + 1;
   console.log(process.env.FAIL_ATTEMPT);
diff --git a/test/data/sandbox/configs/run-rerun/first_test.js b/test/data/sandbox/configs/run-rerun/first_test.js
index 450890a3..973c6216 100644
--- a/test/data/sandbox/configs/run-rerun/first_test.js
+++ b/test/data/sandbox/configs/run-rerun/first_test.js
@@ -1,5 +1,5 @@
 Feature('Run Rerun - Command');
 
-Scenario('@RunRerun', (I) => {
+Scenario('@RunRerun', ({ I }) => {
   I.printMessage('RunRerun');
 });
diff --git a/test/data/sandbox/configs/workers/codecept.workers-negative.conf.js b/test/data/sandbox/configs/workers/codecept.workers-negative.conf.js
new file mode 100644
index 00000000..8788d5b0
--- /dev/null
+++ b/test/data/sandbox/configs/workers/codecept.workers-negative.conf.js
@@ -0,0 +1,11 @@
+exports.config = {
+  tests: '../../workers/negative_results/*.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/custom-worker/base_test.worker.js b/test/data/sandbox/custom-worker/base_test.worker.js
index 9cae4f5b..a89c6a25 100644
--- a/test/data/sandbox/custom-worker/base_test.worker.js
+++ b/test/data/sandbox/custom-worker/base_test.worker.js
@@ -1,11 +1,11 @@
 Feature('Workers');
 
-Scenario('say something', (I) => {
+Scenario('say something', ({ I }) => {
   I.say('Hello Workers');
   share({ fromWorker: true });
 });
 
-Scenario('glob current dir', (I) => {
+Scenario('glob current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('codecept.glob.json');
diff --git a/test/data/sandbox/custom-worker/custom_test.worker.js b/test/data/sandbox/custom-worker/custom_test.worker.js
index 13c38e70..7bafb43a 100644
--- a/test/data/sandbox/custom-worker/custom_test.worker.js
+++ b/test/data/sandbox/custom-worker/custom_test.worker.js
@@ -1,6 +1,6 @@
 Feature('Custom Workers');
 
-Scenario('say custom something', (I) => {
+Scenario('say custom something', ({ I }) => {
   I.say('Hello Workers');
   I.sayCustomMessage();
 });
diff --git a/test/data/sandbox/custom-worker/share_test.worker.js b/test/data/sandbox/custom-worker/share_test.worker.js
index c687ce9b..551928b6 100644
--- a/test/data/sandbox/custom-worker/share_test.worker.js
+++ b/test/data/sandbox/custom-worker/share_test.worker.js
@@ -2,14 +2,14 @@ const assert = require('assert');
 
 Feature('Shared Memory in Workers');
 
-Scenario('Should get the data shared from main process', (I) => {
+Scenario('Should get the data shared from main process', ({ I }) => {
   I.say('Hello Workers');
   const { fromMain } = inject();
   console.log(fromMain);
   assert.equal(fromMain, true);
 });
 
-Scenario('Should get the data shared from other worker', (I) => {
+Scenario('Should get the data shared from other worker', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   const { fromWorker } = inject();
diff --git a/test/data/sandbox/ddt_test.addt.js b/test/data/sandbox/ddt_test.addt.js
index b98aa95f..95295c58 100644
--- a/test/data/sandbox/ddt_test.addt.js
+++ b/test/data/sandbox/ddt_test.addt.js
@@ -1,5 +1,5 @@
 Feature('ADDT');
 
-Data(['1', '2', '3']).only.Scenario('Should log array of strings', (I, current) => {
-  console.log(`Got array item ${current}`);
+Data(['1', '2', '3']).only.Scenario('Should log array of strings', ({ current }) => {
+  console.log(`Got array item ${(current)}`);
 });
diff --git a/test/data/sandbox/ddt_test.ddt.js b/test/data/sandbox/ddt_test.ddt.js
index 5c4086f5..caf3018a 100644
--- a/test/data/sandbox/ddt_test.ddt.js
+++ b/test/data/sandbox/ddt_test.ddt.js
@@ -8,28 +8,28 @@ const accounts2 = new DataTable(['login', 'password']);
 accounts2.add(['andrey', '555555']);
 accounts2.add(['collaborator', '222222']);
 
-Data(accounts1).Scenario('Should log accounts1', (I, current) => {
+Data(accounts1).Scenario('Should log accounts1', ({ current }) => {
   console.log(`Got login ${current.login} and password ${current.password}`);
 });
 
-Data(accounts2).Scenario('Should log accounts2', (I, current) => {
+Data(accounts2).Scenario('Should log accounts2', ({ current }) => {
   console.log(`Got changed login ${current.login} and password ${current.password}`);
 });
 
 Data(function* () {
   yield ['nick', 'pick'];
   yield ['jack', 'sacj'];
-}).Scenario('Should log accounts3', (I, current) => {
+}).Scenario('Should log accounts3', ({ current }) => {
   console.log(`Got changed login ${current[0]}`);
 });
 
 Data(function* () {
   yield { user: 'nick' };
   yield { user: 'pick' };
-}).Scenario('Should log accounts4', (I, current) => {
+}).Scenario('Should log accounts4', ({ current }) => {
   console.log(`Got generator login ${current.user}`);
 });
 
-Data(['1', '2', '3']).Scenario('Should log array of strings', (I, current) => {
+Data(['1', '2', '3']).Scenario('Should log array of strings', ({ current }) => {
   console.log(`Got array item ${current}`);
 });
diff --git a/test/data/sandbox/ddt_test.gddt.js b/test/data/sandbox/ddt_test.gddt.js
index 9322aacd..cd1679a8 100644
--- a/test/data/sandbox/ddt_test.gddt.js
+++ b/test/data/sandbox/ddt_test.gddt.js
@@ -3,6 +3,6 @@ Feature('ADDT');
 Data(function* () {
   yield { user: 'nick' };
   yield { user: 'pick' };
-}).only.Scenario('Should log generator of strings', (I, current) => {
+}).only.Scenario('Should log generator of strings', ({ current }) => {
   console.log(`Got generator login ${current.user}`);
 });
diff --git a/test/data/sandbox/flaky_test.retry.js b/test/data/sandbox/flaky_test.retry.js
index dba7a469..01dbcb31 100644
--- a/test/data/sandbox/flaky_test.retry.js
+++ b/test/data/sandbox/flaky_test.retry.js
@@ -6,7 +6,7 @@ let tries = 0;
 
 Feature('Retry');
 
-Scenario('flaky step @test1', async (I) => {
+Scenario('flaky step @test1', async ({ I }) => {
   tries++;
   await I.retry(3).failWhen(() => {
     tries++;
@@ -15,7 +15,7 @@ Scenario('flaky step @test1', async (I) => {
   assert.equal(tries, 4);
 });
 
-Scenario('flaky step passed globally @test2', (I) => {
+Scenario('flaky step passed globally @test2', ({ I }) => {
   recorder.retry({
     retries: 3,
     when: () => false,
diff --git a/test/data/sandbox/flaky_test.retryFailed.js b/test/data/sandbox/flaky_test.retryFailed.js
index d605d2e9..54433657 100644
--- a/test/data/sandbox/flaky_test.retryFailed.js
+++ b/test/data/sandbox/flaky_test.retryFailed.js
@@ -4,7 +4,7 @@ let tries = 0;
 
 Feature('Retry');
 
-Scenario('auto repeat failing step @test1', async (I) => {
+Scenario('auto repeat failing step @test1', async ({ I }) => {
   tries++;
   await I.failWhen(() => {
     tries++;
@@ -14,7 +14,7 @@ Scenario('auto repeat failing step @test1', async (I) => {
   console.log(`[T] Retries: ${tries}`);
 });
 
-Scenario('no repeat for waiter @test2', async (I) => {
+Scenario('no repeat for waiter @test2', async ({ I }) => {
   await I.waitForFail(() => {
     tries++;
     return tries < 5;
@@ -22,7 +22,7 @@ Scenario('no repeat for waiter @test2', async (I) => {
   assert.equal(tries, 1);
 });
 
-Scenario('no retries if disabled per test @test3', async (I) => {
+Scenario('no retries if disabled per test @test3', async ({ I }) => {
   await I.failWhen(() => {
     tries++;
     return tries < 5;
diff --git a/test/data/sandbox/fs_test.glob.js b/test/data/sandbox/fs_test.glob.js
index bb652540..c222696f 100644
--- a/test/data/sandbox/fs_test.glob.js
+++ b/test/data/sandbox/fs_test.glob.js
@@ -1,6 +1,6 @@
 Feature('Filesystem');
 
-Scenario('glob current dir', (I) => {
+Scenario('glob current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('codecept.glob.json');
diff --git a/test/data/sandbox/fs_test.js b/test/data/sandbox/fs_test.js
index c158a216..07b1c32a 100644
--- a/test/data/sandbox/fs_test.js
+++ b/test/data/sandbox/fs_test.js
@@ -1,6 +1,6 @@
 Feature('Filesystem').tag('main');
 
-Scenario('check current dir', (I) => {
+Scenario('check current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('codecept.json');
diff --git a/test/data/sandbox/fs_test.po.js b/test/data/sandbox/fs_test.po.js
index 5dcc894a..ec5c8dfd 100644
--- a/test/data/sandbox/fs_test.po.js
+++ b/test/data/sandbox/fs_test.po.js
@@ -1,6 +1,6 @@
 Feature('Filesystem');
 
-Scenario('check current dir', (I, MyPage) => {
+Scenario('check current dir', ({ I, MyPage }) => {
   I.openDir('aaa');
   I.seeFile('codecept.json');
   MyPage.hasFile('uu');
diff --git a/test/data/sandbox/fs_test_failed.js b/test/data/sandbox/fs_test_failed.js
index daa6206d..cdf2f1de 100644
--- a/test/data/sandbox/fs_test_failed.js
+++ b/test/data/sandbox/fs_test_failed.js
@@ -1,6 +1,6 @@
 Feature('Not-A-Filesystem');
 
-Scenario('file is not in dir', (I) => {
+Scenario('file is not in dir', ({ I }) => {
   I.amInPath('.');
   I.seeFile('not-a-codecept.json');
 });
diff --git a/test/data/sandbox/grep_test.multiple.js b/test/data/sandbox/grep_test.multiple.js
index 56a0ea99..c2fc36c7 100644
--- a/test/data/sandbox/grep_test.multiple.js
+++ b/test/data/sandbox/grep_test.multiple.js
@@ -1,5 +1,5 @@
 Feature('@grep print browser size');
 
-Scenario('print browser size info', (I) => {
+Scenario('print browser size info', ({ I }) => {
   I.printWindowSize();
 });
diff --git a/test/data/sandbox/test_before_failure.js b/test/data/sandbox/test_before_failure.js
new file mode 100644
index 00000000..31bed34d
--- /dev/null
+++ b/test/data/sandbox/test_before_failure.js
@@ -0,0 +1,27 @@
+const assert = require('assert');
+
+Feature('Before Test');
+
+let val = 0;
+
+Before(() => {
+  assert.equal(val, 0);
+  val++;
+});
+
+
+Scenario('First test will be passed @grep', () => {
+  assert(true);
+});
+
+Scenario('Second test will be Failed @grep', () => {
+  assert(true);
+});
+
+Scenario('Third test will be skipped @grep', () => {
+  assert(true);
+});
+
+Scenario('Fourth test will be skipped', () => {
+  assert(true);
+});
diff --git a/test/data/sandbox/testevents_test.testevents.js b/test/data/sandbox/testevents_test.testevents.js
index 4f589219..971269fb 100644
--- a/test/data/sandbox/testevents_test.testevents.js
+++ b/test/data/sandbox/testevents_test.testevents.js
@@ -16,42 +16,42 @@ AfterSuite(() => {
   console.log('I\'m simple AfterSuite hook');
 });
 
-BeforeSuite(function* (I) {
+BeforeSuite(function* ({ I }) {
   const text = yield I.stringWithHook('BeforeSuite');
   console.log(text);
 });
 
-Before(function* (I) {
+Before(function* ({ I }) {
   const text = yield I.stringWithHook('Before');
   console.log(text);
 });
 
-After(function* (I) {
+After(function* ({ I }) {
   const text = yield I.stringWithHook('After');
   console.log(text);
 });
 
-AfterSuite(function* (I) {
+AfterSuite(function* ({ I }) {
   const text = yield I.stringWithHook('AfterSuite');
   console.log(text);
 });
 
-BeforeSuite(async (I) => {
+BeforeSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('BeforeSuite');
   console.log(text);
 });
 
-Before(async (I) => {
+Before(async ({ I }) => {
   const text = await I.asyncStringWithHook('Before');
   console.log(text);
 });
 
-After(async (I) => {
+After(async ({ I }) => {
   const text = await I.asyncStringWithHook('After');
   console.log(text);
 });
 
-AfterSuite(async (I) => {
+AfterSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('AfterSuite');
   console.log(text);
 });
diff --git a/test/data/sandbox/testhooks_test.testhooks.different.order.js b/test/data/sandbox/testhooks_test.testhooks.different.order.js
index 71f4a0d7..10c8c513 100644
--- a/test/data/sandbox/testhooks_test.testhooks.different.order.js
+++ b/test/data/sandbox/testhooks_test.testhooks.different.order.js
@@ -1,11 +1,11 @@
 Feature('Test hooks');
 
-BeforeSuite(async (I) => {
+BeforeSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('BeforeSuite');
   console.log(text);
 });
 
-Before(async (I) => {
+Before(async ({ I }) => {
   const text = await I.asyncStringWithHook('Before');
   console.log(text);
 });
@@ -14,12 +14,12 @@ Scenario('Simple test 1', () => {
   console.log('Scenario: It\'s first test');
 });
 
-After(async (I) => {
+After(async ({ I }) => {
   const text = await I.asyncStringWithHook('After');
   console.log(text);
 });
 
-AfterSuite(async (I) => {
+AfterSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('AfterSuite');
   console.log(text);
 });
diff --git a/test/data/sandbox/testhooks_test.testhooks.js b/test/data/sandbox/testhooks_test.testhooks.js
index a6022f86..d6bc011f 100644
--- a/test/data/sandbox/testhooks_test.testhooks.js
+++ b/test/data/sandbox/testhooks_test.testhooks.js
@@ -16,42 +16,42 @@ AfterSuite(() => {
   console.log('Test: I\'m simple AfterSuite hook');
 });
 
-BeforeSuite(async (I) => {
+BeforeSuite(async ({ I }) => {
   const text = await I.stringWithHook('BeforeSuite');
   console.log(text);
 });
 
-Before(async (I) => {
+Before(async ({ I }) => {
   const text = await I.stringWithHook('Before');
   console.log(text);
 });
 
-After(async (I) => {
+After(async ({ I }) => {
   const text = await I.stringWithHook('After');
   console.log(text);
 });
 
-AfterSuite(async (I) => {
+AfterSuite(async ({ I }) => {
   const text = await I.stringWithHook('AfterSuite');
   console.log(text);
 });
 
-BeforeSuite(async (I) => {
+BeforeSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('BeforeSuite');
   console.log(text);
 });
 
-Before(async (I) => {
+Before(async ({ I }) => {
   const text = await I.asyncStringWithHook('Before');
   console.log(text);
 });
 
-After(async (I) => {
+After(async ({ I }) => {
   const text = await I.asyncStringWithHook('After');
   console.log(text);
 });
 
-AfterSuite(async (I) => {
+AfterSuite(async ({ I }) => {
   const text = await I.asyncStringWithHook('AfterSuite');
   console.log(text);
 });
diff --git a/test/data/sandbox/tests_test_override.multiple.js b/test/data/sandbox/tests_test_override.multiple.js
index d8cb6c27..cb3271c4 100644
--- a/test/data/sandbox/tests_test_override.multiple.js
+++ b/test/data/sandbox/tests_test_override.multiple.js
@@ -1,5 +1,5 @@
 Feature('print browser size');
 
-Scenario('print browser size info', (I) => {
+Scenario('print browser size info', ({ I }) => {
   I.printWindowSize();
 });
diff --git a/test/data/sandbox/testscenario_test.testscenario.js b/test/data/sandbox/testscenario_test.testscenario.js
index 71f1cae4..43791005 100644
--- a/test/data/sandbox/testscenario_test.testscenario.js
+++ b/test/data/sandbox/testscenario_test.testscenario.js
@@ -4,13 +4,13 @@ Scenario('Simple test', () => {
   console.log('It\'s usual test');
 });
 
-Scenario('Simple async/await test', async (I) => {
+Scenario('Simple async/await test', async ({ I }) => {
   const text = await I.stringWithScenarioType('async/await');
   console.log(text);
 });
 
 // eslint-disable-next-line arrow-parens
-Scenario('Should understand async without brackets', async I => {
+Scenario('Should understand async without brackets', async ({ I }) => {
   const text = await I.stringWithScenarioType('asyncbrackets');
   console.log(text);
 });
diff --git a/test/data/sandbox/workers/base_test.workers.js b/test/data/sandbox/workers/base_test.workers.js
index ea184f92..0b1c6ae5 100644
--- a/test/data/sandbox/workers/base_test.workers.js
+++ b/test/data/sandbox/workers/base_test.workers.js
@@ -1,11 +1,11 @@
 Feature('Workers');
 
-Scenario('say something', (I) => {
+Scenario('say something', ({ I }) => {
   I.say('Hello Workers');
   I.seeThisIsWorker();
 });
 
-Scenario('glob current dir', (I) => {
+Scenario('glob current dir', ({ I }) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeThisIsWorker();
@@ -13,7 +13,7 @@ Scenario('glob current dir', (I) => {
 });
 
 
-Scenario('fail a test', (I) => {
+Scenario('fail a test', ({ I }) => {
   I.amInPath('.');
   I.seeThisIsWorker();
   I.seeFile('notafile');
diff --git a/test/data/sandbox/workers/failing_test.worker.js b/test/data/sandbox/workers/failing_test.worker.js
index 233676c1..3f15495f 100644
--- a/test/data/sandbox/workers/failing_test.worker.js
+++ b/test/data/sandbox/workers/failing_test.worker.js
@@ -4,7 +4,7 @@ Before(() => {
   throw new Error('worker has failed');
 });
 
-Scenario('should not be executed', (I) => {
+Scenario('should not be executed', ({ I }) => {
   I.say('Hello Workers');
   I.seeThisIsWorker();
 });
diff --git a/test/data/sandbox/workers/negative_results/negative1.workers.js b/test/data/sandbox/workers/negative_results/negative1.workers.js
new file mode 100644
index 00000000..bcd0769c
--- /dev/null
+++ b/test/data/sandbox/workers/negative_results/negative1.workers.js
@@ -0,0 +1,17 @@
+Feature('Workers - negative Results1');
+
+Scenario('the same name', () => {
+  throw new Error('The same error');
+});
+
+Scenario('the same name', () => {
+  throw new Error('The same error');
+});
+
+Scenario('the same name', () => {
+  throw new Error('The same error');
+});
+
+Scenario('the another name', () => {
+  console.log('asd');
+});
diff --git a/test/data/sandbox/workers/negative_results/negative2.workers.js b/test/data/sandbox/workers/negative_results/negative2.workers.js
new file mode 100644
index 00000000..4ccea74b
--- /dev/null
+++ b/test/data/sandbox/workers/negative_results/negative2.workers.js
@@ -0,0 +1,14 @@
+Feature('Workers - negative Results2');
+
+Scenario('the same name', () => {
+  throw new Error('The same error');
+});
+
+Scenario('the same name', () => {
+  throw new Error('The same error');
+});
+
+
+Scenario('the another name', () => {
+  console.log('asd');
+});
diff --git a/test/data/sandbox/workers/test_grep.workers.js b/test/data/sandbox/workers/test_grep.workers.js
index 68d5e428..a99d3605 100644
--- a/test/data/sandbox/workers/test_grep.workers.js
+++ b/test/data/sandbox/workers/test_grep.workers.js
@@ -1,11 +1,11 @@
 Feature('@feature_grep in worker');
 
-Scenario('From worker @1_grep print message 1', (I) => {
+Scenario('From worker @1_grep print message 1', ({ I }) => {
   console.log('message 1');
   I.seeThisIsWorker();
 });
 
-Scenario('From worker @2_grep print message 2', (I) => {
+Scenario('From worker @2_grep print message 2', ({ I }) => {
   console.log('message 2');
   I.seeThisIsWorker();
 });
diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js
index 7d127738..7dd7b898 100644
--- a/test/helper/Playwright_test.js
+++ b/test/helper/Playwright_test.js
@@ -482,91 +482,12 @@ describe('Playwright', function () {
       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');
+      await I.seeInField('Name', ';::=++***+++,<<<<-_-.>/?/?');
     });
   });
 
@@ -619,26 +540,6 @@ describe('Playwright', function () {
       .then(() => I.seeInField('#text2', 'London')));
   });
 
-
-  describe('#grabHTMLFrom', () => {
-    it('should grab inner html from an element using xpath query', () => I.amOnPage('/')
-      .then(() => I.grabHTMLFrom('//title'))
-      .then(html => assert.equal(html, 'TestEd Beta 2.0')));
-
-    it('should grab inner html from an element using id query', () => I.amOnPage('/')
-      .then(() => I.grabHTMLFrom('#area1'))
-      .then(html => assert.equal(html.trim(), '<a href="/form/file" qa-id="test" qa-link="test"> Test Link </a>')));
-
-    it('should grab inner html from multiple elements', () => I.amOnPage('/')
-      .then(() => I.grabHTMLFrom('//a'))
-      .then(html => assert.equal(html.length, 5)));
-
-    it('should grab inner html from within an iframe', () => I.amOnPage('/iframe')
-      .then(() => I.switchTo({ frame: 'iframe' }))
-      .then(() => I.grabHTMLFrom('#new-tab'))
-      .then(html => assert.equal(html.trim(), '<a href="/login" target="_blank">New tab</a>')));
-  });
-
   describe('#grabBrowserLogs', () => {
     it('should grab browser logs', () => I.amOnPage('/')
       .then(() => I.executeScript(() => {
@@ -717,6 +618,16 @@ describe('Playwright', function () {
     });
   });
 
+  describe('#usePlaywrightTo', () => {
+    it('should return title', async () => {
+      await I.amOnPage('/');
+      const title = await I.usePlaywrightTo('test', async ({ page }) => {
+        return page.title();
+      });
+      assert.equal('TestEd Beta 2.0', title);
+    });
+  });
+
   describe('#grabElementBoundingRect', () => {
     it('should get the element bounding rectangle', async () => {
       await I.amOnPage('https://www.google.com');
@@ -740,7 +651,7 @@ describe('Playwright', function () {
     });
   });
 
-  xdescribe('#handleDownloads', () => {
+  describe('#handleDownloads', () => {
     before(() => {
       // create download folder;
       global.output_dir = path.join(`${__dirname}/../data/output`);
@@ -752,7 +663,7 @@ describe('Playwright', function () {
 
     it('should dowload file', async () => {
       await I.amOnPage('/form/download');
-      await I.handleDownloads();
+      await I.handleDownloads('downloads/avatar.jpg');
       await I.click('Download file');
       await FS.waitForFile('downloads/avatar.jpg', 5);
     });
diff --git a/test/helper/ProtractorWeb_test.js b/test/helper/ProtractorWeb_test.js
index c9376cbb..ba9b86eb 100644
--- a/test/helper/ProtractorWeb_test.js
+++ b/test/helper/ProtractorWeb_test.js
@@ -275,7 +275,7 @@ describe('Protractor-NonAngular', function () {
       .then(html => assert.equal(html.trim(), '<a href="/form/file" qa-id="test" qa-link="test"> Test Link </a>')));
 
     it('should grab inner html from multiple elements', () => I.amOnPage('/')
-      .then(() => I.grabHTMLFrom('//a'))
+      .then(() => I.grabHTMLFromAll('//a'))
       .then(html => assert.equal(html.length, 5)));
   });
 
diff --git a/test/helper/Protractor_test.js b/test/helper/Protractor_test.js
index 63ffbe33..42f200a1 100644
--- a/test/helper/Protractor_test.js
+++ b/test/helper/Protractor_test.js
@@ -572,4 +572,14 @@ describe('Protractor', function () {
       });
     });
   });
+
+  describe('#useProtractorTo', () => {
+    it('should return title', async () => {
+      await I.amOnPage('/');
+      const title = await I.useProtractorTo('test', async ({ browser }) => {
+        return browser.getTitle();
+      });
+      assert.equal('Event App', title);
+    });
+  });
 });
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 74e7e9c1..39314b0a 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -12,11 +12,14 @@ const webApiTests = require('./webapi');
 const FileSystem = require('../../lib/helper/FileSystem');
 
 let I;
+let browser;
 let page;
 let FS;
 const siteUrl = TestHelper.siteUrl();
 
-describe('Puppeteer - BasicAuth', () => {
+describe('Puppeteer - BasicAuth', function () {
+  this.timeout(10000);
+
   before(() => {
     global.codecept_dir = path.join(__dirname, '/../data');
 
@@ -42,6 +45,7 @@ describe('Puppeteer - BasicAuth', () => {
     });
     return I._before().then(() => {
       page = I.page;
+      browser = I.browser;
     });
   });
 
@@ -701,7 +705,7 @@ describe('Puppeteer', function () {
       .then(html => assert.equal(html.trim(), '<a href="/form/file" qa-id="test" qa-link="test"> Test Link </a>')));
 
     it('should grab inner html from multiple elements', () => I.amOnPage('/')
-      .then(() => I.grabHTMLFrom('//a'))
+      .then(() => I.grabHTMLFromAll('//a'))
       .then(html => assert.equal(html.length, 5)));
 
     it('should grab inner html from within an iframe', () => I.amOnPage('/iframe')
@@ -827,7 +831,7 @@ describe('Puppeteer', function () {
     });
   });
 
-  describe('#handleDownloads', () => {
+  describe('#saveDownload', () => {
     before(() => {
       // create download folder;
       global.output_dir = path.join(`${__dirname}/../data/output`);
@@ -839,8 +843,8 @@ describe('Puppeteer', function () {
 
     it('should dowload file', async () => {
       await I.amOnPage('/form/download');
-      await I.handleDownloads();
       await I.click('Download file');
+      await I.saveDownload();
       await FS.waitForFile('downloads/avatar.jpg', 5);
     });
   });
@@ -936,6 +940,16 @@ describe('Puppeteer', function () {
       });
     });
   });
+
+  describe('#usePuppeteerTo', () => {
+    it('should return title', async () => {
+      await I.amOnPage('/');
+      const title = await I.usePuppeteerTo('test', async ({ page }) => {
+        return page.title();
+      });
+      assert.equal('TestEd Beta 2.0', title);
+    });
+  });
 });
 
 let remoteBrowser;
diff --git a/test/helper/TestCafe_test.js b/test/helper/TestCafe_test.js
index 456dda17..e54e7e54 100644
--- a/test/helper/TestCafe_test.js
+++ b/test/helper/TestCafe_test.js
@@ -1,4 +1,5 @@
 const path = require('path');
+const assert = require('assert');
 
 const TestHelper = require('../support/TestHelper');
 const TestCafe = require('../../lib/helper/TestCafe');
@@ -77,4 +78,14 @@ describe('TestCafe', function () {
   });
 
   webApiTests.tests();
+
+  describe('#useTestCafeTo', () => {
+    it('should return title', async () => {
+      await I.amOnPage('/');
+      const title = await I.useTestCafeTo('test', async ({ t }) => {
+        return t.eval(() => document.title, { boundTestRun: null });
+      });
+      assert.equal('TestEd Beta 2.0', title);
+    });
+  });
 });
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index ec1cc554..b2f520e2 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -1154,6 +1154,16 @@ describe('WebDriver', function () {
       expect(await element.isDisplayedInViewport()).to.be.true;
     });
   });
+
+  describe('#useWebDriverTo', () => {
+    it('should return title', async () => {
+      await wd.amOnPage('/');
+      const title = await wd.useWebDriverTo('test', async ({ browser }) => {
+        return browser.getTitle();
+      });
+      assert.equal('TestEd Beta 2.0', title);
+    });
+  });
 });
 
 describe('WebDriver - Basic Authentication', () => {
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index 968d2cae..d6eeca45 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -236,6 +236,46 @@ module.exports.tests = function () {
     });
   });
 
+  describe('#forceClick', () => {
+    beforeEach(function () {
+      if (isHelper('Protractor')) this.skip();
+      if (isHelper('TestCafe')) this.skip();
+    });
+
+    it('should forceClick by inner text', async () => {
+      if (isHelper('Nightmare')) return;
+      await I.amOnPage('/');
+      await I.forceClick('More info');
+      if (isHelper('Puppeteer')) await I.waitForNavigation();
+      await I.seeInCurrentUrl('/info');
+    });
+
+    it('should forceClick by css', async () => {
+      if (isHelper('Nightmare')) return;
+      await I.amOnPage('/');
+      await I.forceClick('#link');
+      if (isHelper('Puppeteer')) await I.waitForNavigation();
+      await I.seeInCurrentUrl('/info');
+    });
+
+    it('should forceClick by xpath', async () => {
+      if (isHelper('Nightmare')) return;
+      await I.amOnPage('/');
+      await I.forceClick('//a[@id="link"]');
+      if (isHelper('Puppeteer')) await I.waitForNavigation();
+      await I.seeInCurrentUrl('/info');
+    });
+
+    it('should forceClick on context', async () => {
+      if (isHelper('Nightmare')) return;
+      await I.amOnPage('/');
+      await I.forceClick('More info', 'body>p');
+      if (isHelper('Puppeteer')) await I.waitForNavigation();
+      await I.seeInCurrentUrl('/info');
+    });
+  });
+
+
   // Could not get double click to work
   describe('#doubleClick', () => {
     it('it should doubleClick', async () => {
@@ -561,6 +601,67 @@ module.exports.tests = function () {
     });
   });
 
+  describe('#grabTextFromAll, #grabHTMLFromAll, #grabValueFromAll, #grabAttributeFromAll', () => {
+    it('should grab multiple texts from page', async () => {
+      await I.amOnPage('/info');
+      let vals = await I.grabTextFromAll('#grab-multiple a');
+      assert.equal(vals[0], 'First');
+      assert.equal(vals[1], 'Second');
+      assert.equal(vals[2], 'Third');
+
+      await I.amOnPage('/info');
+      vals = await I.grabTextFromAll('#invalid-id a');
+      assert.equal(vals.length, 0);
+    });
+
+    it('should grab multiple html from page', async function () {
+      if (isHelper('TestCafe')) this.skip();
+
+      await I.amOnPage('/info');
+      let vals = await I.grabHTMLFromAll('#grab-multiple a');
+      assert.equal(vals[0], 'First');
+      assert.equal(vals[1], 'Second');
+      assert.equal(vals[2], 'Third');
+
+      await I.amOnPage('/info');
+      vals = await I.grabHTMLFromAll('#invalid-id a');
+      assert.equal(vals.length, 0);
+    });
+
+    it('should grab multiple attribute from element', async () => {
+      await I.amOnPage('/form/empty');
+      const vals = await I.grabAttributeFromAll({
+        css: 'input',
+      }, 'name');
+      assert.equal(vals[0], 'text');
+      assert.equal(vals[1], 'empty_input');
+    });
+
+    it('Should return empty array if no attribute found', async () => {
+      await I.amOnPage('/form/empty');
+      const vals = await I.grabAttributeFromAll({
+        css: 'div',
+      }, 'test');
+      assert.equal(vals.length, 0);
+    });
+
+    it('should grab values if multiple field matches', async () => {
+      await I.amOnPage('/form/hidden');
+      let vals = await I.grabValueFromAll('//form/input');
+      assert.equal(vals[0], 'kill_people');
+      assert.equal(vals[1], 'Submit');
+
+      vals = await I.grabValueFromAll("//form/input[@name='action']");
+      assert.equal(vals[0], 'kill_people');
+    });
+
+    it('Should return empty array if no value found', async () => {
+      await I.amOnPage('/');
+      const vals = await I.grabValueFromAll('//form/input');
+      assert.equal(vals.length, 0);
+    });
+  });
+
   describe('#grabTextFrom, #grabHTMLFrom, #grabValueFrom, #grabAttributeFrom', () => {
     it('should grab text from page', async () => {
       await I.amOnPage('/');
@@ -571,14 +672,6 @@ module.exports.tests = function () {
       assert.equal(val, 'Welcome to test app!');
     });
 
-    it('should grab multiple texts from page', async () => {
-      await I.amOnPage('/info');
-      const vals = await I.grabTextFrom('#grab-multiple a');
-      assert.equal(vals[0], 'First');
-      assert.equal(vals[1], 'Second');
-      assert.equal(vals[2], 'Third');
-    });
-
     it('should grab html from page', async function () {
       if (isHelper('TestCafe')) this.skip();
 
@@ -589,11 +682,6 @@ module.exports.tests = function () {
     <a id="second-link">Second</a>
     <a id="third-link">Third</a>
 `, val);
-
-      const vals = await I.grabHTMLFrom('#grab-multiple a');
-      assert.equal(vals[0], 'First');
-      assert.equal(vals[1], 'Second');
-      assert.equal(vals[2], 'Third');
     });
 
 
@@ -1152,6 +1240,16 @@ module.exports.tests = function () {
       const css = await I.grabCssPropertyFrom('#block', 'user-select');
       assert.equal(css, 'text');
     });
+
+    it('should grab multiple values if more than one matching element found', async () => {
+      if (isHelper('Nightmare')) return;
+      if (isHelper('TestCafe')) return;
+
+      await I.amOnPage('/info');
+      const css = await I.grabCssPropertyFromAll('.span', 'height');
+      assert.equal(css[0], '12px');
+      assert.equal(css[1], '15px');
+    });
   });
 
   describe('#seeAttributesOnElements', () => {
diff --git a/test/runner/before_failure_test.js b/test/runner/before_failure_test.js
new file mode 100644
index 00000000..a2cb94f8
--- /dev/null
+++ b/test/runner/before_failure_test.js
@@ -0,0 +1,37 @@
+const path = require('path');
+const exec = require('child_process').exec;
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox');
+const codecept_run = `${runner} run --config ${codecept_dir}/codecept.beforetest.failure.json `;
+
+describe('Failure in before', () => {
+  it('should skip tests that are skipped because of failure in before hook', (done) => {
+    exec(`${codecept_run}`, (err, stdout) => {
+      stdout.should.include('✔ First test will be passed');
+      stdout.should.include('S Third test will be skipped @grep');
+      stdout.should.include('S Fourth test will be skipped');
+      stdout.should.include('1 passed, 1 failed, 2 skipped');
+      err.code.should.eql(1);
+      done();
+    });
+  });
+
+  it('should skip tests correctly with grep options', (done) => {
+    exec(`${codecept_run} --grep @grep`, (err, stdout) => {
+      stdout.should.include('✔ First test will be passed');
+      stdout.should.include('S Third test will be skipped @grep');
+      stdout.should.include('1 passed, 1 failed, 1 skipped');
+      err.code.should.eql(1);
+      done();
+    });
+  });
+
+  it('should trigger skipped events', (done) => {
+    exec(`${codecept_run} --verbose`, (err, stdout) => {
+      err.code.should.eql(1);
+      stdout.should.include('Emitted | test.skipped');
+      done();
+    });
+  });
+});
diff --git a/test/runner/definitions_test.js b/test/runner/definitions_test.js
index afc6d48f..cd838e9b 100644
--- a/test/runner/definitions_test.js
+++ b/test/runner/definitions_test.js
@@ -184,11 +184,11 @@ describe('Definitions', function () {
       types.should.be.valid;
 
       const definitionsFile = types.getSourceFileOrThrow(`${codecept_dir}/steps.d.ts`);
-      const CallbackOrder = definitionsFile.getNamespaceOrThrow('CodeceptJS').getInterfaceOrThrow('CallbackOrder').getStructure();
+      const CallbackOrder = definitionsFile.getNamespaceOrThrow('CodeceptJS').getInterfaceOrThrow('SupportObject').getStructure();
       CallbackOrder.properties.should.containSubset([
-        { name: '[0]', type: 'CodeceptJS.I' },
-        { name: '[1]', type: 'MyPage' },
-        { name: '[2]', type: 'SecondPage' },
+        { name: 'I', type: 'CodeceptJS.I' },
+        { name: 'MyPage', type: 'MyPage' },
+        { name: 'SecondPage', type: 'SecondPage' },
       ]);
       done();
     });
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 971af7a5..95f26a1b 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -125,14 +125,12 @@ describe('CodeceptJS Interface', () => {
   it('should run all tests with data of array by only', (done) => {
     exec(config_run_config('codecept.addt.json'), (err, stdout) => {
       const output = stdout.replace(/in [0-9]ms/g, '').replace(/\r/g, '');
-      output.should.include(`Got array item 1
-  ✔ Should log array of strings | {"1"}`);
-
-      output.should.include(`Got array item 2
-  ✔ Should log array of strings | {"2"}`);
-
-      output.should.include(`Got array item 3
-  ✔ Should log array of strings | {"3"}`);
+      output.should.include('Got array item 1');
+      output.should.include('Should log array of strings | {"1"}');
+      output.should.include('Got array item 2');
+      output.should.include('Should log array of strings | {"2"}');
+      output.should.include('Got array item 3');
+      output.should.include('Should log array of strings | {"3"}');
       assert(!err);
       done();
     });
diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js
index 4fb0200c..4d4a104c 100644
--- a/test/runner/run_workers_test.js
+++ b/test/runner/run_workers_test.js
@@ -32,6 +32,17 @@ describe('CodeceptJS Workers Runner', function () {
     });
   });
 
+  it('should print positive or zero failures with same name tests', function (done) {
+    if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version');
+    exec(`${codecept_run_glob('configs/workers/codecept.workers-negative.conf.js')} 2`, (err, stdout) => {
+      stdout.should.include('Running tests in 2 workers...');
+      stdout.should.not.include('FAIL  | 2 passed, -6 failed');
+      stdout.should.include('FAIL  | 2 passed, 2 failed');
+      assert(err);
+      done();
+    });
+  });
+
   it('should use grep', function (done) {
     if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version');
     exec(`${codecept_run} 2 --grep "grep"`, (err, stdout) => {
diff --git a/test/unit/parser_test.js b/test/unit/parser_test.js
index 6d990336..70440345 100644
--- a/test/unit/parser_test.js
+++ b/test/unit/parser_test.js
@@ -15,8 +15,15 @@ class Obj {
   async method4(locator, context) {
     return false;
   }
+
+  method5({ locator, sec }) {}
 }
-/* eslint-enable no-unused-vars */
+const fixturesDestructuredArgs = [
+  'function namedFn({locator, sec}) {}',
+  'function * namedFn({locator, sec}) {}',
+  '({locator, sec}) => {}',
+  '({locator, sec}) => {}',
+];
 
 describe('parser', () => {
   const obj = new Obj();
@@ -29,5 +36,18 @@ describe('parser', () => {
     it('should get params for async function', () => {
       expect(parser.getParamsToString(obj.method4)).to.eql('locator, context');
     });
+    fixturesDestructuredArgs.forEach(arg => {
+      it(`should get params for anonymous function with destructured args | ${arg}`, () => {
+        expect(parser.getParams(arg)).to.eql(['locator', 'sec']);
+      });
+    });
+
+    it('should get params for anonymous function with destructured args', () => {
+      expect(parser.getParams(({ locator, sec }, { first, second }) => {})).to.eql(['locator', 'sec', 'first', 'second']);
+    });
+
+    it('should get params for class method with destructured args', () => {
+      expect(parser.getParams(obj.method5)).to.eql(['locator', 'sec']);
+    });
   });
 });
diff --git a/test/unit/plugin/tryTo_test.js b/test/unit/plugin/tryTo_test.js
new file mode 100644
index 00000000..16f1acb8
--- /dev/null
+++ b/test/unit/plugin/tryTo_test.js
@@ -0,0 +1,23 @@
+const assert = require('assert');
+const tryTo = require('../../../lib/plugin/tryTo')();
+const recorder = require('../../../lib/recorder');
+
+describe('retryFailedStep', () => {
+  beforeEach(() => {
+    recorder.start();
+  });
+
+  it('should execute command on success', async () => {
+    const ok = await tryTo(() => recorder.add(() => 5));
+    assert.equal(true, ok);
+    return recorder.promise();
+  });
+
+  it('should execute command on fail', async () => {
+    const notOk = await tryTo(() => recorder.add(() => {
+      throw new Error('Ups');
+    }));
+    assert.equal(false, notOk);
+    return recorder.promise();
+  });
+});