Repository: Codeception/CodeceptJS
Pull request #1654 (2019-05-17 11:52:20)

Feat/uncheck option for nightmare and protractor
Introduce the `unCheckOption` for `Nightmare` and `Protractor`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4956c7a..e7f694df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,20 @@
+## 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
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/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/changelog.md b/docs/changelog.md
index b4956c7a..3c186ef8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,3 +1,15 @@
+## 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
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 1f2c3a9c..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)
 
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/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 ef68d1d7..ca456bc7 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -9,23 +9,24 @@ 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 {
@@ -110,20 +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 inject(): { [key: string]: any };
 declare function locate(selector: LocatorOrString): Locator;
 declare function within(selector: LocatorOrString, callback: Function): Promise<any>;
-declare function session(selector: string, callback: Function): Promise<any>;
-declare function session(selector: string, config: any, 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;
 
@@ -135,7 +147,7 @@ declare namespace CodeceptJS {
 }
 
 declare module "codeceptjs" {
-    export = CodeceptJS;
+  export = CodeceptJS;
 }
 `;
 
@@ -151,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);
 
@@ -180,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/container.js b/lib/container.js
index e8227b2b..c6f5ee94 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -32,8 +32,8 @@ class Container {
     container.mocha = MochaFactory.create(mochaConfig, opts || {});
     container.helpers = createHelpers(config.helpers || {});
     container.translation = loadTranslation(config.translation || null);
-    container.plugins = createPlugins(config.plugins || {}, opts);
     container.support = createSupportObjects(config.include || {});
+    container.plugins = createPlugins(config.plugins || {}, opts);
     if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
   }
 
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/WebDriver.js b/lib/helper/WebDriver.js
index f6bea0f7..487d9eb8 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -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();
@@ -685,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);
   }
 
   /**
@@ -885,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);
   }
 
@@ -904,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);
   }
 
@@ -1613,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));
   }
 
   /**
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/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/package.json b/package.json
index 2e3fce4a..1877d4c7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "codeceptjs",
-  "version": "2.1.1",
+  "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",
@@ -54,6 +54,7 @@
     "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",


Test patch:
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/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/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);
+    });
   });
 });