Repository: Codeception/CodeceptJS
Pull request #1671 (2019-05-24 11:37:18)

Fix 'ERROR: ENAMETOOLONG' when creating screenshots directory
Issue https://github.com/Codeception/CodeceptJS/issues/1636
When browser config has many settings, it would try to create output folder with too long name. With this PR, it will hash the `browserConfig` object and use it as a part of output folder name.


Patch:
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/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/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 78678f5f..131f3ae7 100644
--- a/docs/helpers/Nightmare.md
+++ b/docs/helpers/Nightmare.md
@@ -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 08616ceb..23af1b00 100644
--- a/docs/helpers/Protractor.md
+++ b/docs/helpers/Protractor.md
@@ -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 44160996..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.
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index fb784eae..7ee71f85 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -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/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/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/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/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 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 8c07a8c7..1877d4c7 100644
--- a/package.json
+++ b/package.json
@@ -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",


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/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();