Repository: Codeception/CodeceptJS
Pull request #1632 (2019-05-01 05:17:20)

Update puppeteer.md
Update puppeteer.md


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c696ecfa..e7f694df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+## 2.1.3
+
+* Fixed autoLogin plugin to inject `login` function
+* Fixed using `toString()` in DataTablewhen it is defined by @tsuemura
+
+## 2.1.2
+
+* Fixed `inject` to load objects recursively.
+* Fixed TypeScript definitions for locators by @LukoyanovE
+* **EXPERIMENTAL** [WebDriver] ReactJS locators support with webdriverio v5.8+:
+
+```js
+// locating React element by name, prop, state
+I.click({ react: 'component-name', props: {}, state: {} });
+I.seeElement({ react: 'component-name', props: {}, state: {} });
+```
+
+## 2.1.1
+
+* Do not retry `within` and `session` calls inside `retryFailedStep` plugin. Fix by @tsuemura
+
 ## 2.1.0
 
 * Added global `inject()` function to require actor and page objects using dependency injection. Recommended to use in page objects, step definition files, support objects:
diff --git a/bin/codecept.js b/bin/codecept.js
index 5586e171..63df5619 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -36,6 +36,7 @@ program.command('list [path]')
 program.command('def [path]')
   .description('Generates TypeScript definitions for all I actions.')
   .option('-c, --config [file]', 'configuration file to be used')
+  .option('-o, --output [folder]', 'target folder to paste definitions')
   .action(require('../lib/command/definitions'));
 
 program.command('gherkin:init [path]')
