Repository: Codeception/CodeceptJS
Pull request #2248 (2020-03-03 13:42:48)

handle beforesuite failures when allure enabled and report beforesuite failures
## Motivation/Description of the PR
- Fixes issue with allure when BeforeSuite fails in parallel workers codeceptjs still output success when allure is enabled. Observe this is marked as `OK` rather than failed:
![image](https://user-images.githubusercontent.com/9056958/75780987-4422f880-5d54-11ea-80fa-7d2b9d40108b.png)

This underlying error is masked in workers mode, but if you run in non workers mode you'll see this error:
```
   TypeError: Cannot read property 'end' of undefined
    at Allure.endCase (/Users/myuser/Code/codecepttest/node_modules/allure-js-commons/index.js:50:26)
    at EventEmitter.<anonymous> (/Users/myuser/Code/codecepttest/node_modules/codeceptjs/lib/plugin/allure.js:219:16)
    at EventEmitter.emit (events.js:208:15)
    at Object.emit (/Users/myuser/Code/codecepttest/node_modules/codeceptjs/lib/event.js:115:28)
    at errHandler (/Users/myuser/Code/codecepttest/node_modules/codeceptjs/lib/scenario.js:101:13)
    at /Users/myuser/Code/codecepttest/node_modules/codeceptjs/lib/scenario.js:132:11
    at /Users/myuser/Code/codecepttest/node_modules/codeceptjs/lib/recorder.js:227:9
    at processTicksAndRejections (internal/process/task_queues.js:85:5)
    [1] <teardown> Stopping recording promises
```


- Resolves https://github.com/Codeception/CodeceptJS/issues/2247

Also fixes: BeforeSuite failures do not appear in allure report:


Prior to my fix: 
![image](https://user-images.githubusercontent.com/9056958/75786820-c4019080-5d5d-11ea-83cd-71c6c73441c1.png)


> This fixes a broken case at the moment where if a `BeforeSuite` fails there is nothing in the allure report indicate a problem. This is especially problematic when running parallel.
> 
> I wasn't sure it it was possible to generate name here rather than hard coding a blank test case.
> BeforeSuite failed'.
> 
> The result here is that:
*NOTE IN IMAGES I TYPED BEFOREACH INSTEAD OF BEFORESUITE*
> ![image](https://user-images.githubusercontent.com/9056958/75786507-55243780-5d5d-11ea-8212-2ae75d86cc02.png)
> 
> How does this impact run-workers where `BeforeSuite` can be run multiple time? see below
> The setup failure is correctly reported:
*NOTE IN IMAGES I TYPED BEFOREACH INSTEAD OF BEFORESUITE*
> ![image](https://user-images.githubusercontent.com/9056958/75786403-24dc9900-5d5d-11ea-8365-fd5dd97ba2f6.png)
> (WRONG NUMBER HERE error message comes from my test where i'm randomly generating number between 0 and 1 so i can fail the beforesuite in one or more threads)



Applicable plugins:

- [X] allure
- [ ] autoDelay
- [ ] autoLogin
- [ ] customLocator
- [ ] pauseOnFail
- [ ] puppeteerCoverage
- [ ] retryFailedStep
- [ ] screenshotOnFail
- [ ] selenoid
- [ ] stepByStepReport
- [ ] wdio

## Type of change

- [ ] :fire: Breaking changes
- [ ] :rocket: New functionality
- [X] :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`)
- [X] Lint checking (Run `npm run lint`)
- [X] Local tests are passed (Run `npm test`)


PeterNgTr: @Georgegriff would be nice if we can have tests for this?
Georgegriff: I'll try, unfortunately this plugin isn't very thoroughly tested, that I could find, so I wasn't sure where to start with testing the events
Georgegriff: I wanted to write tests, If you could provide any pointers that's be great
PeterNgTr: I would think of using the child process to execute codeceptjs and then asserting the console log. 
Georgegriff: Thanks, where would these tests live in the codebase?
PeterNgTr: > Thanks, where would these tests live in the codebase?

maybe here https://github.com/Codeception/CodeceptJS/blob/master/test/runner/allure_test.js?
Georgegriff: Thanks thats what I thought I'll try to get to this soon :)
Georgegriff: @PeterNgTr unit tests added
DavertMik: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Clones added
============
- test/data/sandbox/configs/allure/allure-failed.conf.js  1
- test/runner/allure_test.js  4
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/DavertMik/CodeceptJS/pullRequest?prid=5111204&bid=16806513)
        
DavertMik: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Clones added
============
- test/data/sandbox/configs/allure/before_suite_test_failed.conf.js  1
- test/runner/allure_test.js  4
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/DavertMik/CodeceptJS/pullRequest?prid=5111204&bid=16806513)
        
DavertMik: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Clones added
============
- test/data/sandbox/configs/allure/before_suite_test_failed.conf.js  1
- test/runner/allure_test.js  4
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/DavertMik/CodeceptJS/pullRequest?prid=5111204&bid=16806513)
        

Patch:
diff --git a/README.md b/README.md
index fff23040..846067eb 100644
--- a/README.md
+++ b/README.md
@@ -182,13 +182,13 @@ In case you need to return a value from a webpage and use it directly in test, y
 They are expected to be used inside async/await functions, and their results will be available in test:
 
 ```js
-let assert = require('assert');
+const assert = require('assert');
 
 Feature('CodeceptJS Demonstration');
 
 Scenario('test page title', async (I) => {
   I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
-  let title = await I.grabTitle();
+  const title = await I.grabTitle();
   assert.equal(title, 'Example application with SimpleForm and Twitter Bootstrap');
 });
 ```
@@ -282,6 +282,7 @@ Thanks all to those who are and will have contributing to this awesome project!
 <a href="https://github.com/lennym"><img src="https://avatars3.githubusercontent.com/u/117398?v=4" title="lennym" width="80" height="80"></a><a href="https://github.com/petehouston"><img src="https://avatars0.githubusercontent.com/u/9006720?v=4" title="petehouston" width="80" height="80"></a><a href="https://github.com/Holorium"><img src="https://avatars1.githubusercontent.com/u/10815542?v=4" title="Holorium" width="80" height="80"></a><a href="https://github.com/johnyb"><img src="https://avatars2.githubusercontent.com/u/86358?v=4" title="johnyb" width="80" height="80"></a><a href="https://github.com/jamesgeorge007"><img src="https://avatars2.githubusercontent.com/u/25279263?v=4" title="jamesgeorge007" width="80" height="80"></a><a href="https://github.com/jinjorge"><img src="https://avatars3.githubusercontent.com/u/2208083?v=4" title="jinjorge" width="80" height="80"></a>
 <a href="https://github.com/galkin"><img src="https://avatars3.githubusercontent.com/u/5930544?v=4" title="galkin" width="80" height="80"></a><a href="https://github.com/radhey1851"><img src="https://avatars2.githubusercontent.com/u/22446528?v=4" title="radhey1851" width="80" height="80"></a>
 <a href="https://github.com/nitschSB"><img src="https://avatars0.githubusercontent.com/u/39341455?v=4" title="nitschSB" width="80" height="80"></a><a href="https://github.com/abner"><img src="https://avatars1.githubusercontent.com/u/42773?v=4" title="abner" width="80" height="80"></a><a href="https://github.com/Akxe"><img src="https://avatars3.githubusercontent.com/u/2001798?v=4" title="Akxe" width="80" height="80"></a><a href="https://github.com/Kalostrinho"><img src="https://avatars0.githubusercontent.com/u/19229249?v=4" title="Kalostrinho" width="80" height="80"></a><a href="https://github.com/asselin"><img src="https://avatars2.githubusercontent.com/u/911250?v=4" title="asselin" width="80" height="80"></a><a href="https://github.com/xt1"><img src="https://avatars2.githubusercontent.com/u/3820037?v=4" title="xt1" width="80" height="80"></a>
+<a href="https://github.com/gkushang"><img src="https://avatars0.githubusercontent.com/u/3663389?s=460&u=0f7dc8baaf29dc15fb2ec51398530c2e6f506f54&v=4" title="KushangGajjar" width="80" height="80"></a>
 
 [//]: contributor-faces
 
diff --git a/docs/basics.md b/docs/basics.md
index dcc6be5c..d3cb25dc 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -431,10 +431,11 @@ This launches the interactive console where you can call any action from the `I`
 ```
  Interactive shell started
  Press ENTER to resume test
- - Use JavaScript syntax to try steps in action
+ Use JavaScript syntax to try steps in action
+ - Press ENTER to run the next step
  - Press TAB twice to see all available commands
- - Enter next to run the next step
-
+ - Type exit + Enter to exit the interactive shell
+ - Prefix => to run js commands 
  I.
 ```
 
diff --git a/docs/examples.md b/docs/examples.md
index 48a888a9..0a2d1c1c 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -52,19 +52,21 @@ This repository demonstrates usage of:
 * testing WYSIWYG editor
 * GitLab CI
 
-## [CodeceptJS Cucumber E2E Framework](https://github.com/gkushang/codeceptjs-e2e)
+## [CodeceptJS Cucumber BDD E2E Framework](https://github.com/gkushang/codeceptjs-bdd)
 
-This repository contains complete E2E framework for CodeceptJS with Cucumber and SauceLabs Integration
+This repository contains complete BDD E2E framework for CodeceptJS with Cucumber and SauceLabs Integration
 
-* CodecepJS-Cucumber E2E Framework
+* CodeceptJS-Cucumber BDD Framework. Link to [docs](https://gkushang.github.io/)
+* [CLI](https://gkushang.github.io/1-getting-started/setup-framework/) to quick start
 * Saucelabs Integration
 * Run Cross Browser tests in Parallel on SauceLabs with a simple command
 * Run tests on `chrome:headless`
 * Page Objects
 * `Should.js` Assertion Library
+* Soft Assertions
 * Uses `wdio` service (selenium-standalone, sauce)
 * Allure HTML Reports
-* Uses shared Master configuration
+* Uses shared configuration
 * Sample example and feature files of GitHub Features
 
 ## [Amazon Tests v2](https://gitlab.com/thanhnguyendh/codeceptjs-wdio-services)
diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md
index ca29134b..cdf451da 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -19,9 +19,9 @@ Uses [Playwright][1] library to run tests inside:
 
 This helper works with a browser out of the box with no additional tools required to install.
 
-Requires `playwright` package version ^0.11.0 to be installed:
+Requires `playwright` package version ^0.12.1 to be installed:
 
-    npm i playwright@^0.11.0 --save
+    npm i playwright@^0.12.1 --save
 
 ## Configuration
 
diff --git a/docs/helpers/Puppeteer-firefox.md b/docs/helpers/Puppeteer-firefox.md
index 1e72d695..dee26de1 100644
--- a/docs/helpers/Puppeteer-firefox.md
+++ b/docs/helpers/Puppeteer-firefox.md
@@ -21,7 +21,7 @@ If you want to use puppeteer-firefox, you should add it in Puppeteer section in
 ```js
 helpers: {
         Puppeteer: {
-            browser: process.env.BROWSER || 'firefox',            
+            browser: process.env.BROWSER || 'firefox',
             url: process.env.BASE_URL || 'https://example.com',
             chrome: {
                 args: [
@@ -39,6 +39,7 @@ helpers: {
 ## Run-multiple
 
 Example multiple section in codecept.conf.js:
+
 ```js
  multiple: {
         parallel: {
@@ -53,3 +54,33 @@ Example multiple section in codecept.conf.js:
         },
     },
 ```
+
+## Puppeteer v2.1.0 onwards
+
+Historically, Puppeteer supported Firefox indirectly through puppeteer-firefox, which relied on a custom, patched version of Firefox. This approach was also known as “Juggler”. After discussions with Mozilla, we collectively concluded that relying on custom patches was infeasible. Since then, we have been collaborating with Mozilla on supporting Puppeteer on “stock” Firefox. From Puppeteer v2.1.0 onwards, as an experimental feature, you can specify puppeteer.launch({product: 'firefox'}) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches.
+
+```sh
+npm i puppeteer@v2.1.0
+```
+
+If you want to try this expirement within CodeceptJS, you should add it in Puppeteer section in codecept.conf.js.
+
+- browser: 'chrome' OR 'firefox', 'chrome' is default value
+
+```js
+helpers: {
+        Puppeteer: {
+            browser: process.env.BROWSER || 'firefox',
+            url: process.env.BASE_URL || 'https://example.com',
+            chrome: {
+                args: [
+                    '--ignore-certificate-errors',
+                ],
+            },
+            firefox: {
+                args: [
+                    '--ignore-certificate-errors'
+                ],
+            },
+        },
+```
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index a504d0b4..23a36e7e 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -1094,7 +1094,7 @@ Returns **[Promise][25]&lt;[string][18]>** attribute value
 ### moveCursorTo
 
 Moves cursor to element matched by locator.
-Extra shift can be set with offsetX and offsetY options.
+Extra shift can be set with xOffset and yOffset options.
 
 ```js
 I.moveCursorTo('.tooltip');
@@ -1104,8 +1104,8 @@ I.moveCursorTo('#submit', 5,5);
 #### Parameters
 
 -   `locator` **([string][18] | [object][19])** located by CSS|XPath|strict locator.
--   `offsetX` **[number][22]** (optional, `0` by default) X-axis offset. 
--   `offsetY` **[number][22]** (optional, `0` by default) Y-axis offset. 
+-   `offsetX` **[number][22]** (optional, `undefined` by default) X-axis offset. 
+-   `offsetY` **[number][22]** (optional, `undefined` by default) Y-axis offset. 
 
 ### openNewTab
 
diff --git a/docs/playwright.md b/docs/playwright.md
index 09712b19..4a7416e3 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -28,7 +28,7 @@ It's readable and simple and working using Playwright API!
 To start you need CodeceptJS with Playwright packages installed
 
 ```bash
-npm install codeceptjs playwright@^0.10.0 --save
+npm install codeceptjs playwright@^0.12.1 --save
 ```
 
 Or see [alternative installation options](http://codecept.io/installation/)
@@ -52,7 +52,8 @@ Make sure `Playwright` helper is enabled in `codecept.conf.js` config:
   helpers: {
     Playwright: {
       url: "http://localhost",
-      show: true
+      show: true,
+      browser: 'chromium'
     }
   }
   // ..
@@ -75,6 +76,7 @@ When to consider navigation succeeded, defaults to `load`. Given an array of eve
     Playwright: {
       url: "http://localhost",
       show: true,
+      browser: 'chromium',
       waitForNavigation: "networkidle0"
     }
   }
@@ -87,7 +89,7 @@ It will wait for a small amount of time (100ms) by default after each user actio
 
 ## Writing Tests
 
-CodeceptJS test should be created with `gt` command:
+Additional CodeceptJS tests should be created with `gt` command:
 
 ```sh
 npx codeceptjs gt
@@ -110,7 +112,7 @@ Tests consist with a scenario of user's action taken on a page. The most widely
 
 > ℹ  All actions are listed in [Playwright helper reference](http://codecept.io/helpers/Playwright/).*
 
-All actions which interact with elements **support CSS and XPath locators**. Actions like `click` or `fillField` by locate elements by their name or value on a page:
+All actions which interact with elements can use **[CSS or XPath locators](https://codecept.io/locators/#css-and-xpath)**. Actions like `click` or `fillField` can locate elements by their name or value on a page:
 
 ```js
 // search for link or button
@@ -202,7 +204,7 @@ CodeceptJS allows you to implement custom actions like `I.createTodo` or use **P
 
 ## Extending
 
-Playwright has a very [rich and flexible API](https://github.com/microsoft/playwright/blob/v0.10.0/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.
+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.
 
 Start with creating an `MyPlaywright` helper using `generate:helper` or `gh` command:
 
@@ -238,7 +240,7 @@ async setPermissions() {
   return context.setPermissions('https://html5demos.com', ['geolocation']);
 }
 
-> [▶ Learn more about BrowserContext](https://github.com/microsoft/playwright/blob/v0.10.0/docs/api.md#class-browsercontext)
+> [▶ Learn more about BrowserContext](https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md#class-browsercontext)
 
 > [▶ Learn more about Helpers](http://codecept.io/helpers/)
 
diff --git a/docs/shadow.md b/docs/shadow.md
new file mode 100644
index 00000000..9955a3cc
--- /dev/null
+++ b/docs/shadow.md
@@ -0,0 +1,66 @@
+---
+permalink: /shadow
+title: Locating Shadow Dom Elements
+---
+
+# Locating Shadow Dom Elements
+
+Shadow DOM is one of the key browser features that make up web components. Web components are a really great way to build reusable elements, and are able to scale all the way up to complete web applications. Style encapsulation, the feature that gives shadow DOM it's power, has been a bit of a pain when it comes to E2E or UI testing. Things just got a little easier though, as CodeceptJS introduced built-in support for shadow DOM via locators of type `shadow`. Let's dig into what they're all about.
+
+Generated HTML code may often look like this (ref: [Salesforce's Lighting Web Components](https://github.com/salesforce/lwc)):
+
+```js
+<body>
+  <my-app>
+    <recipe-hello>
+      <button>Click Me!</button>
+    </recipe-hello>
+    <recipe-hello-binding>
+      <ui-input>
+        <input type="text" class="input">
+      </ui-input>
+    </recipe-hello-binding>
+  </my-app>
+</body>
+```
+
+This uses custom elements, `my-app`, `recipe-hello`, `recipe-hello-binding` and `ui-input`. It's quite common that clickable elements are not actual `a` or `button` elements but custom elements. This way `I.click('Click Me!');` won't work, as well as `fillField('.input', 'value)`. Finding a correct locator for such cases turns to be almost impossible until `shadow` element support is added to CodeceptJS.
+
+## Locate Shadow Dom
+
+For Web Components or [Salesforce's Lighting Web Components](https://github.com/salesforce/lwc) with Shadow DOM's, a special `shadow` locator is available. It allows to select an element by its shadow dom sequences and sequences are defined as an Array of `elements`. Elements defined in the array of `elements` must be in the ordered the shadow elements appear in the DOM.
+
+```js
+{ shadow: ['my-app', 'recipe-hello', 'button'] } 
+{ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }
+```
+
+In WebDriver, you can use shadow locators in any method where locator is required. 
+
+For example, to fill value in `input` field or to click the `Click Me!` button, in above HTML code:
+
+```js
+I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');
+I.click({ shadow: ['my-app', 'recipe-hello', 'button'] });
+```
+
+## Example
+
+```js
+Feature('Shadow Dom Locators');
+
+Scenario('should fill input field within shadow elements', I => {
+
+  // navigate to LWC webpage containing shadow dom
+  I.amOnPage('https://recipes.lwc.dev/');
+
+  // click Click Me! button
+  I.click({ shadow: ['my-app', 'recipe-hello', 'button'] });
+
+  // fill the input field
+  I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');
+
+});
+
+
+```
diff --git a/lib/codecept.js b/lib/codecept.js
index 32b62827..1e4f202e 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -6,7 +6,7 @@ const resolve = require('path').resolve;
 
 const container = require('./container');
 const Config = require('./config');
-const event = require('../lib/event');
+const event = require('./event');
 const runHook = require('./hooks');
 const output = require('./output');
 
diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js
index d95c3430..cb836113 100644
--- a/lib/command/run-workers.js
+++ b/lib/command/run-workers.js
@@ -46,6 +46,12 @@ module.exports = function (workers, options) {
     ...overrideConfigs,
   };
 
+  const configRerun = config.rerun || {};
+  let maxReruns = configRerun.maxReruns || 1;
+  if (maxReruns > 1) {
+    maxReruns *= numberOfWorkers;
+  }
+
   const testRoot = getTestRoot(configPath);
 
   const codecept = new Codecept(config, options);
@@ -95,11 +101,11 @@ module.exports = function (workers, options) {
 
       switch (message.event) {
         case event.test.failed:
-          updateFinishedTests(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data), maxReruns);
           output.test.failed(repackTest(message.data));
           break;
         case event.test.passed: output.test.passed(repackTest(message.data));
-          updateFinishedTests(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data), maxReruns);
           break;
         case event.suite.before: output.suite.started(message.data); break;
         case event.all.after: appendStats(message.data); break;
@@ -223,11 +229,21 @@ function simplifyObject(object) {
     }, {});
 }
 
-const updateFinishedTests = (test) => {
+const updateFinishedTests = (test, maxReruns) => {
   const { id } = test;
   if (finishedTests[id]) {
-    const stats = { passes: 0, failures: -1, tests: 0 };
-    appendStats(stats);
+    if (finishedTests[id].runs > maxReruns) {
+      const stats = {
+        passes: 0,
+        failures: -1,
+        tests: 0,
+      };
+      appendStats(stats);
+    } else {
+      finishedTests[id].runs++;
+    }
+  } else {
+    finishedTests[id] = test;
+    finishedTests[id].runs = 1;
   }
-  finishedTests[id] = test;
 };
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index 04bed112..49681640 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -37,6 +37,7 @@ const popupStore = new Popup();
 const consoleLogStore = new Console();
 const availableBrowsers = ['chromium', 'webkit', 'firefox'];
 
+const { createValueEngine } = require('./extras/PlaywrightPropEngine');
 /**
  * Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside:
  *
@@ -46,10 +47,10 @@ const availableBrowsers = ['chromium', 'webkit', 'firefox'];
  *
  * This helper works with a browser out of the box with no additional tools required to install.
  *
- * Requires `playwright` package version ^0.11.0 to be installed:
+ * Requires `playwright` package version ^0.12.1 to be installed:
  *
  * ```
- * npm i playwright@^0.11.0 --save
+ * npm i playwright@^0.12.1 --save
  * ```
  *
  * ## Configuration
@@ -185,6 +186,8 @@ class Playwright extends Helper {
     this.isRemoteBrowser = false;
     this.isRunning = false;
     this.isAuthenticated = false;
+    this.sessionPages = {};
+    this.activeSessionName = '';
 
     // override defaults with config
     this._setConfig(config);
@@ -250,11 +253,17 @@ class Playwright extends Helper {
     try {
       requireg('playwright');
     } catch (e) {
-      return ['playwright@^0.10'];
+      return ['playwright@^0.12.1'];
     }
   }
 
-  _init() {
+  async _init() {
+    // register an internal selector engine for reading value property of elements in a selector
+    try {
+      await playwright.selectors.register('__value', createValueEngine);
+    } catch (e) {
+      console.warn(e);
+    }
   }
 
   _beforeSuite() {
@@ -289,6 +298,12 @@ class Playwright extends Helper {
       return this._stopBrowser();
     }
 
+    // ensure current page is in default context
+    if (this.page) {
+      const existingPages = await this.browserContext.pages();
+      await this._setPage(existingPages[0]);
+    }
+
     if (this.options.keepBrowserState) return;
 
     if (!this.options.keepCookies) {
@@ -316,8 +331,9 @@ class Playwright extends Helper {
   _session() {
     const defaultContext = this.browserContext;
     return {
-      start: async () => {
+      start: async (sessionName = '') => {
         this.debugSection('Incognito Tab', 'opened');
+        this.activeSessionName = sessionName;
 
         const bc = await this.browser.newContext();
         const page = await bc.newPage();
@@ -332,13 +348,22 @@ class Playwright extends Helper {
       loadVars: async (context) => {
         this.browserContext = context;
         const existingPages = await context.pages();
-        return this._setPage(existingPages[0]);
+        this.sessionPages[this.activeSessionName] = existingPages[0];
+        return this._setPage(this.sessionPages[this.activeSessionName]);
       },
-      restoreVars: async () => {
+      restoreVars: async (session) => {
         this.withinLocator = null;
         this.browserContext = defaultContext;
-        const existingPages = await this.browserContext.pages();
-        await this._setPage(existingPages[0]);
+
+        if (!session) {
+          this.activeSessionName = '';
+          const existingPages = await this.browserContext.pages();
+          await this._setPage(existingPages[0]);
+        } else {
+          this.activeSessionName = session;
+          await this._setPage(this.sessionPages[this.activeSessionName]);
+        }
+
         return this._waitForAction();
       },
     };
@@ -566,7 +591,7 @@ class Playwright extends Helper {
 
     if (this.config.basicAuth && (this.isAuthenticated !== true)) {
       if (url.includes(this.options.url)) {
-        await this.page.authenticate(this.config.basicAuth);
+        await this.browserContext.setHTTPCredentials(this.config.basicAuth);
         this.isAuthenticated = true;
       }
     }
@@ -1529,7 +1554,7 @@ class Playwright extends Helper {
     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);
+      const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
       array.push(await a.jsonValue());
     }
 
@@ -1544,10 +1569,19 @@ class Playwright extends Helper {
     const outputFile = screenshotOutputFolder(fileName);
 
     this.debug(`Screenshot is saving to ${outputFile}`);
-    const openPages = await this.browserContext.pages();
-    if (openPages.length > 1) {
-      this.page = openPages[openPages.length - 1];
+
+    if (this.activeSessionName) {
+      const activeSessionPage = this.sessionPages[this.activeSessionName];
+
+      if (activeSessionPage) {
+        return activeSessionPage.screenshot({
+          path: outputFile,
+          fullPage: fullPageOption,
+          type: 'png',
+        });
+      }
     }
+
     return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
   }
 
@@ -1573,21 +1607,15 @@ class Playwright extends Helper {
     const matcher = await this.context;
     let waiter;
     const context = await this._getContext();
-    if (locator.isCSS()) {
-      const enabledFn = function (locator) {
-        const els = document.querySelectorAll(locator);
-        if (!els || els.length === 0) {
-          return false;
-        }
-        return Array.prototype.filter.call(els, el => !el.disabled).length > 0;
-      };
-      waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value);
+    if (!locator.isXPath()) {
+      // playwright combined selectors
+      waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> :not([disabled])`, { timeout: waitTimeout });
     } else {
-      const enabledFn = function (locator, $XPath) {
+      const enabledFn = function ([locator, $XPath]) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).filter(el => !el.disabled).length > 0;
       };
-      waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+      waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
     }
     return waiter.catch((err) => {
       throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1603,21 +1631,15 @@ class Playwright extends Helper {
     const matcher = await this.context;
     let waiter;
     const context = await this._getContext();
-    if (locator.isCSS()) {
-      const valueFn = function (locator, value) {
-        const els = document.querySelectorAll(locator);
-        if (!els || els.length === 0) {
-          return false;
-        }
-        return Array.prototype.filter.call(els, el => (el.value || '').indexOf(value) !== -1).length > 0;
-      };
-      waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value);
+    if (!locator.isXPath()) {
+      // uses a custom selector engine for finding value properties on elements
+      waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> __value=${value}`, { timeout: waitTimeout, waitFor: 'visible' });
     } else {
-      const valueFn = function (locator, $XPath, value) {
+      const valueFn = function ([locator, $XPath, value]) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
       };
-      waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value);
+      waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], { timeout: waitTimeout });
     }
     return waiter.catch((err) => {
       const loc = locator.toString();
@@ -1636,20 +1658,20 @@ class Playwright extends Helper {
     let waiter;
     const context = await this._getContext();
     if (locator.isCSS()) {
-      const visibleFn = function (locator, num) {
+      const visibleFn = function ([locator, num]) {
         const els = document.querySelectorAll(locator);
         if (!els || els.length === 0) {
           return false;
         }
         return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num;
       };
-      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num);
+      waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout });
     } else {
-      const visibleFn = function (locator, $XPath, num) {
+      const visibleFn = function ([locator, $XPath, num]) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num;
       };
-      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num);
+      waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], { timeout: waitTimeout });
     }
     return waiter.catch((err) => {
       throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1681,7 +1703,7 @@ class Playwright extends Helper {
     locator = new Locator(locator, 'css');
 
     const context = await this._getContext();
-    const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout });
+    const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout });
     return waiter.catch((err) => {
       throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
@@ -1696,7 +1718,7 @@ class Playwright extends Helper {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
     const context = await this._getContext();
-    const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'visible' });
+    const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'visible' });
     return waiter.catch((err) => {
       throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
     });
@@ -1709,7 +1731,7 @@ class Playwright extends Helper {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
     const context = await this._getContext();
-    const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'hidden' });
+    const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'hidden' });
     return waiter.catch((err) => {
       throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
     });
@@ -1722,7 +1744,7 @@ class Playwright extends Helper {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
     const context = await this._getContext();
-    return context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'hidden' }).catch((err) => {
+    return context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'hidden' }).catch((err) => {
       throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
@@ -1743,7 +1765,7 @@ class Playwright extends Helper {
     return this.page.waitForFunction((urlPart) => {
       const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
       return currUrl.indexOf(urlPart) > -1;
-    }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
+    }, urlPart, { timeout: waitTimeout }).catch(async (e) => {
       const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
       if (/failed: timeout/i.test(e.message)) {
         throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
@@ -1767,7 +1789,7 @@ class Playwright extends Helper {
     return this.page.waitForFunction((urlPart) => {
       const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
       return currUrl.indexOf(urlPart) > -1;
-    }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
+    }, urlPart, { timeout: waitTimeout }).catch(async (e) => {
       const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
       if (/failed: timeout/i.test(e.message)) {
         throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
@@ -1788,26 +1810,21 @@ class Playwright extends Helper {
 
     if (context) {
       const locator = new Locator(context, 'css');
-      if (locator.isCSS()) {
-        waiter = contextObject.waitForFunction((locator, text) => {
-          const el = document.querySelector(locator);
-          if (!el) return false;
-          return el.innerText.indexOf(text) > -1;
-        }, { timeout: waitTimeout }, locator.value, text);
+      if (!locator.isXPath()) {
+        waiter = contextObject.waitFor(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`, { timeout: waitTimeout, waitFor: 'visible' });
       }
 
       if (locator.isXPath()) {
-        waiter = contextObject.waitForFunction((locator, text, $XPath) => {
+        waiter = contextObject.waitForFunction(([locator, text, $XPath]) => {
           eval($XPath); // eslint-disable-line no-eval
           const el = $XPath(null, locator);
           if (!el.length) return false;
           return el[0].innerText.indexOf(text) > -1;
-        }, { timeout: waitTimeout }, locator.value, text, $XPath.toString());
+        }, [locator.value, text, $XPath.toString()], { timeout: waitTimeout });
       }
     } else {
-      waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text);
+      waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: waitTimeout });
     }
-
     return waiter.catch((err) => {
       throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
@@ -1897,7 +1914,7 @@ class Playwright extends Helper {
     }
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     const context = await this._getContext();
-    return context.waitForFunction(fn, { timeout: waitTimeout }, ...args);
+    return context.waitForFunction(fn, args, { timeout: waitTimeout });
   }
 
   /**
@@ -1942,17 +1959,14 @@ class Playwright extends Helper {
 
     let waiter;
     const context = await this._getContext();
-    if (locator.isCSS()) {
-      const visibleFn = function (locator) {
-        return document.querySelector(locator) === null;
-      };
-      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value);
+    if (!locator.isXPath()) {
+      waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'detached' });
     } else {
-      const visibleFn = function (locator, $XPath) {
+      const visibleFn = function ([locator, $XPath]) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).length === 0;
       };
-      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+      waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
     }
     return waiter.catch((err) => {
       throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1986,7 +2000,11 @@ module.exports = Playwright;
 
 async function findElements(matcher, locator) {
   locator = new Locator(locator, 'css');
-  if (!locator.isXPath()) return matcher.$$(locator.simplify());
+  if (locator.isCustom()) {
+    return matcher.$$(`${locator.type}=${locator.value}`);
+  } if (!locator.isXPath()) {
+    return matcher.$$(locator.simplify());
+  }
   return matcher.$$(`xpath=${locator.value}`);
 }
 
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index e879b60d..d838bb43 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -167,12 +167,14 @@ class Puppeteer extends Helper {
   constructor(config) {
     super(config);
 
-    puppeteer = requireg(config.browser === 'firefox' ? 'puppeteer-firefox' : 'puppeteer');
+    puppeteer = requireg('puppeteer');
 
     // set defaults
     this.isRemoteBrowser = false;
     this.isRunning = false;
     this.isAuthenticated = false;
+    this.sessionPages = {};
+    this.activeSessionName = '';
 
     // override defaults with config
     this._setConfig(config);
@@ -201,7 +203,7 @@ class Puppeteer extends Helper {
   }
 
   _getOptions(config) {
-    return config.browser === 'firefox' ? this.options.firefox : this.options.chrome;
+    return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome;
   }
 
   _setConfig(config) {
@@ -257,7 +259,8 @@ class Puppeteer extends Helper {
 
     // close other sessions
     const contexts = this.browser.browserContexts();
-    contexts.shift();
+    const defaultCtx = contexts.shift();
+
     await Promise.all(contexts.map(c => c.close()));
 
     if (this.options.restart) {
@@ -265,6 +268,12 @@ class Puppeteer extends Helper {
       return this._stopBrowser();
     }
 
+    // ensure this.page is from default context
+    if (this.page) {
+      const existingPages = defaultCtx.targets().filter(t => t.type() === 'page');
+      await this._setPage(await existingPages[0].page());
+    }
+
     if (this.options.keepBrowserState) return;
 
     if (!this.options.keepCookies) {
@@ -291,11 +300,13 @@ class Puppeteer extends Helper {
 
   _session() {
     return {
-      start: async () => {
+      start: async (name = '', config = {}) => {
         this.debugSection('Incognito Tab', 'opened');
+        this.activeSessionName = name;
 
         const bc = await this.browser.createIncognitoBrowserContext();
         await bc.newPage();
+
         // Create a new page inside context.
         return bc;
       },
@@ -304,12 +315,24 @@ class Puppeteer extends Helper {
       },
       loadVars: async (context) => {
         const existingPages = context.targets().filter(t => t.type() === 'page');
-        return this._setPage(await existingPages[0].page());
+        this.sessionPages[this.activeSessionName] = await existingPages[0].page();
+        return this._setPage(this.sessionPages[this.activeSessionName]);
       },
-      restoreVars: async () => {
+      restoreVars: async (session) => {
         this.withinLocator = null;
-        const existingPages = await this.browser.targets().filter(t => t.type() === 'page');
-        await this._setPage(await existingPages[0].page());
+
+        if (!session) {
+          this.activeSessionName = '';
+
+          // Find original page on default context
+          const defaultCtx = this.browser.defaultBrowserContext();
+          const existingPages = defaultCtx.targets().filter(t => t.type() === 'page');
+          await this._setPage(await existingPages[0].page());
+        } else {
+          this.activeSessionName = session;
+          await this._setPage(this.sessionPages[this.activeSessionName]);
+        }
+
         return this._waitForAction();
       },
     };
@@ -1614,10 +1637,19 @@ class Puppeteer extends Helper {
     const outputFile = screenshotOutputFolder(fileName);
 
     this.debug(`Screenshot is saving to ${outputFile}`);
-    const openSessions = await this.browser.pages();
-    if (openSessions.length > 1) {
-      this.page = await this.browser.targets()[this.browser.targets().length - 1].page();
+
+    if (this.activeSessionName) {
+      const activeSessionPage = this.sessionPages[this.activeSessionName];
+
+      if (activeSessionPage) {
+        return activeSessionPage.screenshot({
+          path: outputFile,
+          fullPage: fullPageOption,
+          type: 'png',
+        });
+      }
     }
+
     return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
   }
 
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index 8817cb02..5d6243d3 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -75,7 +75,9 @@ class REST extends Helper {
     }
 
     if ((typeof request.data) === 'string') {
-      request.headers = Object.assign(request.headers, { 'Content-Type': 'application/x-www-form-urlencoded' });
+      if (!request.headers || !request.headers['Content-Type']) {
+        request.headers = Object.assign(request.headers, { 'Content-Type': 'application/x-www-form-urlencoded' });
+      }
     }
 
     if (this.config.onRequest) {
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index ffbddb6b..f13845d5 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -28,6 +28,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
 const ConnectionRefused = require('./errors/ConnectionRefused');
 const Locator = require('../locator');
 
+const SHADOW = 'shadow';
 const webRoot = 'body';
 
 /**
@@ -402,6 +403,7 @@ class WebDriver extends Helper {
   _validateConfig(config) {
     const defaults = {
       logLevel: 'silent',
+      path: '/wd/hub',
       // codeceptjs
       remoteFileUpload: true,
       smartWait: 0,
@@ -626,6 +628,62 @@ class WebDriver extends Helper {
     this.$$ = this.browser.$$.bind(this.browser);
   }
 
+  /**
+   * Check if locator is type of "Shadow"
+   *
+   * @param {object} locator
+   */
+  _isShadowLocator(locator) {
+    return locator.type === SHADOW || locator[SHADOW];
+  }
+
+  /**
+   * Locate Element within the Shadow Dom
+   *
+   * @param {object} locator
+   */
+  async _locateShadow(locator) {
+    const shadow = locator.value ? locator.value : locator[SHADOW];
+    const shadowSequence = [];
+    let elements;
+
+    if (!Array.isArray(shadow)) {
+      throw new Error(`Shadow '${shadow}' should be defined as an Array of elements.`);
+    }
+
+    // traverse through the Shadow locators in sequence
+    for (let index = 0; index < shadow.length; index++) {
+      const shadowElement = shadow[index];
+      shadowSequence.push(shadowElement);
+
+      if (!elements) {
+        elements = await (this.browser.$$(shadowElement));
+      } else if (Array.isArray(elements)) {
+        elements = await elements[0].shadow$$(shadowElement);
+      } else if (elements) {
+        elements = await elements.shadow$$(shadowElement);
+      }
+
+      if (!elements || !elements[0]) {
+        throw new Error(`Shadow Element '${shadowElement}' is not found. It is possible the element is incorrect or elements sequence is incorrect. Please verify the sequence '${shadowSequence.join('>')}' is correctly chained.`);
+      }
+    }
+
+    this.debugSection('Elements', `Found ${elements.length} '${SHADOW}' elements`);
+
+    return elements;
+  }
+
+  /**
+   * Smart Wait to locate an element
+   *
+   * @param {object} locator
+   */
+  async _smartWait(locator) {
+    this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);
+    await this.defineTimeout({ implicit: this.options.smartWait });
+  }
+
   /**
    * Get elements by different locator types, including strict locator.
    * Should be used in custom helpers:
@@ -640,6 +698,18 @@ class WebDriver extends Helper {
   async _locate(locator, smartWait = false) {
     if (require('../store').debugMode) smartWait = false;
 
+    // special locator type for Shadow DOM
+    if (this._isShadowLocator(locator)) {
+      if (!this.options.smartWait || !smartWait) {
+        const els = await this._locateShadow(locator);
+        return els;
+      }
+
+
+      const els = await this._locateShadow(locator);
+      return els;
+    }
+
     // special locator type for React
     if (locator.react) {
       const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined);
@@ -651,9 +721,9 @@ class WebDriver extends Helper {
       const els = await this.$$(withStrictLocator(locator));
       return els;
     }
-    this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);
 
-    await this.defineTimeout({ implicit: this.options.smartWait });
+    await this._smartWait(locator);
+
     const els = await this.$$(withStrictLocator(locator));
     await this.defineTimeout({ implicit: 0 });
     return els;
@@ -1382,11 +1452,11 @@ class WebDriver extends Helper {
    * {{> moveCursorTo }}
    *
    */
-  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
+  async moveCursorTo(locator, xOffset, yOffset) {
     const res = await this._locate(withStrictLocator(locator), true);
     assertElementExists(res, locator);
     const elem = usingFirstElement(res);
-    return elem.moveTo(offsetX, offsetY);
+    return elem.moveTo({ xOffset, yOffset });
   }
 
   /**
@@ -1820,7 +1890,7 @@ class WebDriver extends Helper {
           currUrl = decodeUrl(res);
           return currUrl.indexOf(urlPart) > -1;
         });
-      }, aSec * 1000).catch((e) => {
+      }, { timeout: aSec * 1000 }).catch((e) => {
         if (e.message.indexOf('timeout')) {
           throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
         }
@@ -1867,8 +1937,10 @@ class WebDriver extends Helper {
           return selected.filter(part => part.indexOf(text) >= 0).length > 0;
         }
         return selected.indexOf(text) >= 0;
-      }, aSec * 1000,
-      `element (${_context}) is not in DOM or there is no element(${_context}) with text "${text}" after ${aSec} sec`,
+      }, {
+        timeout: aSec * 1000,
+        timeoutMsg: `element (${_context}) is not in DOM or there is no element(${_context}) with text "${text}" after ${aSec} sec`,
+      },
     );
   }
 
@@ -1887,8 +1959,10 @@ class WebDriver extends Helper {
           return selected.filter(part => part.indexOf(value) >= 0).length > 0;
         }
         return selected.indexOf(value) >= 0;
-      }, aSec * 1000,
-      `element (${field}) is not in DOM or there is no element(${field}) with value "${value}" after ${aSec} sec`,
+      }, {
+        timeout: aSec * 1000,
+        timeoutMsg: `element (${field}) is not in DOM or there is no element(${field}) with value "${value}" after ${aSec} sec`,
+      },
     );
   }
 
@@ -1906,7 +1980,7 @@ class WebDriver extends Helper {
         return selected.filter(val => val === true).length > 0;
       }
       return selected;
-    }, aSec * 1000, `element (${new Locator(locator)}) still not visible after ${aSec} sec`);
+    }, { timeout: aSec * 1000, timeoutMsg: `element (${new Locator(locator)}) still not visible after ${aSec} sec` });
   }
 
   /**
@@ -1921,7 +1995,7 @@ class WebDriver extends Helper {
 
       if (!Array.isArray(selected)) selected = [selected];
       return selected.length === num;
-    }, aSec * 1000, `The number of elements (${new Locator(locator)}) is not ${num} after ${aSec} sec`);
+    }, { timeout: aSec * 1000, timeoutMsg: `The number of elements (${new Locator(locator)}) is not ${num} after ${aSec} sec` });
   }
 
   /**
@@ -1935,7 +2009,7 @@ class WebDriver extends Helper {
       if (!res || res.length === 0) return true;
       const selected = await forEachAsync(res, async el => el.isDisplayed());
       return !selected.length;
-    }, aSec * 1000, `element (${new Locator(locator)}) still visible after ${aSec} sec`);
+    }, { timeout: aSec * 1000, timeoutMsg: `element (${new Locator(locator)}) still visible after ${aSec} sec` });
   }
 
   /**
@@ -1963,7 +2037,7 @@ class WebDriver extends Helper {
         return true;
       }
       return false;
-    }, aSec * 1000, `element (${new Locator(locator)}) still on page after ${aSec} sec`);
+    }, { timeout: aSec * 1000, timeoutMsg: `element (${new Locator(locator)}) still on page after ${aSec} sec` });
   }
 
   /**
@@ -1981,7 +2055,7 @@ class WebDriver extends Helper {
     }
 
     const aSec = sec || this.options.waitForTimeout;
-    return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), aSec * 1000);
+    return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), { timeout: aSec * 1000, timeoutMsg: '' });
   }
 
   /**
@@ -2035,7 +2109,7 @@ class WebDriver extends Helper {
         }
       });
       return target;
-    }, aSec * 1000, `There is no ability to switch to next tab with offset ${num}`);
+    }, { timeout: aSec * 1000, timeoutMsg: `There is no ability to switch to next tab with offset ${num}` });
     return this.browser.switchToWindow(target);
   }
 
@@ -2061,7 +2135,7 @@ class WebDriver extends Helper {
         }
       });
       return target;
-    }, aSec * 1000, `There is no ability to switch to previous tab with offset ${num}`);
+    }, { timeout: aSec * 1000, timeoutMsg: `There is no ability to switch to previous tab with offset ${num}` });
     return this.browser.switchToWindow(target);
   }
 
@@ -2296,7 +2370,6 @@ async function filterAsync(array, callback) {
   return values;
 }
 
-
 async function findClickable(locator, locateFn) {
   locator = new Locator(locator);
   if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator, true);
@@ -2320,6 +2393,7 @@ async function findClickable(locator, locateFn) {
 
 async function findFields(locator) {
   locator = new Locator(locator);
+
   if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true);
   if (!locator.isFuzzy()) return this._locate(locator, true);
 
diff --git a/lib/helper/extras/PlaywrightPropEngine.js b/lib/helper/extras/PlaywrightPropEngine.js
new file mode 100644
index 00000000..28966ef9
--- /dev/null
+++ b/lib/helper/extras/PlaywrightPropEngine.js
@@ -0,0 +1,25 @@
+module.exports.createValueEngine = () => {
+  return {
+    // Creates a selector that matches given target when queried at the root.
+    // Can return undefined if unable to create one.
+    create(root, target) {
+      return null;
+    },
+
+    // Returns the first element matching given selector in the root's subtree.
+    query(root, selector) {
+      if (!root) {
+        return null;
+      }
+      return `${root.value}` === selector;
+    },
+
+    // Returns all elements matching given selector in the root's subtree.
+    queryAll(root, selector) {
+      if (!root) {
+        return null;
+      }
+      return `${root.value}` === selector;
+    },
+  };
+};
diff --git a/lib/helper/scripts/isElementClickable.js b/lib/helper/scripts/isElementClickable.js
index 49e5d3f4..811f9da1 100644
--- a/lib/helper/scripts/isElementClickable.js
+++ b/lib/helper/scripts/isElementClickable.js
@@ -1,24 +1,64 @@
-function isElementClickable(elem) {
-  if (!elem.getBoundingClientRect || !elem.scrollIntoView || !document.elementFromPoint) {
+function isElementClickable(element) {
+  if (!element.getBoundingClientRect || !element.scrollIntoView || !element.contains || !element.getClientRects || !document.elementFromPoint) {
     return false;
   }
 
-  const isElementInViewport = (elem) => {
-    const rect = elem.getBoundingClientRect();
-    const verticleInView = (rect.top <= window.innerHeight) && ((rect.top + rect.height) > 0);
-    const horizontalInView = (rect.left <= window.innerWidth) && ((rect.left + rect.width) > 0);
-    return horizontalInView && verticleInView;
+  const getOverlappingElement = (element, context = document) => {
+    const elemDimension = element.getBoundingClientRect();
+    const x = elemDimension.left + (element.clientWidth / 2);
+    const y = elemDimension.top + (element.clientHeight / 2);
+
+    return context.elementFromPoint(x, y);
+  };
+
+  const getOverlappingRects = (element, context = document) => {
+    const rects = element.getClientRects();
+    const rect = rects[0];
+    const x = rect.left + (rect.width / 2);
+    const y = rect.top + (rect.height / 2);
+
+    return context.elementFromPoint(x, y);
   };
 
-  const getOverlappingElement = (elem) => {
-    const rect = elem.getBoundingClientRect();
-    const x = rect.left + (elem.clientWidth / 2);
-    const y = rect.top + (elem.clientHeight / 2);
-    return document.elementFromPoint(x, y);
+  const getOverlappingElements = (element, context) => {
+    return [getOverlappingElement(element, context), getOverlappingRects(element, context)];
+  };
+
+  const isOverlappingElementMatch = (elementsFromPoint, element) => {
+    if (elementsFromPoint.some(elementFromPoint => elementFromPoint === element || element.contains(elementFromPoint))) {
+      return true;
+    }
+
+    let elementsWithShadowRoot = [...new Set(elementsFromPoint)];
+    elementsWithShadowRoot = elementsWithShadowRoot.filter(elem => elem && elem.shadowRoot && elem.shadowRoot.elementFromPoint);
+
+    let shadowElementsFromPoint = [];
+    for (const shadowElement of elementsWithShadowRoot) {
+      shadowElementsFromPoint.push(...getOverlappingElements(element, shadowElement.shadowRoot));
+    }
+    shadowElementsFromPoint = [...new Set(shadowElementsFromPoint)];
+    shadowElementsFromPoint = shadowElementsFromPoint.filter(element => !elementsFromPoint.includes(element));
+
+    if (shadowElementsFromPoint.length === 0) {
+      return false;
+    }
+
+    return isOverlappingElementMatch(shadowElementsFromPoint, element);
+  };
+
+  const isElementInViewport = (element) => {
+    const rect = element.getBoundingClientRect();
+
+    const windowHeight = (window.innerHeight || document.documentElement.clientHeight);
+    const windowWidth = (window.innerWidth || document.documentElement.clientWidth);
+
+    const vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) > 0);
+    const horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) > 0);
+
+    return (vertInView && horInView);
   };
 
-  const isClickable = elem => elem.disabled !== true && isElementInViewport(elem) && getOverlappingElement(elem) === elem;
-  return isClickable(elem);
+  return element.disabled !== true && isElementInViewport(element) && isOverlappingElementMatch(getOverlappingElements(element), element);
 }
 
 module.exports = isElementClickable;
diff --git a/lib/locator.js b/lib/locator.js
index 6a8e90b9..0fef5239 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -3,6 +3,7 @@ const sprintf = require('sprintf-js').sprintf;
 
 const xpathLocator = require('./utils').xpathLocator;
 
+const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame'];
 /** @class */
 class Locator {
   /**
@@ -96,6 +97,10 @@ class Locator {
     return this.type === 'xpath';
   }
 
+  isCustom() {
+    return this.type && !locatorTypes.includes(this.type);
+  }
+
   isStrict() {
     return this.strict;
   }
diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js
index 93d497f3..70a3966a 100644
--- a/lib/plugin/allure.js
+++ b/lib/plugin/allure.js
@@ -215,7 +215,13 @@ module.exports = (config) => {
     }
 
     err.message = err.message.replace(ansiRegExp(), '');
-    reporter.endCase('failed', err);
+    if (reporter.getCurrentTest()) {
+      reporter.endCase('failed', err);
+    } else {
+      // this means before suite failed, we should report this.
+      reporter.startCase(`BeforeSuite of suite ${reporter.getCurrentSuite().name} failed.`);
+      reporter.endCase('failed', err);
+    }
   });
 
   event.dispatcher.on(event.test.passed, () => {
diff --git a/lib/session.js b/lib/session.js
index c6afa2cd..a7229ce3 100644
--- a/lib/session.js
+++ b/lib/session.js
@@ -65,7 +65,7 @@ function session(sessionName, config, fn) {
     }
 
     recorder.add('save vars', async () => {
-      savedSessions[sessionName].vars = await savedSessions[sessionName].start(config);
+      savedSessions[sessionName].vars = await savedSessions[sessionName].start(sessionName);
     });
   }
 
@@ -97,13 +97,14 @@ function session(sessionName, config, fn) {
       });
     };
 
+    // Indicate when executing this function that we are in a session
     if (isAsyncFunction(fn)) {
       return fn.apply(null).then((res) => {
         finalize();
         return recorder.promise().then(() => res);
       }).catch((e) => {
         output.stepShift = 0;
-        session.restoreVars();
+        session.restoreVars(sessionName);
         event.dispatcher.removeListener(event.step.after, addContextToStep);
         recorder.throw(e);
         return recorder.promise();
@@ -117,7 +118,7 @@ function session(sessionName, config, fn) {
       recorder.throw(err);
     } finally {
       recorder.catch((e) => {
-        session.restoreVars();
+        session.restoreVars(sessionName);
         output.stepShift = 0;
         event.dispatcher.removeListener(event.step.after, addContextToStep);
         throw e;
diff --git a/package.json b/package.json
index bb3c725f..6cd187f7 100644
--- a/package.json
+++ b/package.json
@@ -68,13 +68,13 @@
     "js-beautify": "^1.10.2",
     "lodash.clonedeep": "^4.5.0",
     "lodash.merge": "^4.6.2",
-    "mkdirp": "^0.5.1",
+    "mkdirp": "^1.0.3",
     "mocha": "^6.2.2",
     "mocha-junit-reporter": "^1.23.1",
     "ms": "^2.1.2",
     "parse-function": "^5.5.0",
     "promise-retry": "^1.1.1",
-    "requireg": "^0.1.8",
+    "requireg": "^0.2.2",
     "resq": "^1.7.0",
     "semver": "^6.2.0",
     "sprintf-js": "^1.1.1"
@@ -107,9 +107,9 @@
     "mocha-parallel-tests": "^2.2.2",
     "nightmare": "^3.0.2",
     "nodemon": "^1.19.4",
-    "playwright": "^0.11.0",
+    "playwright": "^0.12.1",
     "protractor": "^5.4.1",
-    "puppeteer": "^1.20.0",
+    "puppeteer": "^2.1.1",
     "qrcode-terminal": "^0.12.0",
     "rosie": "^1.6.0",
     "runio.js": "^1.0.20",
@@ -120,7 +120,7 @@
     "tsd-jsdoc": "^2.3.0",
     "typescript": "^2.9.2",
     "wdio-docker-service": "^1.5.0",
-    "webdriverio": "^5.18.4",
+    "webdriverio": "^6.0.4",
     "xmldom": "^0.1.31",
     "xpath": "0.0.27"
   },


Test patch:
diff --git a/.circleci/test.sh b/.circleci/test.sh
index 04032c32..e08fcea7 100755
--- a/.circleci/test.sh
+++ b/.circleci/test.sh
@@ -9,4 +9,4 @@ docker-compose run --rm test-rest &&
 docker-compose run --rm test-acceptance.webdriverio &&
 docker-compose run --rm test-acceptance.nightmare &&
 docker-compose run --rm test-acceptance.puppeteer &&
-docker-compose run --rm test-acceptance.protractor &&
+docker-compose run --rm test-acceptance.protractor
\ No newline at end of file
diff --git a/test/acceptance/codecept.Playwright.js b/test/acceptance/codecept.Playwright.js
index 9c38d3ba..2d952ca9 100644
--- a/test/acceptance/codecept.Playwright.js
+++ b/test/acceptance/codecept.Playwright.js
@@ -11,6 +11,10 @@ module.exports.config = {
       show: false,
       browser: process.env.BROWSER || 'chromium',
     },
+    ScreenshotSessionHelper: {
+      require: '../support/ScreenshotSessionHelper.js',
+      outputPath: 'test/acceptance/output',
+    },
   },
   include: {},
   bootstrap: false,
diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js
index 56badbc6..9ae9c5d4 100644
--- a/test/acceptance/codecept.Puppeteer.js
+++ b/test/acceptance/codecept.Puppeteer.js
@@ -15,6 +15,10 @@ module.exports.config = {
         ],
       },
     },
+    ScreenshotSessionHelper: {
+      require: '../support/ScreenshotSessionHelper.js',
+      outputPath: './output',
+    },
     MockRequest: {},
   },
   include: {},
diff --git a/test/acceptance/gherkin/shadow_dom.feature b/test/acceptance/gherkin/shadow_dom.feature
new file mode 100644
index 00000000..9ed511b1
--- /dev/null
+++ b/test/acceptance/gherkin/shadow_dom.feature
@@ -0,0 +1,10 @@
+@WebDriverIO @bdd
+Feature: Web Components Shadow Dom Elements
+
+  In order to achieve my goals
+  As a persona
+  I want to be able to interact with a shadow dom
+
+  Scenario: Interacts with Shadow DOM elements in Web Componenets
+    Given I opened "https://recipes.lwc.dev" website
+    Then I should be able to fill the value in Hello Binding Shadow Input Element
diff --git a/test/acceptance/gherkin/steps.js b/test/acceptance/gherkin/steps.js
index 006fbca7..dba8a68f 100644
--- a/test/acceptance/gherkin/steps.js
+++ b/test/acceptance/gherkin/steps.js
@@ -16,3 +16,11 @@ Then('I should see {string}', (str) => {
   // From "gherkin/basic.feature" {"line":10,"column":5}
   I.see(str);
 });
+
+Given('I opened {string} website', (website) => {
+  I.amOnPage(website);
+});
+
+Then('I should be able to fill the value in Hello Binding Shadow Input Element', (website) => {
+  I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');
+});
diff --git a/test/acceptance/session_test.js b/test/acceptance/session_test.js
index d3f3942f..5b8ad9d0 100644
--- a/test/acceptance/session_test.js
+++ b/test/acceptance/session_test.js
@@ -13,6 +13,38 @@ Scenario('simple session @WebDriverIO @Protractor @Puppeteer @Playwright', (I) =
   I.seeInCurrentUrl('/info');
 });
 
+Scenario('screenshots reflect the current page of current session @Puppeteer @Playwright', async (I) => {
+  const outputPath = await I.getOutputPath();
+
+  I.amOnPage('/');
+  I.saveScreenshot('session_default_1.png');
+
+  session('john', () => {
+    I.amOnPage('/info');
+    I.saveScreenshot('session_john_1.png');
+  });
+
+  I.saveScreenshot('session_default_2.png');
+
+  session('john', () => {
+    I.saveScreenshot('session_john_2.png');
+  });
+
+  const [default1Digest, default2Digest, john1Digest, john2Digest] = await I.getMD5Digests([
+    `${outputPath}/session_default_1.png`,
+    `${outputPath}/session_default_2.png`,
+    `${outputPath}/session_john_1.png`,
+    `${outputPath}/session_john_2.png`,
+  ]);
+
+  // Assert that screenshots of same page in same session are equal
+  assert.equal(default1Digest, default2Digest);
+  assert.equal(john1Digest, john2Digest);
+
+  // Assert that screenshots of different pages in different sessions are not equal
+  assert.notEqual(default1Digest, john1Digest);
+});
+
 Scenario('Different cookies for different sessions @WebDriverIO @Protractor @Playwright @Puppeteer', async (I) => {
   const cookiePage = 'https://www.microsoft.com/en-au/';
   const cookieName = 'MUID';
diff --git a/test/data/app/view/form/wait_for_clickable.php b/test/data/app/view/form/wait_for_clickable.php
index 4ca55095..a22d16b7 100644
--- a/test/data/app/view/form/wait_for_clickable.php
+++ b/test/data/app/view/form/wait_for_clickable.php
@@ -1,4 +1,15 @@
 <html>
+<head>
+<script>
+ function delay() {
+  setTimeout(() => {
+      const button = document.getElementById('publish_button');
+      button.removeAttribute('disabled');
+      button.classList.add('ooops');
+      }, 500);
+ }
+</script>
+</head>
 <body>
 <style>
 #notInViewportTop {
@@ -30,5 +41,13 @@
   <button id="div2_button" type="button" name="button1" value="first">Second Button</button>
 </div>
 
+<div id="save_button" style="position:absolute; top:300; left:0;">
+  <button id="div2_button" type="button" name="button_save" value="first" onclick="delay()">SAVE</button>
+</div>
+
+<div id="some" style="position:absolute; top:400; left:0;">
+  <button class="some" id="publish_button" type="button" name="button_publish" disabled value="first">PUBLISH</button>
+</div>
+
 </body>
 </html>
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
new file mode 100644
index 00000000..4fb9dc81
--- /dev/null
+++ b/test/data/sandbox/configs/allure/before_suite_test_failed.conf.js
@@ -0,0 +1,16 @@
+exports.config = {
+  tests: './before_suite_test_failed.js',
+  timeout: 10000,
+  output: './output/failed',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  plugins: {
+    allure: {
+      enabled: true,
+    },
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/allure/before_suite_test_failed.js b/test/data/sandbox/configs/allure/before_suite_test_failed.js
new file mode 100644
index 00000000..1a6c9d31
--- /dev/null
+++ b/test/data/sandbox/configs/allure/before_suite_test_failed.js
@@ -0,0 +1,14 @@
+Feature('failing setup test suite');
+
+let number;
+BeforeSuite(async () => {
+  throw new Error('the before suite setup failed');
+});
+
+Scenario('failing setup test 1', async I => {
+  I.say('Test was fine.');
+});
+
+Scenario('failing setup test 2', async I => {
+  I.say('Test was fine.');
+});
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 9f1f29ae..9e61d998 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -921,6 +921,22 @@ describe('Puppeteer', function () {
         e.message.should.include('element {css: #div1_button} still not clickable after 0.1 sec');
       });
     });
+
+    it('should pass if element change class', async () => {
+      await I.amOnPage('/form/wait_for_clickable');
+      await I.click('button_save');
+      await I.waitForClickable('//button[@name="button_publish"]');
+    });
+
+    it('should fail if element change class and not clickable', async () => {
+      await I.amOnPage('/form/wait_for_clickable');
+      await I.click('button_save');
+      I.waitForClickable('//button[@name="button_publish"]', 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element //button[@name="button_publish"] still not clickable after 0.1 sec');
+      });
+    });
   });
 });
 
diff --git a/test/helper/WebDriverIO_test.js b/test/helper/WebDriverIO_test.js
index 7aedfb94..631691da 100644
--- a/test/helper/WebDriverIO_test.js
+++ b/test/helper/WebDriverIO_test.js
@@ -7,7 +7,6 @@ const WebDriverIO = require('../../lib/helper/WebDriverIO');
 
 let wd;
 const siteUrl = TestHelper.siteUrl();
-const fileExists = require('../../lib/utils').fileExists;
 const AssertionFailedError = require('../../lib/assert/error');
 const webApiTests = require('./webapi');
 
@@ -145,6 +144,98 @@ describe('WebDriverIO', function () {
     });
   });
 
+  describe('#waitForClickable', () => {
+    it('should wait for clickable', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: 'input#text' });
+    });
+
+    it('should wait for clickable by XPath', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ xpath: './/input[@id="text"]' });
+    });
+
+    it('should fail for disabled element', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#button' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element #button still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for disabled element by XPath', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ xpath: './/button[@id="button"]' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element .//button[@id="button"] still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for element not in viewport by top', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#notInViewportTop' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element {css: #notInViewportTop} still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for element not in viewport by bottom', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#notInViewportBottom' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element #notInViewportBottom still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for element not in viewport by left', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#notInViewportLeft' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element #notInViewportLeft still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for element not in viewport by right', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#notInViewportRight' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element #notInViewportRight still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should fail for overlapping element', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.waitForClickable({ css: '#div2_button' }, 0.1);
+      await wd.waitForClickable({ css: '#div1_button' }, 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element #div1_button still not clickable after 0.1 sec');
+      });
+    });
+
+    it('should pass if element change class', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.click('button_save');
+      await wd.waitForClickable('//button[@name="button_publish"]');
+    });
+
+    it('should fail if element change class and not clickable', async () => {
+      await wd.amOnPage('/form/wait_for_clickable');
+      await wd.click('button_save');
+      wd.waitForClickable('//button[@name="button_publish"]', 0.1).then((isClickable) => {
+        if (isClickable) throw new Error('Element is clickable, but must be unclickable');
+      }).catch((e) => {
+        e.message.should.include('element //button[@name="button_publish"] still not clickable after 0.1 sec');
+      });
+    });
+  });
+
   describe('#seeInSource, #grabSource', () => {
     it('should check for text to be in HTML source', () => wd.amOnPage('/')
       .then(() => wd.seeInSource('<title>TestEd Beta 2.0</title>'))
diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js
index 7b5e3027..85ba770d 100644
--- a/test/rest/REST_test.js
+++ b/test/rest/REST_test.js
@@ -128,6 +128,27 @@ describe('REST', () => {
       response.config.headers.should.have.property('HTTP_X_REQUESTED_WITH');
       response.config.headers.HTTP_X_REQUESTED_WITH.should.eql('xmlhttprequest');
     });
+
+    it('should set Content-Type header if data is string and Content-Type is omitted', async () => {
+      const response = await I.sendPostRequest(
+        '/user',
+        'string of data',
+      );
+
+      response.config.headers.should.have.property('Content-Type');
+      response.config.headers['Content-Type'].should.eql('application/x-www-form-urlencoded');
+    });
+
+    it('should respect any passsed in Content-Type header', async () => {
+      const response = await I.sendPostRequest(
+        '/user',
+        'bad json data',
+        { 'Content-Type': 'application/json' },
+      );
+
+      response.config.headers.should.have.property('Content-Type');
+      response.config.headers['Content-Type'].should.eql('application/json');
+    });
   });
 
   describe('_url autocompletion', () => {
diff --git a/test/runner/allure_test.js b/test/runner/allure_test.js
index bd4447e6..929ba813 100644
--- a/test/runner/allure_test.js
+++ b/test/runner/allure_test.js
@@ -7,18 +7,22 @@ const { deleteDir } = require('../../lib/utils');
 const runner = path.join(__dirname, '/../../bin/codecept.js');
 const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/allure');
 const codecept_run = `${runner} run`;
+const codecept_workers = `${runner} run-workers 2`;
 const codecept_run_config = (config, grep) => `${codecept_run} --config ${codecept_dir}/${config} ${grep ? `--grep ${grep}` : ''}`;
+const codecept_workers_config = (config, grep) => `${codecept_workers} --config ${codecept_dir}/${config} ${grep ? `--grep ${grep}` : ''}`;
 
 
 describe('CodeceptJS Allure Plugin', () => {
   beforeEach(() => {
     deleteDir(path.join(codecept_dir, 'output/ansi'));
     deleteDir(path.join(codecept_dir, 'output/success'));
+    deleteDir(path.join(codecept_dir, 'output/failed'));
   });
 
   afterEach(() => {
     deleteDir(path.join(codecept_dir, 'output/ansi'));
     deleteDir(path.join(codecept_dir, 'output/success'));
+    deleteDir(path.join(codecept_dir, 'output/failed'));
   });
 
   it('should enable allure reports', (done) => {
@@ -39,4 +43,32 @@ describe('CodeceptJS Allure Plugin', () => {
       done();
     });
   });
+
+  it('should report BeforeSuite errors when executing via run command', (done) => {
+    exec(codecept_run_config('before_suite_test_failed.conf.js'), (err, stdout) => {
+      stdout.should.include('FAIL  | 0 passed, 1 failed');
+
+      const files = fs.readdirSync(path.join(codecept_dir, 'output/failed'));
+      const testResultPath = files[0];
+      assert(testResultPath.match(/\.xml$/), 'not a xml file');
+      const file = fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8');
+      file.should.include('BeforeSuite of suite failing setup test suite: failed.');
+      file.should.include('the before suite setup failed');
+      done();
+    });
+  });
+
+  it('should report BeforeSuite errors when executing via run-workers command', (done) => {
+    exec(codecept_workers_config('before_suite_test_failed.conf.js'), (err, stdout) => {
+      stdout.should.include('FAIL  | 0 passed, 1 failed');
+
+      const files = fs.readdirSync(path.join(codecept_dir, 'output/failed'));
+      const testResultPath = files[0];
+      assert(testResultPath.match(/\.xml$/), 'not a xml file');
+      const file = fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8');
+      file.should.include('BeforeSuite of suite failing setup test suite: failed.');
+      file.should.include('the before suite setup failed');
+      done();
+    });
+  });
 });
diff --git a/test/support/ScreenshotSessionHelper.js b/test/support/ScreenshotSessionHelper.js
new file mode 100644
index 00000000..49d82d2d
--- /dev/null
+++ b/test/support/ScreenshotSessionHelper.js
@@ -0,0 +1,39 @@
+const Helper = codecept_helper;
+
+const crypto = require('crypto');
+const fs = require('fs');
+
+class ScreenshotSessionHelper extends Helper {
+  _finishTest() {
+    // Cleanup screenshots created by session screenshot test
+    const screenshotDir = fs.readdirSync(this.outputPath, { withFileTypes: true })
+      .filter(item => item.isFile() && item.name.includes('session'));
+
+    screenshotDir.forEach(file => fs.unlinkSync(`${this.outputPath}/${file.name}`));
+  }
+
+  constructor(config) {
+    super(config);
+    this.outputPath = config.outputPath;
+  }
+
+  getMD5Digests(files = []) {
+    const digests = [];
+
+    for (const file of files) {
+      const hash = crypto.createHash('md5');
+      const data = fs.readFileSync(file);
+      hash.update(data);
+
+      digests.push(hash.digest('base64'));
+    }
+
+    return digests;
+  }
+
+  getOutputPath() {
+    return this.outputPath;
+  }
+}
+
+module.exports = ScreenshotSessionHelper;