diff --git a/docs/acceptance.md b/docs/acceptance.md
index b369712d..632eb42c 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -277,7 +277,7 @@ within({frame: [".content", "#editor"]}, () => {
 
 ## Auto Login
 
-To share the same user session accoross different tests CodeceptJS provides [autoLogin plugin](https://codecept.io/plugins#autologin). It simplifies login management and reduces time consuming login operations. Instead of filling in login form before each test it saves the cookies of a valid user session and reuses it for next tests. If a session expires or doesn't exist, logs in a user again.
+To share the same user session across different tests CodeceptJS provides [autoLogin plugin](https://codecept.io/plugins#autologin). It simplifies login management and reduces time consuming login operations. Instead of filling in login form before each test it saves the cookies of a valid user session and reuses it for next tests. If a session expires or doesn't exist, logs in a user again.
 
 This plugin requires some configuration but is very simple in use:
 
diff --git a/docs/api/recorder.md b/docs/api/recorder.md
index 3fe07549..58c4bfe5 100644
--- a/docs/api/recorder.md
+++ b/docs/api/recorder.md
@@ -14,6 +14,8 @@ Promise description should be passed as first parameter.
 -   `taskName` **any** 
 -   `fn` **any**  (optional, default `undefined`)
 -   `force` **any**  (optional, default `false`)
+-   `retry` **[boolean][1]**     true: it will retries if `retryOpts` set.
+        false: ignore `retryOpts` and won't retry. (optional, default `true`)
 
 ### errHandler
 
@@ -57,3 +59,5 @@ Adds a promise which throws an error into a chain
 ### toString
 
 Get a state of current queue and tasks
+
+[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
diff --git a/docs/basics.md b/docs/basics.md
index c0204b90..c4d9cebe 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -302,8 +302,19 @@ you can generate step definitions with
 codeceptjs def
 ```
 
-Now you should include `/// <reference path="./steps.d.ts" />` into your test files to get
-method autocompletion while writing tests.
+Now you should create `tsconfig.json` in your project root directory.
+
+```tsconfig.json
+{
+  "compilerOptions": {
+    "allowJs": true,
+  }
+}
+```
+but in usually case, this file has already generated when you execute `codeceptjs init`.
+
+Alternatively, you can include `/// <reference path="./steps.d.ts" />` into your test files 
+to get method autocompletion while writing tests.
 
 ## Skipping
 
diff --git a/docs/bdd.md b/docs/bdd.md
index 729a62b9..2c9694ef 100644
--- a/docs/bdd.md
+++ b/docs/bdd.md
@@ -138,11 +138,11 @@ This scenarios are nice as live documentation but they do not test anything yet.
 Steps can be defined by executing `gherkin:snippets` command:
 
 ```bash
-codeceptjs gherkin:snippets
+codeceptjs gherkin:snippets [--path=PATH]
 ```
 
 This will produce code templates for all undefined steps in all feature files of this suite.
-It will also place stub definitions into `step_definitions/steps.js` file.
+It will also place stub definitions into `step_definitions/steps.js` file. However, you may also target a specific file to place all undefined steps in. This file must exist and be placed in the gherkin steps in the current config.
 Our next step will be to define those steps and transforming feature-file into a valid test.
 
 ### Step Definitions
diff --git a/docs/best.md b/docs/best.md
index 7d30a6db..592d95fe 100644
--- a/docs/best.md
+++ b/docs/best.md
@@ -61,7 +61,7 @@ Scenario('editing a metric', async (I, loginAs, metricPage) => {
 
 ## Refactoring and PageObjects
 
-When a project is growing and more and more tests are required, it's time to think about reusing test code accoross the tests. Some common actions should be moved from tests to other files so to be accessible from different tests.
+When a project is growing and more and more tests are required, it's time to think about reusing test code across the tests. Some common actions should be moved from tests to other files so to be accessible from different tests.
 
 Here is a recommended strategy what to store where:
 
diff --git a/docs/changelog.md b/docs/changelog.md
index c696ecfa..3c186ef8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,3 +1,19 @@
+## 2.1.2
+
+* Fixed `inject` to load objects recursively.
+* Fixed TypeScript definitions for locators by @LukoyanovE
+* **EXPERIMENTAL** [WebDriver] ReactJS locators support with webdriverio v5.8+:
+
+```js
+// locating React element by name, prop, state
+I.click({ react: 'component-name', props: {}, state: {} });
+I.seeElement({ react: 'component-name', props: {}, state: {} });
+```
+
+## 2.1.1
+
+* Do not retry `within` and `session` calls inside `retryFailedStep` plugin. Fix by @tsuemura
+
 ## 2.1.0
 
 * Added global `inject()` function to require actor and page objects using dependency injection. Recommended to use in page objects, step definition files, support objects:
diff --git a/docs/commands.md b/docs/commands.md
index c17cb226..869fa3fb 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -146,14 +146,14 @@ codeceptjs def
 codeceptjs def --config path/to/codecept.json
 ```
 
-Produces `steps.d.ts` file, which referenced in the very beginning of a test file.
+After doing that IDE should provide autocompletion for `I` object inside `Scenario` and `within` blocks.
 
-```js
-/// <reference path="./steps.d.ts" />
-Feature('My new test');
-```
+Add optional parameter `output` (or shortcat `-o`), if you want to place your definition file in specific folder:
 
-After doing that IDE should provide autocompletion for `I` object inside `Scenario` and `within` blocks.
+```sh
+codeceptjs def --output ./tests/typings
+codeceptjs def -o ./tests/typings
+```
 
 ## List Commands
 
diff --git a/docs/examples.md b/docs/examples.md
index 90a52fb2..7dce2356 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -9,6 +9,13 @@ title: Examples
 CodeceptJS repo contains basic tests (both failing and passing) just to show how it works.
 Our team uses it to test new features and run simple scenarios.
 
+## [CodeceptJS Quick Start with Cucumber/Gherkin](https://github.com/gkushang/codeceptjs-quick-start)
+
+This repository demonstrate the CodeceptJS with Cucumber/Gherkin configuration
+
+* Easy 3-Steps process to colocate the CodeceptJS with Gherkin tests with your project
+* Quick Start with CodeceptJS wih Cucumber/Gherkin
+* Sample example and feature files to test GitHub Features
 
 ## [Testing Single Page Application](https://github.com/bugiratracker/codeceptjs-demo)
 
@@ -88,3 +95,8 @@ This repository demonstrates usage of
 * Working with Angular CLI
 * Reports with Mochawesome helper
 
+## [REST Example Tests](https://github.com/PeterNgTr/codeceptjs-rest-demo)
+
+This repository demonstrates usage of
+
+* REST helper
diff --git a/docs/helpers/Nightmare.md b/docs/helpers/Nightmare.md
index eb51e396..131f3ae7 100644
--- a/docs/helpers/Nightmare.md
+++ b/docs/helpers/Nightmare.md
@@ -542,7 +542,7 @@ Reload the page
 Reload the current page.
 
 ```js
-`I.refreshPage();
+I.refreshPage();
 ```
 
 ### resizeWindow
@@ -835,6 +835,24 @@ button: "left"
 
 -   `event`  
 
+### uncheckOption
+
+Unselects a checkbox or radio button.
+Element is located by label or name or CSS or XPath.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+I.uncheckOption('#agree');
+I.uncheckOption('I Agree to Terms and Conditions');
+I.uncheckOption('agree', '//form');
+```
+
+#### Parameters
+
+-   `field`  checkbox located by label | name | CSS | XPath | strict locator.
+-   `context`  (optional) element located by CSS | XPath | strict locator.
+
 ### wait
 
 Pauses execution for a number of seconds.
diff --git a/docs/helpers/Protractor.md b/docs/helpers/Protractor.md
index 7f8b3ce9..23af1b00 100644
--- a/docs/helpers/Protractor.md
+++ b/docs/helpers/Protractor.md
@@ -779,7 +779,7 @@ Reloads page
 Reload the current page.
 
 ```js
-`I.refreshPage();
+I.refreshPage();
 ```
 
 ### resetModule
@@ -1163,6 +1163,24 @@ I.switchToPreviousTab(2);
 
 -   `num`   (optional, default `1`)
 
+### uncheckOption
+
+Unselects a checkbox or radio button.
+Element is located by label or name or CSS or XPath.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+I.uncheckOption('#agree');
+I.uncheckOption('I Agree to Terms and Conditions');
+I.uncheckOption('agree', '//form');
+```
+
+#### Parameters
+
+-   `field`  checkbox located by label | name | CSS | XPath | strict locator.
+-   `context`  (optional) element located by CSS | XPath | strict locator.
+
 ### wait
 
 Pauses execution for a number of seconds.
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 18d6371f..774dd7be 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -658,6 +658,29 @@ let url = await I.grabCurrentUrl();
 console.log(`Current URL is [${url}]`);
 ```
 
+### grabDataFromPerformanceTiming
+
+Grab the data from performance timing using Navigation Timing API.
+The returned data will contain following things in ms:
+
+-   responseEnd,
+-   domInteractive,
+-   domContentLoadedEventEnd,
+-   loadEventEnd
+    Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+await I.amOnPage('https://example.com');
+let data = await I.grabDataFromPerformanceTiming();
+//Returned data
+{ // all results are in [ms]
+responseEnd: 23,
+domInteractive: 44,
+domContentLoadedEventEnd: 196,
+loadEventEnd: 241
+}
+```
+
 ### grabHTMLFrom
 
 Retrieves the innerHTML from an element located by CSS or XPath and returns it to test.
@@ -814,7 +837,7 @@ I.pressKey(['Control','a']);
 Reload the current page.
 
 ```js
-`I.refreshPage();
+I.refreshPage();
 ```
 
 ### resizeWindow
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 5803b665..7ee71f85 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -1003,7 +1003,7 @@ I.pressKey(['Control','a']);
 Reload the current page.
 
 ```js
-`I.refreshPage();
+I.refreshPage();
 ```
 
 ### resizeWindow
@@ -1107,7 +1107,7 @@ I.scrollTo('#submit', 5, 5);
 
 -   `locator`  located by CSS|XPath|strict locator.
 -   `offsetX`  (optional) X-axis offset.
--   `offsetY`  (optional) Y-axis offset.-   _Appium_: supported only for web testing
+-   `offsetY`  (optional) Y-axis offset.
 
 ### scrollTo
 
@@ -1123,7 +1123,7 @@ I.scrollTo('#submit', 5, 5);
 
 -   `locator`  located by CSS|XPath|strict locator.
 -   `offsetX`  (optional) X-axis offset.
--   `offsetY`  (optional) Y-axis offset.
+-   `offsetY`  (optional) Y-axis offset.-   _Appium_: supported only for web testing
 
 ### see
 
diff --git a/docs/helpers/WebDriverIO.md b/docs/helpers/WebDriverIO.md
index fd9b4b07..2ab7ff7f 100644
--- a/docs/helpers/WebDriverIO.md
+++ b/docs/helpers/WebDriverIO.md
@@ -875,7 +875,7 @@ I.pressKey(['Control','a']);
 Reload the current page.
 
 ```js
-`I.refreshPage();
+I.refreshPage();
 ```
 
 ### resizeWindow
diff --git a/docs/mobile-react-native-locators.md b/docs/mobile-react-native-locators.md
new file mode 100644
index 00000000..360284db
--- /dev/null
+++ b/docs/mobile-react-native-locators.md
@@ -0,0 +1,64 @@
+## Automating React Native apps
+
+### Problem
+Let's say we have a React Native app with component defined like this
+```html
+<Button testID='someButton'>My button</Button>
+```
+
+If you will try to execute a simple test like
+```js
+I.tap('~someButton')
+```
+it will work correctly on iOS (with XUITest driver), but on Android's UIAutomator2 it will give you an error
+```
+Touch actions like "tap" need at least some kind of position information like "element", "x" or "y" options, you've none given.
+```
+
+This happens because of the way React Native implements `testID` for Android, it puts provided value into the [View tag](https://developer.android.com/reference/android/view/View#tags),
+as could be found in the [source code](https://github.com/facebook/react-native/blob/19a88d7f4addcd9f95fd4908d50db37b3604b5b1/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java#L114).
+But UIAutomator doesn't have any means to work with view tags.
+
+### Solutions
+As many resources suggest (like [here](https://github.com/appium/appium/issues/6025#issuecomment-406141946) or [here](https://github.com/facebook/react-native/issues/7135)),
+you could use `testID` for iOS and `accesibilityLabel` for Android, but `accessibilityLabel` is a user-facing text that's intended for ... accesibility,
+not for UI tests, and you will need to switch it off for production builds.
+
+Another way to solve this issue is to use Espresso driver for Android.
+At first you need to enable Espresso driver for your Android configuration.
+You could do it just by changing `automationName` in the `helpers` section of the config file:
+```js
+{
+  //...
+  helpers: {
+    Appium: {
+      app: '/path/to/apk.apk',
+      platform: 'Android',
+      desiredCapabilities: {
+        automationName: 'Espresso',
+        platformVersion: '9',
+        deviceName: 'Android Emulator'
+      }
+    }
+  }
+  //...
+}
+```
+Then you could locate components using XPath expression:
+```js
+I.tap({android: '//*[@view-tag="someButton"]', ios: '~someButton'})
+```
+This way test would work for both platforms without any changes in code.
+To simplify things further you could write a helper function:
+```js
+function tid(id) {
+  return {
+    android: `//*[@view-tag="${id}"]`,
+    ios: '~' + id
+  }
+}
+```
+Now test will look more concise
+```js
+I.tap(tid('someButton'));
+```
diff --git a/docs/mobile.md b/docs/mobile.md
index 96d253d1..af2769ef 100644
--- a/docs/mobile.md
+++ b/docs/mobile.md
@@ -181,6 +181,7 @@ Accessibility id is recommended to use for locating element, as it rarely change
 * iOS uses [UIAccessibilityIdentification](https://developer.apple.com/documentation/uikit/uiaccessibilityidentification)
 * Android `accessibility id` matches the content-description
 * Web view uses `[aria-label]` attribute as accessibility id
+* ReactNative for Android has some caveats you could find more [here](mobile-react-native-locators.md)
 
 Add `~` prefix to search for element by its accessibility id:
 
diff --git a/docs/plugins.md b/docs/plugins.md
index ae474251..83e88cef 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -257,6 +257,38 @@ plugins: {
 }
 ```
 
+#### Tips: Using async function in the autoLogin
+
+If you use async functions in the autoLogin plugin, login function should be used with `await` keyword.
+
+```js
+autoLogin: {
+  enabled: true,
+  saveToFile: true,
+  inject: 'login',
+  users: {
+    admin: {
+      login: async (I) => {  // If you use async function in the autoLogin plugin
+         const phrase = await I.grabTextFrom('#phrase')
+         I.fillField('username', 'admin'),
+         I.fillField('password', 'password')
+         I.fillField('phrase', phrase)
+      },
+      check: (I) => {
+         I.amOnPage('/');
+         I.see('Admin');
+      }
+    }
+  }
+}
+```
+
+```js
+Scenario('login', async (I, login) => {
+  await login('admin') // you should use `await`
+})
+```
+
 ### Parameters
 
 -   `config`  
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index 4a148a7b..a927927a 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -102,7 +102,7 @@ Tests consist with a scenario of user's action taken on a page. The most widely
 
 *All actions are listed in [helper reference](http://codecept.io/helpers/Puppeteer/).*
 
-All actions whicn 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 **support CSS and XPath locators**. Actions like `click` or `fillField` by locate elements by their name or value on a page:
 
 ```js
 
diff --git a/docs/webapi/grabDataFromPerformanceTiming.mustache b/docs/webapi/grabDataFromPerformanceTiming.mustache
new file mode 100644
index 00000000..99111d92
--- /dev/null
+++ b/docs/webapi/grabDataFromPerformanceTiming.mustache
@@ -0,0 +1,19 @@
+Grab the data from performance timing using Navigation Timing API.
+The returned data will contain following things in ms:
+- responseEnd,
+- domInteractive,
+- domContentLoadedEventEnd,
+- loadEventEnd
+Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+await I.amOnPage('https://example.com');
+let data = await I.grabDataFromPerformanceTiming();
+//Returned data
+{ // all results are in [ms]
+  responseEnd: 23,
+  domInteractive: 44,
+  domContentLoadedEventEnd: 196,
+  loadEventEnd: 241
+}
+```
\ No newline at end of file
diff --git a/docs/webapi/refreshPage.mustache b/docs/webapi/refreshPage.mustache
index b4973a3a..9277fd25 100644
--- a/docs/webapi/refreshPage.mustache
+++ b/docs/webapi/refreshPage.mustache
@@ -1,5 +1,5 @@
 Reload the current page.
 
-````js
-`I.refreshPage();
-````
+```js
+I.refreshPage();
+```
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index fdfd5eea..ca456bc7 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -9,35 +9,38 @@ const fs = require('fs');
 const path = require('path');
 
 const template = `
-type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}) => void;
+type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}, ...args: any) => void;
 
 declare class FeatureConfig {
-  retry(times:number): FeatureConfig
-  timeout(seconds:number): FeatureConfig
-  config(config:object): FeatureConfig
-  config(helperName:string, config:object): FeatureConfig
+  retry(times: number): FeatureConfig
+  timeout(seconds: number): FeatureConfig
+  config(config: object): FeatureConfig
+  config(helperName: string, config: object): FeatureConfig
 }
 
 declare class ScenarioConfig {
-  throws(err:any) : ScenarioConfig;
+  throws(err: any) : ScenarioConfig;
   fails() : ScenarioConfig;
-  retry(times:number): ScenarioConfig
-  timeout(timeout:number): ScenarioConfig
-  inject(inject:object): ScenarioConfig
-  config(config:object): ScenarioConfig
-  config(helperName:string, config:object): ScenarioConfig
+  retry(times: number): ScenarioConfig
+  timeout(timeout: number): ScenarioConfig
+  inject(inject: object): ScenarioConfig
+  config(config: object): ScenarioConfig
+  config(helperName: string, config: object): ScenarioConfig
+  injectDependencies(dependencies: { [key: string]: any }): ScenarioConfig
 }
 
 interface ILocator {
+  id?: string;
   xpath?: string;
   css?: string;
   name?: string;
-  value?: string;
   frame?: string;
   android?: string;
   ios?: string;
 }
 
+type LocatorOrString = string | ILocator | Locator;
+
 declare class Helper {
   /** Abstract method to provide required config options */
   static _config(): any;
@@ -78,28 +81,22 @@ declare class Helper {
   debugSection(section: string, msg: string): void
 }
 
-declare class Locator implements ILocator {
-  xpath?: string;
-  css?: string;
-  name?: string;
-  value?: string;
-  frame?: string;
-  android?: string;
-  ios?: string;
+declare class Locator {
+  constructor(locator: LocatorOrString, defaultType?: string);
 
-  or(locator:string): Locator;
-  find(locator:string): Locator;
-  withChild(locator:string): Locator;
-  find(locator:string): Locator;
-  at(position:number): Locator;
+  or(locator: LocatorOrString): Locator;
+  find(locator: LocatorOrString): Locator;
+  withChild(locator: LocatorOrString): Locator;
+  withDescendant(locator: LocatorOrString): Locator;
+  at(position: number): Locator;
   first(): Locator;
   last(): Locator;
-  inside(locator:string): Locator;
-  before(locator:string): Locator;
-  after(locator:string): Locator;
-  withText(locator:string): Locator;
-  withAttr(locator:object): Locator;
-  as(locator:string): Locator;
+  inside(locator: LocatorOrString): Locator;
+  before(locator: LocatorOrString): Locator;
+  after(locator: LocatorOrString): Locator;
+  withText(text: string): Locator;
+  withAttr(attrs: object): Locator;
+  as(output: string): Locator;
 }
 
 
@@ -114,24 +111,31 @@ declare const Scenario: {
   only(title: string, callback: ICodeceptCallback): ScenarioConfig;
   only(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
 }
+declare interface IScenario {
+  Scenario(title: string, callback: ICodeceptCallback): ScenarioConfig;
+  Scenario(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
+}
 declare function xScenario(title: string, callback: ICodeceptCallback): ScenarioConfig;
 declare function xScenario(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
-declare function Data(data: any): any;
-declare function xData(data: any): any;
+declare function Data(data: any): IData;
+declare interface IData {
+  Scenario(title: string, callback: ICodeceptCallback): ScenarioConfig;
+  Scenario(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
+  only: IScenario;
+}
+declare function xData(data: any): IScenario;
 declare function Before(callback: ICodeceptCallback): void;
 declare function BeforeSuite(callback: ICodeceptCallback): void;
 declare function After(callback: ICodeceptCallback): void;
 declare function AfterSuite(callback: ICodeceptCallback): void;
 
-declare function locate(selector: string): Locator;
-declare function locate(selector: ILocator): Locator;
-declare function within(selector: string, callback: Function): Promise<any>;
-declare function within(selector: ILocator, callback: Function): Promise<any>;
-declare function session(selector: string, callback: Function): Promise<any>;
-declare function session(selector: ILocator, callback: Function): Promise<any>;
-declare function session(selector: string, config: any, callback: Function): Promise<any>;
-declare function session(selector: ILocator, config: any, callback: Function): Promise<any>;
+declare function inject(): { [key: string]: any };
+declare function locate(selector: LocatorOrString): Locator;
+declare function within(selector: LocatorOrString, callback: Function): Promise<any>;
+declare function session(selector: LocatorOrString, callback: Function): Promise<any>;
+declare function session(selector: LocatorOrString, config: any, callback: Function): Promise<any>;
 declare function pause(): void;
+declare function secret(secret: any): string;
 
 declare const codeceptjs: any;
 
@@ -143,7 +147,7 @@ declare namespace CodeceptJS {
 }
 
 declare module "codeceptjs" {
-    export = CodeceptJS;
+  export = CodeceptJS;
 }
 `;
 
@@ -159,6 +163,8 @@ module.exports = function (genPath, options) {
   const config = getConfig(configFile);
   if (!config) return;
 
+  const targetFolderPath = options.output && getTestRoot(options.output) || testsPath;
+
   const codecept = new Codecept(config, {});
   codecept.init(testsPath);
 
@@ -188,20 +194,14 @@ module.exports = function (genPath, options) {
 
   let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
   definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
-  if (callbackParams.length > 0) {
-    definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${callbackParams.join(', ')}`);
-  } else {
-    definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', '');
-  }
+  definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${callbackParams.join(', ')}`);
   if (translations) {
     definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, translations.I);
   }
 
-  fs.writeFileSync(path.join(testsPath, 'steps.d.ts'), definitionsTemplate);
+  fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsTemplate);
   output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs');
   output.print('Definitions were generated in steps.d.ts');
-  output.print('Load them by adding at the top of a test file:');
-  output.print(output.colors.grey('\n/// <reference path="./steps.d.ts" />'));
 };
 
 function addAllMethodsInObject(supportObj, actions, methods, translations) {
diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js
index 6b735c4b..1520c86e 100644
--- a/lib/command/gherkin/snippets.js
+++ b/lib/command/gherkin/snippets.js
@@ -34,6 +34,10 @@ module.exports = function (genPath, options) {
     output.error('No gherkin features defined in config. Exiting');
     process.exit(1);
   }
+  if (options.path && !config.gherkin.steps.includes(options.path)) {
+    output.error(`You must include ${options.path} to the gherkin steps in your config file`);
+    process.exit(1);
+  }
 
   const files = [];
   glob.sync(config.gherkin.features, { cwd: global.codecept_dir }).forEach((file) => {
@@ -81,7 +85,12 @@ module.exports = function (genPath, options) {
 
   files.forEach(file => parseFile(file));
 
-  let stepFile = config.gherkin.steps[0];
+  let stepFile = options.path || config.gherkin.steps[0];
+  if (!fs.existsSync(stepFile)) {
+    output.error('Please enter a valid step file path ', stepFile);
+    process.exit(1);
+  }
+
   if (!fsPath.isAbsolute(stepFile)) {
     stepFile = fsPath.join(global.codecept_dir, stepFile);
   }
diff --git a/lib/command/init.js b/lib/command/init.js
index ce6889ae..5751bbac 100644
--- a/lib/command/init.js
+++ b/lib/command/init.js
@@ -161,6 +161,20 @@ module.exports = function (initPath) {
         success(`Steps file created at ${stepFile}`);
       }
 
+      const tsconfig = {
+        compilerOption: {
+          allowJs: true,
+        },
+      };
+      const tsconfigJson = beautify(JSON.stringify(tsconfig));
+      const tsconfigFile = path.join(testsPath, 'tsconfig.json');
+      if (fileExists(tsconfigFile)) {
+        print(`tsconfig.json has already exists at ${tsconfigFile}`);
+      } else {
+        fs.writeFileSync(tsconfigFile, tsconfigJson);
+        success(`TypeScript project configuration file created at ${tsconfigFile}`);
+      }
+
       fs.writeFileSync(configFile, beautify(`exports.config = ${inspect(config, false, 4, false)}`), 'utf-8');
       success(`Config created at ${configFile}`);
 
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index c9d3fbd0..64570495 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -3,6 +3,7 @@ const {
 } = require('./utils');
 const fork = require('child_process').fork;
 const path = require('path');
+const crypto = require('crypto');
 const runHook = require('../hooks');
 const event = require('../event');
 const collection = require('./run-multiple/collection');
@@ -121,7 +122,9 @@ function executeRun(runName, runConfig) {
   if (browserConfig.outputName) {
     outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
   } else {
-    outputDir += JSON.stringify(browserConfig).replace(/[^\d\w]+/g, '_');
+    const hash = crypto.createHash('md5');
+    hash.update(JSON.stringify(browserConfig));
+    outputDir += hash.digest('hex');
   }
   outputDir += `_${runId}`;
 
diff --git a/lib/container.js b/lib/container.js
index 0bce63e1..c6f5ee94 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -162,18 +162,11 @@ function createHelpers(config) {
 
 function createSupportObjects(config) {
   const objects = {};
+
   for (const name in config) {
-    objects[name] = getSupportObject(config, name);
-    try {
-      if (typeof objects[name] === 'function') {
-        objects[name] = objects[name]();
-      } else if (objects[name]._init) {
-        objects[name]._init();
-      }
-    } catch (err) {
-      throw new Error(`Initialization failed for ${objects[name]}\n${err.message}`);
-    }
+    objects[name] = {}; // placeholders
   }
+
   if (!objects.I) {
     objects.I = require('./actor')();
 
@@ -182,6 +175,21 @@ function createSupportObjects(config) {
     }
   }
 
+  container.support = objects;
+
+  for (const name in config) {
+    let newObj = getSupportObject(config, name);
+    try {
+      if (typeof newObj === 'function') {
+        newObj = newObj();
+      } else if (newObj._init) {
+        newObj._init();
+      }
+    } catch (err) {
+      throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}`);
+    }
+    Object.assign(objects[name], newObj);
+  }
   const asyncWrapper = function (f) {
     return function () {
       return f.apply(this, arguments).catch((e) => {
diff --git a/lib/data/context.js b/lib/data/context.js
index 13c3c989..603edcef 100644
--- a/lib/data/context.js
+++ b/lib/data/context.js
@@ -58,6 +58,13 @@ function replaceTitle(title, dataRow) {
     return `${title} | {${JSON.stringify(dataRow.data)}}`;
   }
 
+  // if `dataRow` is object and has own `toString()` method,
+  // it should be printed
+  if (Object.prototype.toString.call(dataRow.data) === (Object()).toString()
+    && dataRow.data.toString() !== (Object()).toString()) {
+    return `${title} | ${dataRow.data}`;
+  }
+
   return `${title} | ${JSON.stringify(dataRow.data)}`;
 }
 
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index 3ea50d2d..e00b56a3 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -669,6 +669,16 @@ class Nightmare extends Helper {
       .wait(this.options.waitForAction);
   }
 
+  /**
+   * {{> ../webapi/uncheckOption }}
+   */
+  async uncheckOption(field, context = null) {
+    const els = await findCheckable.call(this, field, context);
+    assertElementExists(els[0], field, 'Checkbox or radio');
+    return this.browser.evaluate(els => window.codeceptjs.unCheckEl(els[0]), els)
+      .wait(this.options.waitForAction);
+  }
+
   /**
    * {{> ../webapi/fillField }}
    */
diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js
index d5a91350..69ef2999 100644
--- a/lib/helper/Protractor.js
+++ b/lib/helper/Protractor.js
@@ -657,6 +657,22 @@ class Protractor extends Helper {
     if (!isSelected) return els[0].click();
   }
 
+  /**
+   * {{> ../webapi/uncheckOption }}
+   */
+  async uncheckOption(field, context = null) {
+    let matcher = this.browser;
+    if (context) {
+      const els = await this._locate(context, true);
+      assertElementExists(els, context);
+      matcher = els[0];
+    }
+    const els = await findCheckable(matcher, field);
+    assertElementExists(els, field, 'Checkbox or radio');
+    const isSelected = await els[0].isSelected();
+    if (isSelected) return els[0].click();
+  }
+
   /**
    * {{> ../webapi/seeCheckboxIsChecked }}
    */
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index c48ee81b..4223411b 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -30,6 +30,7 @@ const fs = require('fs');
 
 
 let puppeteer;
+let perfTiming;
 const popupStore = new Popup();
 const consoleLogStore = new Console();
 
@@ -488,6 +489,17 @@ class Puppeteer extends Helper {
     this.context = await this.page.mainFrame().$('body');
   }
 
+  _extractDataFromPerformanceTiming(timing, ...dataNames) {
+    const navigationStart = timing.navigationStart;
+
+    const extractedData = {};
+    dataNames.forEach((name) => {
+      extractedData[name] = timing[name] - navigationStart;
+    });
+
+    return extractedData;
+  }
+
   /**
    * {{> ../webapi/amOnPage }}
    */
@@ -496,6 +508,17 @@ class Puppeteer extends Helper {
       url = this.options.url + url;
     }
     await this.page.goto(url, { waitUntil: this.options.waitForNavigation });
+
+    const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)));
+
+    perfTiming = this._extractDataFromPerformanceTiming(
+      performanceTiming,
+      'responseEnd',
+      'domInteractive',
+      'domContentLoadedEventEnd',
+      'loadEventEnd',
+    );
+
     return this._waitForAction();
   }
 
@@ -1887,6 +1910,13 @@ class Puppeteer extends Helper {
   async _waitForAction() {
     return this.wait(this.options.waitForAction / 1000);
   }
+
+  /**
+   * {{> ../webapi/grabDataFromPerformanceTiming }}
+   */
+  async grabDataFromPerformanceTiming() {
+    return perfTiming;
+  }
 }
 
 module.exports = Puppeteer;
@@ -2181,7 +2211,11 @@ function $XPath(element, selector) {
 function targetCreatedHandler(page) {
   if (!page) return;
   this.withinLocator = null;
-  page.on('load', frame => this.context = page.$('body'));
+  page.on('load', (frame) => {
+    page.$('body')
+      .catch(() => null)
+      .then(context => this.context = context);
+  });
   page.on('console', (msg) => {
     this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '));
     consoleLogStore.add(msg);
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index 4deeb517..dd44069c 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -101,7 +101,7 @@ class REST extends Helper {
    * @param {*} url
    */
   _url(url) {
-    return url.indexOf('http') === -1 ? this.options.endpoint + url : url;
+    return /^\w+\:\/\//.test(url) ? url : this.options.endpoint + url;
   }
 
   /**
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index c71bd795..487d9eb8 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -353,7 +353,7 @@ class WebDriver extends Helper {
     super(config);
     webdriverio = requireg('webdriverio');
     if (webdriverio.VERSION && webdriverio.VERSION.indexOf('4') === 0) {
-      throw new Error('This helper is compatible with "webdriverio@5". Please upgrade webdriverio to v5+ or use WebDriverIO helper instead');
+      throw new Error(`This helper is compatible with "webdriverio@5". Current version: ${webdriverio.VERSION}. Please upgrade webdriverio to v5+ or use WebDriverIO helper instead`);
     }
     // set defaults
     this.root = webRoot;
@@ -490,12 +490,8 @@ class WebDriver extends Helper {
       await this.defineTimeout(this.options.timeouts);
     }
 
-    if (this.isWeb && this.options.windowSize === 'maximize') {
-      await this.resizeWindow('maximize');
-    } else if (this.isWeb && this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
-      const dimensions = this.options.windowSize.split('x');
-      await this.resizeWindow(dimensions[0], dimensions[1]);
-    }
+    await this._resizeWindowIfNeeded(this.browser, this.options.windowSize);
+
     this.$$ = this.browser.$$.bind(this.browser);
     return this.browser;
   }
@@ -547,7 +543,15 @@ class WebDriver extends Helper {
         // opts.disableScreenshots = true; // screenshots cant be saved as session will be already closed
         opts = this._validateConfig(Object.assign(this.options, opts));
         this.debugSection('New Browser', JSON.stringify(opts));
-        return webdriverio.remote(opts);
+        const browser = await webdriverio.remote(opts);
+
+        if (opts.timeouts && this.isWeb) {
+          await this._defineBrowserTimeout(browser, opts.timeouts);
+        }
+
+        await this._resizeWindowIfNeeded(browser, opts.windowSize);
+
+        return browser;
       },
       stop: async (browser) => {
         return browser.deleteSession();
@@ -606,10 +610,19 @@ class WebDriver extends Helper {
    * this.helpers['WebDriver']._locate({name: 'password'}).then //...
    * ```
    *
+   *
    * @param locator element located by CSS|XPath|strict locator.
    */
   async _locate(locator, smartWait = false) {
     if (require('../store').debugMode) smartWait = false;
+
+    // special locator type for React
+    if (locator.react) {
+      const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined);
+      this.debugSection('Elements', `Found ${els.length} react components`);
+      return els;
+    }
+
     if (!this.options.smartWait || !smartWait) {
       const els = await this.$$(withStrictLocator(locator));
       return els;
@@ -676,7 +689,11 @@ class WebDriver extends Helper {
    * @param timeouts WebDriver timeouts object.
    */
   defineTimeout(timeouts) {
-    return this.browser.setTimeout(timeouts);
+    return this._defineBrowserTimeout(this.browser, timeouts);
+  }
+
+  _defineBrowserTimeout(browser, timeouts) {
+    return browser.setTimeout(timeouts);
   }
 
   /**
@@ -876,7 +893,7 @@ class WebDriver extends Helper {
     const elementId = getElementId(elem);
 
     const isSelected = await this.browser.isElementSelected(elementId);
-    if (isSelected.value) return Promise.resolve(true);
+    if (isSelected) return Promise.resolve(true);
     return this.browser[clickMethod](elementId);
   }
 
@@ -895,7 +912,7 @@ class WebDriver extends Helper {
     const elementId = getElementId(elem);
 
     const isSelected = await this.browser.isElementSelected(elementId);
-    if (!isSelected.value) return Promise.resolve(true);
+    if (!isSelected) return Promise.resolve(true);
     return this.browser[clickMethod](elementId);
   }
 
@@ -1092,8 +1109,8 @@ class WebDriver extends Helper {
    * * *Appium*: supported
    */
   async seeElement(locator) {
-    const res = await this._locate(withStrictLocator(locator), true);
-    assertElementExists(res);
+    const res = await this._locate(locator, true);
+    assertElementExists(res, locator);
     const selected = await forEachAsync(res, async el => el.isDisplayed());
     return truth(`elements of ${locator}`, 'to be seen').assert(selected);
   }
@@ -1105,7 +1122,7 @@ class WebDriver extends Helper {
    * * *Appium*: supported
    */
   async dontSeeElement(locator) {
-    const res = await this._locate(withStrictLocator(locator), false);
+    const res = await this._locate(locator, false);
     if (!res || res.length === 0) {
       return truth(`elements of ${locator}`, 'to be seen').negate(false);
     }
@@ -1604,15 +1621,28 @@ class WebDriver extends Helper {
    * Appium: not tested in web, in apps doesn't work
    */
   async resizeWindow(width, height) {
+    return this._resizeBrowserWindow(this.browser, width, height);
+  }
+
+  async _resizeBrowserWindow(browser, width, height) {
     if (width === 'maximize') {
-      const size = await this.browser.maximizeWindow();
+      const size = await browser.maximizeWindow();
       this.debugSection('Window Size', size);
       return;
     }
-    if (this.browser.isW3C) {
-      return this.browser.setWindowRect(null, null, parseInt(width, 10), parseInt(height, 10));
+    if (browser.isW3C) {
+      return browser.setWindowRect(null, null, parseInt(width, 10), parseInt(height, 10));
+    }
+    return browser.setWindowSize(parseInt(width, 10), parseInt(height, 10));
+  }
+
+  async _resizeWindowIfNeeded(browser, windowSize) {
+    if (this.isWeb && windowSize === 'maximize') {
+      await this._resizeBrowserWindow(browser, 'maximize');
+    } else if (this.isWeb && windowSize && windowSize.indexOf('x') > 0) {
+      const dimensions = windowSize.split('x');
+      await this._resizeBrowserWindow(browser, dimensions[0], dimensions[1]);
     }
-    return this.browser.setWindowSize(parseInt(width, 10), parseInt(height, 10));
   }
 
   /**
@@ -2193,7 +2223,7 @@ async function filterAsync(array, callback) {
 
 async function findClickable(locator, locateFn) {
   locator = new Locator(locator);
-  if (!locator.isFuzzy()) return locateFn(locator.simplify(), true);
+  if (!locator.isFuzzy()) return locateFn(locator, true);
 
   let els;
   const literal = xpathLocator.literal(locator.value);
@@ -2213,7 +2243,7 @@ async function findClickable(locator, locateFn) {
 
 async function findFields(locator) {
   locator = new Locator(locator);
-  if (!locator.isFuzzy()) return this._locate(locator.simplify(), true);
+  if (!locator.isFuzzy()) return this._locate(locator, true);
 
   const literal = xpathLocator.literal(locator.value);
   let els = await this._locate(Locator.field.byText(literal));
@@ -2313,7 +2343,7 @@ async function proceedSeeCheckbox(assertType, field) {
 async function findCheckable(locator, locateFn) {
   let els;
   locator = new Locator(locator);
-  if (!locator.isFuzzy()) return locateFn(locator.simplify(), true);
+  if (!locator.isFuzzy()) return locateFn(locator, true);
 
   const literal = xpathLocator.literal(locator.value);
   els = await locateFn(Locator.checkable.byText(literal));
@@ -2362,12 +2392,13 @@ function getElementId(el) {
 
 function prepareLocateFn(context) {
   if (!context) return this._locate.bind(this);
-  let el;
   return (l) => {
     l = new Locator(l, 'css');
-    if (el) return this.browser.findElementsFromElement(el, l.type, l.value);
     return this._locate(context, true).then(async (res) => {
       assertElementExists(res, context, 'Context element');
+      if (l.react) {
+        return res[0].react$$(l.react, l.props || undefined);
+      }
       return res[0].$$(l.simplify());
     });
   };
diff --git a/lib/helper/clientscripts/nightmare.js b/lib/helper/clientscripts/nightmare.js
index c5ce5a0c..ac9d32de 100644
--- a/lib/helper/clientscripts/nightmare.js
+++ b/lib/helper/clientscripts/nightmare.js
@@ -139,5 +139,13 @@ if (!window.codeceptjs) {
     return element.dispatchEvent(event);
   };
 
+  codeceptjs.unCheckEl = function (el) {
+    const element = this.fetchElement(el);
+    const event = document.createEvent('HTMLEvents');
+    element.checked = false;
+    event.initEvent('change', true, true);
+    return element.dispatchEvent(event);
+  };
+
   window.codeceptjs = codeceptjs;
 }
diff --git a/lib/interfaces/gherkin.js b/lib/interfaces/gherkin.js
index 53b7fcdd..2dfa6f90 100644
--- a/lib/interfaces/gherkin.js
+++ b/lib/interfaces/gherkin.js
@@ -73,7 +73,7 @@ module.exports = (text) => {
           }
           const tags = child.tags.map(t => t.name);
           const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
-          const test = new Test(title, async () => runSteps(exampleSteps));
+          const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
           test.tags = suite.tags.concat(tags);
           suite.addTest(scenario.test(test));
         }
@@ -99,3 +99,19 @@ function transformTable(table) {
   }
   return str;
 }
+function addExampleInTable(exampleSteps, placeholders) {
+  const steps = JSON.parse(JSON.stringify(exampleSteps));
+  for (const placeholder in placeholders) {
+    steps.map((step) => {
+      step = Object.assign({}, step);
+      if (step.argument && step.argument.type === 'DataTable') {
+        for (const id in step.argument.rows) {
+          const cells = step.argument.rows[id].cells;
+          cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])));
+        }
+      }
+      return step;
+    });
+  }
+  return steps;
+}
diff --git a/lib/listener/trace.js b/lib/listener/trace.js
index 2e22dfcb..57426aed 100644
--- a/lib/listener/trace.js
+++ b/lib/listener/trace.js
@@ -26,6 +26,10 @@ module.exports = function () {
       err.stack = ''; // hide internal error stack trace in non-verbose mode
     }
 
+    if (err.stack === undefined) {
+      err.stack = '';
+    }
+
     err.stack = msg + err.stack;
     test.err = err;
   });
diff --git a/lib/parser.js b/lib/parser.js
index 99eaa446..f466dad8 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -60,7 +60,7 @@ function guessParamTypes(paramName) {
     case 'field':
     case 'context':
     case 'select':
-      return ['ILocator', 'string'];
+      return ['LocatorOrString'];
     case 'num':
     case 'sec':
     case 'width':
diff --git a/lib/plugin/autoLogin.js b/lib/plugin/autoLogin.js
index 96886c92..cec7ffcf 100644
--- a/lib/plugin/autoLogin.js
+++ b/lib/plugin/autoLogin.js
@@ -5,6 +5,7 @@ const container = require('../container');
 const store = require('../store');
 const recorder = require('../recorder');
 const debug = require('../output').debug;
+const isAsyncFunction = require('../utils').isAsyncFunction;
 
 const defaultUser = {
   fetch: I => I.grabCookie(),
@@ -176,6 +177,40 @@ const defaultConfig = {
  * }
  * ```
  *
+ * #### Tips: Using async function in the autoLogin
+ *
+ * If you use async functions in the autoLogin plugin, login function should be used with `await` keyword.
+ *
+ * ```js
+ * autoLogin: {
+ *   enabled: true,
+ *   saveToFile: true,
+ *   inject: 'login',
+ *   users: {
+ *     admin: {
+ *       login: async (I) => {  // If you use async function in the autoLogin plugin
+ *          const phrase = await I.grabTextFrom('#phrase')
+ *          I.fillField('username', 'admin'),
+ *          I.fillField('password', 'password')
+ *          I.fillField('phrase', phrase)
+ *       },
+ *       check: (I) => {
+ *          I.amOnPage('/');
+ *          I.see('Admin');
+ *       }
+ *     }
+ *   }
+ * }
+ * ```
+ *
+ * ```js
+ * Scenario('login', async (I, login) => {
+ *   await login('admin') // you should use `await`
+ * })
+ * ```
+ *
+ *
+ *
 */
 module.exports = function (config) {
   config = Object.assign(defaultConfig, config);
@@ -200,9 +235,16 @@ module.exports = function (config) {
     const userSession = config.users[name];
     const I = container.support('I');
     const cookies = store[`${name}_session`];
+    const shouldAwait = isAsyncFunction(userSession.login)
+      || isAsyncFunction(userSession.restore)
+      || isAsyncFunction(userSession.check);
 
     const loginAndSave = async () => {
-      await userSession.login(I);
+      if (shouldAwait) {
+        await userSession.login(I);
+      } else {
+        userSession.login(I);
+      }
       store.debugMode = true;
       const cookies = await userSession.fetch(I);
       if (config.saveToFile) {
@@ -218,8 +260,13 @@ module.exports = function (config) {
     store.debugMode = true;
 
     recorder.session.start('check login');
-    await userSession.restore(I, cookies);
-    await userSession.check(I);
+    if (shouldAwait) {
+      await userSession.restore(I, cookies);
+      await userSession.check(I);
+    } else {
+      userSession.restore(I, cookies);
+      userSession.check(I);
+    }
     recorder.session.catch((err) => {
       debug(`Failed auto login for ${name} due to ${err}`);
       debug('Logging in again');
diff --git a/lib/plugin/puppeteerCoverage.js b/lib/plugin/puppeteerCoverage.js
index d1ffdca6..76d60373 100644
--- a/lib/plugin/puppeteerCoverage.js
+++ b/lib/plugin/puppeteerCoverage.js
@@ -130,7 +130,12 @@ module.exports = function (config) {
               process.cwd(),
               options.coverageDir,
             );
-            fs.mkdirSync(coverageDir, { recursive: true });
+
+            // Checking if coverageDir already exists, if not, create new one
+
+            if (!fs.existsSync(coverageDir)) {
+              fs.mkdirSync(coverageDir, { recursive: true });
+            }
 
             const coveragePath = path.resolve(
               coverageDir,
diff --git a/lib/recorder.js b/lib/recorder.js
index 400d47f1..6d6b9553 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -113,8 +113,11 @@ module.exports = {
    * @param {*} taskName
    * @param {*} fn
    * @param {*} force
+   * @param {boolean} retry -
+   *     true: it will retries if `retryOpts` set.
+   *     false: ignore `retryOpts` and won't retry.
    */
-  add(taskName, fn = undefined, force = false) {
+  add(taskName, fn = undefined, force = false, retry = true) {
     if (typeof taskName === 'function') {
       fn = taskName;
       taskName = fn.toString();
@@ -129,7 +132,7 @@ module.exports = {
     return promise = Promise.resolve(promise).then((res) => {
       const retryOpts = this.retries.slice(-1).pop();
       // no retries or unnamed tasks
-      if (!retryOpts || !taskName) {
+      if (!retryOpts || !taskName || !retry) {
         return Promise.resolve(res).then(fn);
       }
 
diff --git a/lib/session.js b/lib/session.js
index cd7cc94c..aae090fe 100644
--- a/lib/session.js
+++ b/lib/session.js
@@ -117,7 +117,7 @@ function session(sessionName, config, fn) {
     }
     finalize();
     return recorder.promise().then(() => res);
-  });
+  }, false, false);
 }
 
 
diff --git a/lib/within.js b/lib/within.js
index f3db724c..e40a2c5c 100644
--- a/lib/within.js
+++ b/lib/within.js
@@ -58,7 +58,7 @@ function within(context, fn) {
     }
     finalize();
     return recorder.promise().then(() => res);
-  });
+  }, false, false);
 }
 
 module.exports = within;
diff --git a/package.json b/package.json
index 7b6e0be6..1877d4c7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "codeceptjs",
-  "version": "2.1.0",
+  "version": "2.1.3",
   "description": "Modern Era Acceptance Testing Framework for NodeJS",
   "keywords": [
     "acceptance",
@@ -39,7 +39,7 @@
   "dependencies": {
     "allure-js-commons": "^1.3.2",
     "archiver": "^3.0.0",
-    "axios": "^0.18.0",
+    "axios": "^0.19.0",
     "chalk": "^1.1.3",
     "commander": "^2.20.0",
     "css-to-xpath": "^0.1.0",
@@ -49,11 +49,12 @@
     "fn-args": "^4.0.0",
     "gherkin": "^5.1.0",
     "glob": "^6.0.1",
-    "inquirer": "^6.2.2",
+    "inquirer": "^6.3.1",
     "js-beautify": "^1.9.1",
     "lodash.merge": "^4.6.1",
     "mkdirp": "^0.5.1",
     "mocha": "^4.1.0",
+    "mocha-junit-reporter": "^1.22.0",
     "parse-function": "^5.2.10",
     "promise-retry": "^1.1.1",
     "requireg": "^0.1.8",
@@ -61,36 +62,36 @@
   },
   "devDependencies": {
     "@types/inquirer": "^0.0.35",
-    "@types/node": "^8.10.45",
-    "@wdio/sauce-service": "^5.7.8",
-    "@wdio/selenium-standalone-service": "^5.7.8",
-    "@wdio/utils": "^5.7.8",
+    "@types/node": "^8.10.48",
+    "@wdio/sauce-service": "^5.8.0",
+    "@wdio/selenium-standalone-service": "^5.8.0",
+    "@wdio/utils": "^5.8.0",
     "chai": "^3.4.1",
     "chai-as-promised": "^5.2.0",
     "co-mocha": "^1.2",
     "documentation": "^8.1.2",
     "eslint": "^4.17.0",
     "eslint-config-airbnb-base": "^12.1.0",
-    "eslint-plugin-import": "^2.16.0",
+    "eslint-plugin-import": "^2.17.2",
     "eslint-plugin-mocha": "^5.3.0",
     "faker": "^4.1.0",
-    "gulp": "^4.0.0",
+    "gulp": "^4.0.1",
     "gulp-append-prepend": "^1.0.8",
     "gulp-documentation": "^3.2.1",
     "gulp-mustache": "^2.2.0",
     "husky": "^1.2.1",
     "json-server": "^0.10.1",
-    "nightmare": "^3.0.0",
+    "nightmare": "^3.0.2",
     "nyc": "^11.9.0",
     "protractor": "^5.4.1",
-    "puppeteer": "^1.14.0",
+    "puppeteer": "^1.15.0",
     "rosie": "^1.6.0",
     "sinon": "^1.17.2",
     "sinon-chai": "^2.14.0",
     "typescript": "^2.9.2",
     "unirest": "^0.5.1",
     "wdio-docker-service": "^1.5.0",
-    "webdriverio": "^5.7.0",
+    "webdriverio": "^5.8.0",
     "xmldom": "^0.1.27",
     "xpath": "0.0.27"
   },
diff --git a/typings/index.d.ts b/typings/index.d.ts
index ce5fc642..85a76d30 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -36,6 +36,8 @@ declare module NodeJS {
     session: any;
     DataTable: any;
     locate: Locator,
+    inject: any,
+    secret: any,
     by: any;
 
     // Used by Protractor helper


Test patch:
diff --git a/examples/absolutenet_test.js b/examples/absolutenet_test.js
deleted file mode 100644
index b4dc65ee..00000000
--- a/examples/absolutenet_test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-Feature('Testing Begins');
-
-// test with some demo params
-Scenario('ANI testing', { id: 123, user_id: 1235 }, function* (I) {
-  I.amOnPage('http://www.absolutenet.com/');
-  const title = yield I.grabTitle();
-  // console.info(title);
-  I.see('bogus text that is not there'); // this should give an error and not success.
-});
diff --git a/examples/react_test.js b/examples/react_test.js
new file mode 100644
index 00000000..47e10c3d
--- /dev/null
+++ b/examples/react_test.js
@@ -0,0 +1,11 @@
+Feature('React Apps');
+
+Scenario('try react app', (I) => {
+  I.amOnPage('https://ahfarmer.github.io/calculator/');
+  I.click('7');
+  I.seeElement({ react: 't', props: { name: '5' } });
+  I.click('button', { react: 't', props: { name: '2' } });
+  I.click('button', { react: 't', props: { name: '+' } });
+  I.click('button', { react: 't', props: { name: '9' } });
+  I.seeElement({ react: 't', props: { value: '81' } });
+});
diff --git a/test/data/I.js b/test/data/I.js
index 3928ac50..76a70cad 100644
--- a/test/data/I.js
+++ b/test/data/I.js
@@ -1,9 +1,7 @@
 module.exports = {
-
   _init: () => {
     global.I_initialized = true;
   },
 
   doSomething: () => 'done',
-
 };
diff --git a/test/data/dummy_page.js b/test/data/dummy_page.js
index 6a31d7a1..49c79338 100644
--- a/test/data/dummy_page.js
+++ b/test/data/dummy_page.js
@@ -1,3 +1,6 @@
+const { I } = inject();
+
 module.exports = {
   openDummyPage: () => 'dummy page opened',
+  getI: () => I,
 };
diff --git a/test/data/sandbox/features/IncludeExamplesInDataTable.feature b/test/data/sandbox/features/IncludeExamplesInDataTable.feature
new file mode 100644
index 00000000..8b9338f3
--- /dev/null
+++ b/test/data/sandbox/features/IncludeExamplesInDataTable.feature
@@ -0,0 +1,17 @@
+Feature: Include Examples in dataTtable placeholder
+
+  @IncludeExamplesIndataTtable
+  Scenario Outline: order a product with discount
+    Given I have this product in my cart
+      | data     | value      |
+      | name     | <name>     |
+      | price    | <price>    |
+      | discount | <discount> |
+      | quantity | <quantity> |
+
+    Then I should see total price is "<total>" $
+
+    Examples:
+      | name         | price | discount | quantity | total |
+      | iPhone 5     | 10    | 1        | 2        | 19.0  |
+      | Nuclear Bomb | 20    | 2        | 1        | 18.0  |
diff --git a/test/data/sandbox/features/step_definitions/my_steps.js b/test/data/sandbox/features/step_definitions/my_steps.js
index c536c028..4d02a0ae 100644
--- a/test/data/sandbox/features/step_definitions/my_steps.js
+++ b/test/data/sandbox/features/step_definitions/my_steps.js
@@ -35,6 +35,18 @@ Given('I login', () => {
   I.login('user', secret('password'));
 });
 
+Given(/^I have this product in my cart$/, (table) => {
+  let str = '';
+  for (const id in table.rows) {
+    const cells = table.rows[id].cells;
+    str += cells.map(c => c.value).map(c => c.slice(0, 15).padEnd(15)).join(' | ');
+    str += '\n';
+  }
+  console.log(str);
+});
+
+Then(/^I should see total price is "([^"]*)" \$$/, (price) => {
+});
 
 Before((test) => {
   console.log(`-- before ${test.title} --`);
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index f8fa6332..b80e162d 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -2,6 +2,7 @@ const TestHelper = require('../support/TestHelper');
 const Puppeteer = require('../../lib/helper/Puppeteer');
 const puppeteer = require('puppeteer');
 const should = require('chai').should();
+const expect = require('chai').expect;
 const assert = require('assert');
 const path = require('path');
 const fs = require('fs');
@@ -73,6 +74,17 @@ describe('Puppeteer', function () {
     });
   });
 
+  describe('grabDataFromPerformanceTiming', () => {
+    it('should return data from performance timing', async () => {
+      await I.amOnPage('/');
+      const res = await I.grabDataFromPerformanceTiming();
+      expect(res).to.have.property('responseEnd');
+      expect(res).to.have.property('domInteractive');
+      expect(res).to.have.property('domContentLoadedEventEnd');
+      expect(res).to.have.property('loadEventEnd');
+    });
+  });
+
   webApiTests.tests();
 
   describe('#waitForFunction', () => {
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 65374b4a..d778d960 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -499,6 +499,19 @@ describe('WebDriver', function () {
       .then(() => wd.see('Height 700', '#height'))
       .then(() => wd.see('Width 500', '#width')));
 
+    it('should set window size on new session', () => wd.amOnPage('/info')
+      .then(() => wd._session())
+      .then(session => session.start()
+        .then(browser => ({
+          browser,
+          session,
+        })))
+      .then(({ session, browser }) => session.loadVars(browser))
+      .then(() => wd.amOnPage('/form/resize'))
+      .then(() => wd.click('Window Size'))
+      .then(() => wd.see('Height 700', '#height'))
+      .then(() => wd.see('Width 500', '#width')));
+
     it('should resize window to specific dimensions', () => wd.amOnPage('/form/resize')
       .then(() => wd.resizeWindow(950, 600))
       .then(() => wd.click('Window Size'))
@@ -664,4 +677,20 @@ describe('WebDriver', function () {
       assert.notEqual(before, after);
     });
   });
+
+  describe('#uncheckOption', () => {
+    it('should uncheck option that is currently checked', async () => {
+      await wd.amOnPage('/info');
+      await wd.uncheckOption('interesting');
+      await wd.dontSeeCheckboxIsChecked('interesting');
+    });
+
+    it('should NOT uncheck option that is NOT currently checked', async () => {
+      await wd.amOnPage('/info');
+      await wd.uncheckOption('interesting');
+      // Unchecking again should not affect the current 'unchecked' status
+      await wd.uncheckOption('interesting');
+      await wd.dontSeeCheckboxIsChecked('interesting');
+    });
+  });
 });
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index f9fa40c1..b34b2f4e 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -290,6 +290,14 @@ module.exports.tests = function () {
     });
   });
 
+  describe('#uncheckOption', () => {
+    it('should uncheck option that is currently checked', async () => {
+      await I.amOnPage('/info');
+      await I.uncheckOption('interesting');
+      await I.dontSeeCheckboxIsChecked('interesting');
+    });
+  });
+
   describe('#selectOption', () => {
     it('should select option by css', function* () {
       yield I.amOnPage('/form/select');
diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js
index 8feb9cbb..20bb3bac 100644
--- a/test/rest/REST_test.js
+++ b/test/rest/REST_test.js
@@ -113,4 +113,18 @@ describe('REST', () => {
       });
     });
   });
+
+  describe('_url autocompletion', () => {
+    it('should not prepend base url, when url is absolute', () => {
+      I._url('https://bla.bla/blabla').should.eql('https://bla.bla/blabla');
+    });
+
+    it('should prepend base url, when url is not absolute', () => {
+      I._url('/blabla').should.eql(`${api_url}/blabla`);
+    });
+
+    it('should prepend base url, when url is not absolute, and "http" in request', () => {
+      I._url('/blabla&p=http://bla.bla').should.eql(`${api_url}/blabla&p=http://bla.bla`);
+    });
+  });
 });
diff --git a/test/runner/bdd_test.js b/test/runner/bdd_test.js
index f599a993..94552462 100644
--- a/test/runner/bdd_test.js
+++ b/test/runner/bdd_test.js
@@ -77,6 +77,17 @@ describe('BDD Gherkin', () => {
     });
   });
 
+  it('should run feature with table and examples files', (done) => {
+    exec(config_run_config('codecept.bdd.json') + ' --steps --grep "Include Examples in dataTtable placeholder"', (err, stdout, stderr) => { //eslint-disable-line
+      stdout.should.include('name            | Nuclear Bomb ');
+      stdout.should.include('price           | 20 ');
+      stdout.should.include('name            | iPhone 5 ');
+      stdout.should.include('price           | 10 ');
+      assert(!err);
+      done();
+    });
+  });
+
   it('should run feature with tables', (done) => {
     exec(config_run_config('codecept.bdd.json') + ' --steps --grep "Checkout products"', (err, stdout, stderr) => { //eslint-disable-line
       stdout.should.include('Given I have products in my cart');
diff --git a/test/runner/list_test.js b/test/runner/list_test.js
index f8978206..816204dc 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -25,7 +25,6 @@ describe('list/def commands', () => {
     }
     exec(`${runner} def ${codecept_dir}`, (err, stdout, stderr) => {
       stdout.should.include('Definitions were generated in steps.d.ts');
-      stdout.should.include('<reference path="./steps.d.ts" />');
       fs.existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
       const def = fs.readFileSync(`${codecept_dir}/steps.d.ts`).toString();
       def.should.include('amInPath(openPath: string) : void');
@@ -43,7 +42,6 @@ describe('list/def commands', () => {
     }
     exec(`${runner} def --config ${codecept_dir}/codecept.ddt.json`, (err, stdout, stderr) => {
       stdout.should.include('Definitions were generated in steps.d.ts');
-      stdout.should.include('<reference path="./steps.d.ts" />');
       fs.existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
       assert(!err);
       done();
diff --git a/test/unit/container_test.js b/test/unit/container_test.js
index ed2a3013..7d3b2189 100644
--- a/test/unit/container_test.js
+++ b/test/unit/container_test.js
@@ -2,14 +2,21 @@ const container = require('../../lib/container');
 const assert = require('assert');
 const path = require('path');
 const FileSystem = require('../../lib/helper/FileSystem');
+const actor = require('../../lib/actor');
 
 describe('Container', () => {
   before(() => {
     global.codecept_dir = path.join(__dirname, '/..');
+    global.inject = container.support;
+    global.actor = actor;
   });
 
   afterEach(() => {
     container.clear();
+    ['I', 'dummy_page'].forEach((po) => {
+      const name = require.resolve(path.join(__dirname, `../data/${po}`));
+      delete require.cache[name];
+    });
   });
 
 
@@ -121,7 +128,7 @@ describe('Container', () => {
         },
       });
       assert.ok(container.support('I'));
-      container.support('I').should.have.keys('_init', 'doSomething');
+      container.support('I').should.include.keys('_init', 'doSomething');
       assert(global.I_initialized);
     });
 
@@ -132,6 +139,44 @@ describe('Container', () => {
         },
       });
       assert.ok(container.support('dummyPage'));
+      container.support('dummyPage').should.include.keys('openDummyPage');
+    });
+
+    it('should load DI and inject I into PO', () => {
+      container.create({
+        include: {
+          dummyPage: './data/dummy_page',
+        },
+      });
+      assert.ok(container.support('dummyPage'));
+      assert.ok(container.support('I'));
+      container.support('dummyPage').should.include.keys('openDummyPage');
+      container.support('dummyPage').getI().should.have.keys(Object.keys(container.support('I')));
+    });
+
+
+    it('should load DI and inject custom I into PO', () => {
+      container.create({
+        include: {
+          dummyPage: './data/dummy_page',
+          I: './data/I',
+        },
+      });
+      assert.ok(container.support('dummyPage'));
+      assert.ok(container.support('I'));
+      container.support('dummyPage').should.include.keys('openDummyPage');
+      container.support('dummyPage').getI().should.include.keys(Object.keys(container.support('I')));
+    });
+
+    it('should load DI includes provided as objects', () => {
+      container.create({
+        include: {
+          dummyPage: {
+            openDummyPage: () => 'dummy page opened',
+          },
+        },
+      });
+      assert.ok(container.support('dummyPage'));
       container.support('dummyPage').should.have.keys('openDummyPage');
     });
 
diff --git a/test/unit/data/ui_test.js b/test/unit/data/ui_test.js
index 19f13f0c..ad079c8d 100644
--- a/test/unit/data/ui_test.js
+++ b/test/unit/data/ui_test.js
@@ -78,5 +78,21 @@ describe('ui', () => {
 
       dataScenarioConfig.scenarios.forEach(scenario => should.equal(helper, scenario.test.config[helperName]));
     });
+
+    it("should shows object's toString() method in each scenario's name if the toString() method is overrided", () => {
+      const data = [
+        {
+          toString: () => 'test case title',
+        },
+      ];
+      const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {});
+      should.equal('scenario | test case title', dataScenarioConfig.scenarios[0].test.title);
+    });
+
+    it("should shows JSON.stringify() in each scenario's name if the toString() method isn't overrided", () => {
+      const data = [{ name: 'John Do' }];
+      const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {});
+      should.equal(`scenario | ${JSON.stringify(data[0])}`, dataScenarioConfig.scenarios[0].test.title);
+    });
   });
 });
diff --git a/test/unit/locator_test.js b/test/unit/locator_test.js
index e4b774e5..f068f5e2 100644
--- a/test/unit/locator_test.js
+++ b/test/unit/locator_test.js
@@ -61,6 +61,64 @@ describe('Locator', () => {
     doc = new Dom().parseFromString(xml);
   });
 
+  describe('constructor', () => {
+    describe('with string argument', () => {
+      it('should create css locator', () => {
+        const l = new Locator('#foo');
+        expect(l.type).to.equal('css');
+        expect(l.value).to.equal('#foo');
+        expect(l.toString()).to.equal('#foo');
+      });
+
+      it('should create xpath locator', () => {
+        const l = new Locator('//foo[@bar="baz"]/*');
+        expect(l.type).to.equal('xpath');
+        expect(l.value).to.equal('//foo[@bar="baz"]/*');
+        expect(l.toString()).to.equal('//foo[@bar="baz"]/*');
+      });
+
+      it('should create fuzzy locator', () => {
+        const l = new Locator('foo');
+        expect(l.type).to.equal('fuzzy');
+        expect(l.value).to.equal('foo');
+        expect(l.toString()).to.equal('foo');
+      });
+
+      it('should create described custom default type locator', () => {
+        const l = new Locator('foo', 'defaultLocator');
+        expect(l.type).to.equal('defaultLocator');
+        expect(l.value).to.equal('foo');
+        expect(l.toString()).to.equal('foo');
+      });
+    });
+
+    describe('with object argument', () => {
+      it('should create id locator', () => {
+        const l = new Locator({ id: 'foo' });
+        expect(l.type).to.equal('id');
+        expect(l.value).to.equal('foo');
+        expect(l.toString()).to.equal('{id: foo}');
+      });
+
+      it('should create described custom locator', () => {
+        const l = new Locator({ customLocator: '=foo' });
+        expect(l.type).to.equal('customLocator');
+        expect(l.value).to.equal('=foo');
+        expect(l.toString()).to.equal('{customLocator: =foo}');
+      });
+    });
+
+    describe('with Locator object argument', () => {
+      it('should create id locator', () => {
+        const l = new Locator(new Locator({ id: 'foo' }));
+        expect(l).to.eql(new Locator({ id: 'foo' }));
+        expect(l.type).to.equal('id');
+        expect(l.value).to.equal('foo');
+        expect(l.toString()).to.equal('{id: foo}');
+      });
+    });
+  });
+
   it('should transform CSS to xpath', () => {
     const l = new Locator('p > #user', 'css');
     const nodes = xpath.select(l.toXPath(), doc);
diff --git a/test/unit/parser_test.js b/test/unit/parser_test.js
index 5b6c22aa..1a87fe8c 100644
--- a/test/unit/parser_test.js
+++ b/test/unit/parser_test.js
@@ -28,30 +28,22 @@ describe('parser', () => {
   describe('#toTypeDef', () => {
     it('should transform function to TS types', () => {
       const res = parser.toTypeDef(obj.method1);
-      expect(res).to.include('    method1(locator: ILocator, sec: number) : void');
-      expect(res).to.include('    method1(locator: string, sec: number) : void');
+      expect(res).to.include('    method1(locator: LocatorOrString, sec: number) : void');
     });
 
     it('should transform function to TS types', () => {
       const res = parser.toTypeDef(obj.method2);
-      expect(res).to.include('method2(locator: ILocator, value: string, sec: number) : void');
-      expect(res).to.include('method2(locator: string, value: string, sec: number) : void');
+      expect(res).to.include('method2(locator: LocatorOrString, value: string, sec: number) : void');
     });
 
     it('should transform function to TS types', () => {
       const res = parser.toTypeDef(obj.method3);
-      expect(res).to.include('method3(locator: ILocator, context: ILocator) : void');
-      expect(res).to.include('method3(locator: ILocator, context: string) : void');
-      expect(res).to.include('method3(locator: string, context: ILocator) : void');
-      expect(res).to.include('method3(locator: string, context: string) : void');
+      expect(res).to.include('method3(locator: LocatorOrString, context: LocatorOrString) : void');
     });
 
     it('should transform function to TS types', () => {
       const res = parser.toTypeDef(obj.method4);
-      expect(res).to.include('method4(locator: ILocator, context: ILocator) : void');
-      expect(res).to.include('method4(locator: ILocator, context: string) : void');
-      expect(res).to.include('method4(locator: string, context: ILocator) : void');
-      expect(res).to.include('method4(locator: string, context: string) : void');
+      expect(res).to.include('method4(locator: LocatorOrString, context: LocatorOrString) : void');
     });
   });
 });
diff --git a/test/unit/plugin/retryFailedStep_test.js b/test/unit/plugin/retryFailedStep_test.js
new file mode 100644
index 00000000..9ef0fc6b
--- /dev/null
+++ b/test/unit/plugin/retryFailedStep_test.js
@@ -0,0 +1,74 @@
+const retryFailedStep = require('../../../lib/plugin/retryFailedStep');
+const sinon = require('sinon');
+const within = require('../../../lib/within');
+const session = require('../../../lib/session');
+const container = require('../../../lib/container');
+const event = require('../../../lib/event');
+const recorder = require('../../../lib/recorder');
+const assert = require('assert');
+
+describe('retryFailedStep', () => {
+  beforeEach(() => {
+    container.clear({
+      mock: {
+        _session: () => {},
+      },
+    });
+    recorder.start();
+  });
+  it('should retry failed step', async () => {
+    retryFailedStep({ retries: 2, minTimeout: 1 });
+    event.dispatcher.emit(event.test.before);
+
+    let counter = 0;
+    recorder.add(() => {
+      counter++;
+      if (counter < 3) {
+        throw new Error();
+      }
+    });
+    return recorder.promise();
+  });
+  it('should not retry within', async () => {
+    retryFailedStep({ retries: 1, minTimeout: 1 });
+    event.dispatcher.emit(event.test.before);
+
+    let counter = 0;
+
+    try {
+      within('foo', () => {
+        recorder.add(() => {
+          counter++;
+          throw new Error();
+        });
+      });
+      await recorder.promise();
+    } catch (e) {
+      recorder.catchWithoutStop((err) => {});
+    }
+
+    // expects to retry only once
+    counter.should.equal(2);
+  });
+  it('should not retry session', async () => {
+    retryFailedStep({ retries: 1, minTimeout: 1 });
+    event.dispatcher.emit(event.test.before);
+
+    let counter = 0;
+
+    try {
+      session('foo', () => {
+        recorder.add(() => {
+          counter++;
+          throw new Error();
+        });
+      });
+      await recorder.promise();
+    } catch (e) {
+      recorder.catchWithoutStop((err) => {});
+    }
+
+    // expects to retry only once
+    counter.should.equal(2);
+  });
+});